From a3ef1ac2d0a7ee28404573a5fc256dda9f562f60 Mon Sep 17 00:00:00 2001 From: tobrun Date: Wed, 9 Oct 2019 13:19:56 +0200 Subject: [PATCH] [build] - initial commit --- .editorconfig | 4 + .gitignore | 35 + .gitmodules | 12 + CHANGELOG.md | 1767 + DISTRIBUTE.md | 57 + LICENSE.md | 227 + MapboxGLAndroidSDK/.gitignore | 2 + MapboxGLAndroidSDK/build.gradle | 105 + MapboxGLAndroidSDK/lint.xml | 7 + MapboxGLAndroidSDK/proguard-rules.pro | 41 + .../src/main/AndroidManifest.xml | 15 + .../com/mapbox/mapboxsdk/AccountsManager.java | 170 + .../com/mapbox/mapboxsdk/LibraryLoader.java | 55 + .../mapboxsdk/LibraryLoaderProvider.java | 15 + .../com/mapbox/mapboxsdk/MapStrictMode.java | 46 + .../mapboxsdk/MapStrictModeException.java | 7 + .../java/com/mapbox/mapboxsdk/Mapbox.java | 225 + .../com/mapbox/mapboxsdk/ModuleProvider.java | 38 + .../mapbox/mapboxsdk/ModuleProviderImpl.java | 30 + .../mapboxsdk/annotations/Annotation.java | 153 + .../mapboxsdk/annotations/ArrowDirection.java | 31 + .../annotations/BaseMarkerOptions.java | 124 + .../annotations/BasePointCollection.java | 82 + .../mapbox/mapboxsdk/annotations/Bubble.java | 315 + .../mapboxsdk/annotations/BubbleLayout.java | 350 + .../annotations/BubblePopupHelper.java | 35 + .../mapbox/mapboxsdk/annotations/Icon.java | 124 + .../mapboxsdk/annotations/IconFactory.java | 188 + .../mapboxsdk/annotations/InfoWindow.java | 351 + .../mapbox/mapboxsdk/annotations/Marker.java | 278 + .../mapboxsdk/annotations/MarkerOptions.java | 195 + .../mapbox/mapboxsdk/annotations/Polygon.java | 109 + .../mapboxsdk/annotations/PolygonOptions.java | 308 + .../mapboxsdk/annotations/Polyline.java | 71 + .../annotations/PolylineOptions.java | 250 + .../mapboxsdk/annotations/package-info.java | 4 + .../mapboxsdk/attribution/Attribution.java | 70 + .../attribution/AttributionLayout.java | 68 + .../attribution/AttributionMeasure.java | 249 + .../attribution/AttributionParser.java | 314 + .../mapboxsdk/attribution/package-info.java | 4 + .../mapboxsdk/camera/CameraPosition.java | 397 + .../mapbox/mapboxsdk/camera/CameraUpdate.java | 23 + .../mapboxsdk/camera/CameraUpdateFactory.java | 637 + .../mapbox/mapboxsdk/camera/package-info.java | 4 + .../constants/GeometryConstants.java | 67 + .../mapboxsdk/constants/MapboxConstants.java | 232 + .../constants/TelemetryConstants.java | 14 + .../mapboxsdk/constants/package-info.java | 4 + .../CalledFromWorkerThreadException.java | 8 + .../exceptions/ConversionException.java | 11 + .../IconBitmapChangedException.java | 25 + .../InvalidLatLngBoundsException.java | 12 + .../InvalidMarkerPositionException.java | 16 + .../MapboxConfigurationException.java | 33 + .../exceptions/TooManyIconsException.java | 20 + .../mapboxsdk/exceptions/package-info.java | 4 + .../com/mapbox/mapboxsdk/geometry/LatLng.java | 334 + .../mapboxsdk/geometry/LatLngBounds.java | 608 + .../mapbox/mapboxsdk/geometry/LatLngQuad.java | 94 + .../mapbox/mapboxsdk/geometry/LatLngSpan.java | 139 + .../mapboxsdk/geometry/ProjectedMeters.java | 155 + .../mapboxsdk/geometry/VisibleRegion.java | 161 + .../mapboxsdk/geometry/package-info.java | 4 + .../mapbox/mapboxsdk/http/HttpIdentifier.java | 39 + .../com/mapbox/mapboxsdk/http/HttpLogger.java | 35 + .../mapbox/mapboxsdk/http/HttpRequest.java | 32 + .../mapbox/mapboxsdk/http/HttpRequestUrl.java | 53 + .../mapbox/mapboxsdk/http/HttpResponder.java | 32 + .../mapboxsdk/http/LocalRequestTask.java | 75 + .../mapboxsdk/http/NativeHttpRequest.java | 90 + .../mapbox/mapboxsdk/http/package-info.java | 4 + .../location/AnimatorListenerHolder.java | 45 + .../mapboxsdk/location/CompassEngine.java | 55 + .../mapboxsdk/location/CompassListener.java | 33 + .../mapboxsdk/location/LatLngEvaluator.java | 21 + .../location/LayerBitmapProvider.java | 32 + .../location/LayerFeatureProvider.java | 26 + .../location/LayerSourceProvider.java | 111 + .../location/LocationAnimatorCoordinator.java | 422 + .../location/LocationCameraController.java | 437 + .../mapboxsdk/location/LocationComponent.java | 1586 + .../LocationComponentActivationOptions.java | 244 + .../LocationComponentCompassEngine.java | 268 + .../location/LocationComponentConstants.java | 94 + ...ationComponentNotInitializedException.java | 8 + .../location/LocationComponentOptions.java | 1778 + .../LocationComponentPositionManager.java | 48 + .../location/LocationLayerController.java | 464 + .../mapboxsdk/location/MapboxAnimator.java | 95 + .../location/MapboxAnimatorProvider.java | 40 + .../location/MapboxAnimatorSetProvider.java | 32 + .../location/MapboxCameraAnimatorAdapter.java | 36 + .../location/MapboxFloatAnimator.java | 17 + .../location/MapboxLatLngAnimator.java | 20 + .../OnCameraMoveInvalidateListener.java | 7 + .../OnCameraTrackingChangedListener.java | 21 + .../OnLocationCameraTransitionListener.java | 24 + .../location/OnLocationClickListener.java | 14 + .../location/OnLocationLongClickListener.java | 14 + .../location/OnLocationStaleListener.java | 17 + .../location/OnRenderModeChangedListener.java | 16 + .../mapboxsdk/location/StaleStateManager.java | 99 + .../com/mapbox/mapboxsdk/location/Utils.java | 121 + .../mapboxsdk/location/modes/CameraMode.java | 66 + .../mapboxsdk/location/modes/RenderMode.java | 45 + .../location/modes/package-info.java | 4 + .../mapboxsdk/location/package-info.java | 4 + .../java/com/mapbox/mapboxsdk/log/Logger.java | 321 + .../mapboxsdk/log/LoggerDefinition.java | 102 + .../mapbox/mapboxsdk/log/package-info.java | 4 + .../mapboxsdk/maps/AnnotationContainer.java | 87 + .../mapboxsdk/maps/AnnotationManager.java | 519 + .../mapbox/mapboxsdk/maps/Annotations.java | 25 + .../maps/AttributionDialogManager.java | 208 + .../maps/CameraChangeDispatcher.java | 204 + .../maps/FocalPointChangeListener.java | 11 + .../mapbox/mapboxsdk/maps/IconManager.java | 155 + .../java/com/mapbox/mapboxsdk/maps/Image.java | 22 + .../mapboxsdk/maps/InfoWindowManager.java | 96 + .../mapboxsdk/maps/MapChangeReceiver.java | 395 + .../mapbox/mapboxsdk/maps/MapFragment.java | 241 + .../mapboxsdk/maps/MapGestureDetector.java | 1140 + .../mapbox/mapboxsdk/maps/MapKeyListener.java | 271 + .../com/mapbox/mapboxsdk/maps/MapView.java | 1350 + .../com/mapbox/mapboxsdk/maps/MapboxMap.java | 2391 + .../mapboxsdk/maps/MapboxMapOptions.java | 1242 + .../mapboxsdk/maps/MarkerContainer.java | 146 + .../com/mapbox/mapboxsdk/maps/Markers.java | 27 + .../com/mapbox/mapboxsdk/maps/NativeMap.java | 268 + .../mapbox/mapboxsdk/maps/NativeMapView.java | 1489 + .../mapboxsdk/maps/OnMapReadyCallback.java | 22 + .../mapboxsdk/maps/PolygonContainer.java | 84 + .../com/mapbox/mapboxsdk/maps/Polygons.java | 22 + .../mapboxsdk/maps/PolylineContainer.java | 83 + .../com/mapbox/mapboxsdk/maps/Polylines.java | 22 + .../com/mapbox/mapboxsdk/maps/Projection.java | 283 + .../maps/ShapeAnnotationContainer.java | 41 + .../mapboxsdk/maps/ShapeAnnotations.java | 13 + .../java/com/mapbox/mapboxsdk/maps/Style.java | 1150 + .../mapboxsdk/maps/SupportMapFragment.java | 219 + .../mapboxsdk/maps/TelemetryDefinition.java | 66 + .../com/mapbox/mapboxsdk/maps/Transform.java | 341 + .../com/mapbox/mapboxsdk/maps/UiSettings.java | 1106 + .../mapbox/mapboxsdk/maps/package-info.java | 4 + .../mapboxsdk/maps/renderer/MapRenderer.java | 158 + .../maps/renderer/MapRendererRunnable.java | 32 + .../maps/renderer/MapRendererScheduler.java | 17 + .../maps/renderer/egl/EGLConfigChooser.java | 328 + .../maps/renderer/egl/EGLConfigException.java | 21 + .../maps/renderer/egl/package-info.java | 4 + .../GLSurfaceViewMapRenderer.java | 115 + .../glsurfaceview/MapboxGLSurfaceView.java | 54 + .../renderer/glsurfaceview/package-info.java | 4 + .../mapboxsdk/maps/renderer/package-info.java | 4 + .../textureview/TextureViewMapRenderer.java | 115 + .../textureview/TextureViewRenderThread.java | 470 + .../renderer/textureview/package-info.java | 4 + .../mapboxsdk/maps/widgets/CompassView.java | 178 + .../mapboxsdk/maps/widgets/package-info.java | 4 + .../module/http/HttpRequestImpl.java | 196 + .../module/http/HttpRequestUtil.java | 71 + .../mapboxsdk/module/http/package-info.java | 4 + .../loader/LibraryLoaderProviderImpl.java | 49 + .../mapboxsdk/module/loader/package-info.java | 4 + .../module/telemetry/MapBaseEvent.java | 39 + .../module/telemetry/MapEventFactory.java | 57 + .../module/telemetry/MapLoadEvent.java | 179 + .../telemetry/OfflineDownloadEndEvent.java | 140 + .../telemetry/OfflineDownloadStartEvent.java | 98 + .../module/telemetry/PerformanceEvent.java | 179 + .../module/telemetry/PhoneState.java | 171 + .../module/telemetry/TelemetryImpl.java | 136 + .../module/telemetry/package-info.java | 4 + .../mapboxsdk/net/ConnectivityListener.java | 10 + .../mapboxsdk/net/ConnectivityReceiver.java | 160 + .../net/NativeConnectivityListener.java | 44 + .../mapbox/mapboxsdk/net/package-info.java | 4 + .../OfflineGeometryRegionDefinition.java | 175 + .../mapboxsdk/offline/OfflineManager.java | 682 + .../mapboxsdk/offline/OfflineRegion.java | 532 + .../offline/OfflineRegionDefinition.java | 72 + .../mapboxsdk/offline/OfflineRegionError.java | 92 + .../offline/OfflineRegionStatus.java | 172 + .../OfflineTilePyramidRegionDefinition.java | 162 + .../mapboxsdk/offline/package-info.java | 4 + .../com/mapbox/mapboxsdk/package-info.java | 4 + .../mapboxsdk/snapshotter/MapSnaphotUtil.java | 29 + .../mapboxsdk/snapshotter/MapSnapshot.java | 81 + .../mapboxsdk/snapshotter/MapSnapshotter.java | 661 + .../mapboxsdk/snapshotter/package-info.java | 4 + .../mapbox/mapboxsdk/storage/FileSource.java | 413 + .../mapbox/mapboxsdk/storage/Resource.java | 60 + .../mapboxsdk/storage/package-info.java | 4 + .../style/expressions/Expression.java | 4891 + .../style/expressions/package-info.java | 4 + .../style/layers/BackgroundLayer.java | 216 + .../style/layers/CannotAddLayerException.java | 15 + .../mapboxsdk/style/layers/CircleLayer.java | 584 + .../mapboxsdk/style/layers/CustomLayer.java | 37 + .../style/layers/FillExtrusionLayer.java | 463 + .../mapboxsdk/style/layers/FillLayer.java | 436 + .../mapboxsdk/style/layers/HeatmapLayer.java | 331 + .../style/layers/HillshadeLayer.java | 360 + .../mapbox/mapboxsdk/style/layers/Layer.java | 168 + .../style/layers/LayoutPropertyValue.java | 11 + .../mapboxsdk/style/layers/LineLayer.java | 676 + .../style/layers/PaintPropertyValue.java | 11 + .../mapboxsdk/style/layers/Property.java | 696 + .../style/layers/PropertyFactory.java | 2558 + .../mapboxsdk/style/layers/PropertyValue.java | 127 + .../mapboxsdk/style/layers/RasterLayer.java | 397 + .../mapboxsdk/style/layers/SymbolLayer.java | 1436 + .../style/layers/TransitionOptions.java | 136 + .../mapboxsdk/style/layers/layer.java.ejs | 250 + .../mapboxsdk/style/layers/package-info.java | 4 + .../mapboxsdk/style/layers/property.java.ejs | 61 + .../style/layers/property_factory.java.ejs | 100 + .../mapbox/mapboxsdk/style/light/Light.java | 242 + .../mapboxsdk/style/light/Position.java | 90 + .../mapboxsdk/style/light/light.java.ejs | 154 + .../mapboxsdk/style/light/package-info.java | 4 + .../sources/CannotAddSourceException.java | 15 + .../style/sources/CustomGeometrySource.java | 379 + .../sources/CustomGeometrySourceOptions.java | 85 + .../style/sources/GeoJsonOptions.java | 139 + .../style/sources/GeoJsonSource.java | 518 + .../style/sources/GeometryTileProvider.java | 22 + .../mapboxsdk/style/sources/ImageSource.java | 271 + .../style/sources/RasterDemSource.java | 185 + .../mapboxsdk/style/sources/RasterSource.java | 182 + .../mapboxsdk/style/sources/Source.java | 91 + .../mapboxsdk/style/sources/TileSet.java | 341 + .../style/sources/UnknownSource.java | 27 + .../mapboxsdk/style/sources/VectorSource.java | 171 + .../mapboxsdk/style/sources/package-info.java | 4 + .../mapboxsdk/style/types/Formatted.java | 69 + .../style/types/FormattedSection.java | 240 + .../mapboxsdk/style/types/package-info.java | 4 + .../mapboxsdk/text/LocalGlyphRasterizer.java | 59 + .../mapbox/mapboxsdk/text/package-info.java | 4 + .../mapbox/mapboxsdk/utils/AnimatorUtils.java | 168 + .../mapbox/mapboxsdk/utils/BitmapUtils.java | 189 + .../mapbox/mapboxsdk/utils/ColorUtils.java | 204 + .../com/mapbox/mapboxsdk/utils/Compare.java | 27 + .../com/mapbox/mapboxsdk/utils/FileUtils.java | 147 + .../com/mapbox/mapboxsdk/utils/FontUtils.java | 81 + .../mapboxsdk/utils/MapFragmentUtils.java | 51 + .../com/mapbox/mapboxsdk/utils/MathUtils.java | 64 + .../mapbox/mapboxsdk/utils/StringUtils.java | 27 + .../mapbox/mapboxsdk/utils/ThreadUtils.java | 48 + .../mapbox/mapboxsdk/utils/package-info.java | 4 + .../src/main/java/overview.html | 19 + .../src/main/res-public/values/public.xml | 162 + .../res/drawable-hdpi/mapbox_compass_icon.png | Bin 0 -> 3787 bytes .../res/drawable-hdpi/mapbox_logo_helmet.png | Bin 0 -> 1650 bytes .../res/drawable-hdpi/mapbox_logo_icon.png | Bin 0 -> 4778 bytes .../mapbox_marker_icon_default.png | Bin 0 -> 1520 bytes .../mapbox_markerview_icon_default.png | Bin 0 -> 1669 bytes .../mapbox_mylocation_icon_bearing.png | Bin 0 -> 1046 bytes .../mapbox_mylocation_icon_default.png | Bin 0 -> 885 bytes .../res/drawable-mdpi/mapbox_compass_icon.png | Bin 0 -> 2488 bytes .../res/drawable-mdpi/mapbox_logo_helmet.png | Bin 0 -> 950 bytes .../res/drawable-mdpi/mapbox_logo_icon.png | Bin 0 -> 2622 bytes .../mapbox_marker_icon_default.png | Bin 0 -> 1010 bytes .../mapbox_markerview_icon_default.png | Bin 0 -> 1115 bytes .../mapbox_mylocation_icon_bearing.png | Bin 0 -> 649 bytes .../mapbox_mylocation_icon_default.png | Bin 0 -> 555 bytes .../drawable-xhdpi/mapbox_compass_icon.png | Bin 0 -> 4775 bytes .../res/drawable-xhdpi/mapbox_logo_helmet.png | Bin 0 -> 2184 bytes .../res/drawable-xhdpi/mapbox_logo_icon.png | Bin 0 -> 6579 bytes .../mapbox_marker_icon_default.png | Bin 0 -> 1995 bytes .../mapbox_markerview_icon_default.png | Bin 0 -> 2163 bytes .../mapbox_mylocation_icon_bearing.png | Bin 0 -> 1345 bytes .../mapbox_mylocation_icon_default.png | Bin 0 -> 1096 bytes .../drawable-xxhdpi/mapbox_compass_icon.png | Bin 0 -> 7527 bytes .../drawable-xxhdpi/mapbox_logo_helmet.png | Bin 0 -> 3389 bytes .../res/drawable-xxhdpi/mapbox_logo_icon.png | Bin 0 -> 10802 bytes .../mapbox_marker_icon_default.png | Bin 0 -> 2998 bytes .../mapbox_markerview_icon_default.png | Bin 0 -> 3163 bytes .../mapbox_mylocation_icon_bearing.png | Bin 0 -> 1902 bytes .../mapbox_mylocation_icon_default.png | Bin 0 -> 1586 bytes .../drawable-xxxhdpi/mapbox_compass_icon.png | Bin 0 -> 18537 bytes .../drawable-xxxhdpi/mapbox_logo_helmet.png | Bin 0 -> 4468 bytes .../res/drawable-xxxhdpi/mapbox_logo_icon.png | Bin 0 -> 14441 bytes .../mapbox_marker_icon_default.png | Bin 0 -> 4006 bytes .../mapbox_markerview_icon_default.png | Bin 0 -> 4071 bytes .../mapbox_mylocation_icon_bearing.png | Bin 0 -> 3022 bytes .../mapbox_mylocation_icon_default.png | Bin 0 -> 2456 bytes .../res/drawable/mapbox_info_bg_selector.xml | 5 + .../res/drawable/mapbox_info_icon_default.xml | 9 + .../drawable/mapbox_info_icon_selected.xml | 9 + .../drawable/mapbox_mylocation_bg_shape.xml | 10 + .../mapbox_popup_window_transparent.xml | 12 + .../res/drawable/mapbox_rounded_corner.xml | 10 + .../res/drawable/mapbox_user_bearing_icon.xml | 10 + .../main/res/drawable/mapbox_user_icon.xml | 10 + .../res/drawable/mapbox_user_icon_shadow.xml | 19 + .../res/drawable/mapbox_user_icon_stale.xml | 10 + .../res/drawable/mapbox_user_puck_icon.xml | 17 + .../res/drawable/mapbox_user_stroke_icon.xml | 10 + .../layout/mapbox_attribution_list_item.xml | 14 + .../res/layout/mapbox_infowindow_content.xml | 53 + .../res/layout/mapbox_mapview_internal.xml | 27 + .../res/layout/mapbox_view_image_marker.xml | 6 + .../src/main/res/values-bg/strings.xml | 15 + .../src/main/res/values-ca/strings.xml | 15 + .../src/main/res/values-cs/strings.xml | 17 + .../src/main/res/values-es/strings.xml | 17 + .../src/main/res/values-fr/strings.xml | 17 + .../src/main/res/values-gl/strings.xml | 17 + .../src/main/res/values-he/strings.xml | 16 + .../src/main/res/values-hu/strings.xml | 15 + .../src/main/res/values-iw/strings.xml | 16 + .../src/main/res/values-ko/strings.xml | 16 + .../src/main/res/values-lt/strings.xml | 15 + .../src/main/res/values-nl/strings.xml | 15 + .../src/main/res/values-pl/strings.xml | 17 + .../src/main/res/values-pt-rPT/strings.xml | 16 + .../src/main/res/values-ru/strings.xml | 16 + .../src/main/res/values-sv/strings.xml | 16 + .../src/main/res/values-uk/strings.xml | 16 + .../src/main/res/values-vi/strings.xml | 17 + .../src/main/res/values-zh-rCN/strings.xml | 17 + .../src/main/res/values-zh-rHK/strings.xml | 17 + .../src/main/res/values-zh-rTW/strings.xml | 17 + .../src/main/res/values/attrs.xml | 186 + .../src/main/res/values/colors.xml | 9 + .../src/main/res/values/dimens.xml | 29 + .../src/main/res/values/strings.xml | 30 + .../src/main/res/values/styles.xml | 37 + .../mapbox/mapboxsdk/AccountsManagerTest.java | 59 + .../com/mapbox/mapboxsdk/MapboxInjector.java | 48 + .../java/com/mapbox/mapboxsdk/MapboxTest.java | 109 + .../mapboxsdk/annotations/AnnotationTest.java | 93 + .../mapboxsdk/annotations/IconTest.java | 56 + .../mapboxsdk/annotations/InfoWindowTest.java | 86 + .../mapboxsdk/annotations/MarkerTest.java | 160 + .../mapboxsdk/annotations/PolygonTest.java | 75 + .../mapboxsdk/annotations/PolylineTest.java | 75 + .../attribution/AttributionParseTest.java | 322 + .../mapboxsdk/camera/CameraPositionTest.java | 120 + .../mapboxsdk/constants/AppConstant.java | 6 + .../mapboxsdk/geometry/LatLngBoundsTest.java | 813 + .../mapboxsdk/geometry/LatLngSpanTest.java | 65 + .../mapbox/mapboxsdk/geometry/LatLngTest.java | 286 + .../geometry/ProjectedMetersTest.java | 66 + .../mapboxsdk/geometry/VisibleRegionTest.java | 95 + .../mapboxsdk/location/CompassEngineTest.java | 89 + .../LocationAnimatorCoordinatorTest.kt | 532 + .../LocationCameraControllerTest.java | 1001 + ...ocationComponentActivationOptionsTest.java | 119 + .../LocationComponentOptionsTest.java | 81 + .../LocationComponentPositionManagerTest.kt | 130 + .../location/LocationComponentTest.kt | 453 + .../location/LocationLayerControllerTest.java | 718 + .../mapboxsdk/location/MapboxAnimatorTest.kt | 43 + .../mapbox/mapboxsdk/location/UtilsTest.java | 28 + .../com/mapbox/mapboxsdk/log/LoggerTest.kt | 93 + .../mapboxsdk/maps/AnnotationManagerTest.java | 86 + .../mapboxsdk/maps/MapChangeReceiverTest.java | 586 + .../mapboxsdk/maps/MapTouchListenersTest.java | 157 + .../maps/MapboxMapOptionsAttrsTest.kt | 101 + .../mapboxsdk/maps/MapboxMapOptionsTest.java | 186 + .../mapbox/mapboxsdk/maps/MapboxMapTest.kt | 202 + .../mapbox/mapboxsdk/maps/StyleBuilderTest.kt | 237 + .../com/mapbox/mapboxsdk/maps/StyleTest.kt | 408 + .../mapbox/mapboxsdk/maps/TransformTest.kt | 183 + .../mapbox/mapboxsdk/maps/UiSettingsTest.java | 415 + .../module/http/HttpRequestUrlTest.kt | 67 + .../module/http/HttpRequestUtilTest.kt | 35 + .../module/telemetry/MapEventFactoryTest.java | 149 + .../module/telemetry/SchemaTest.java | 270 + .../style/expressions/ExpressionTest.java | 1537 + .../telemetry/HttpTransportTest.java | 20 + .../mapbox/mapboxsdk/utils/ColorUtilsTest.kt | 22 + .../mapbox/mapboxsdk/utils/MockParcel.java | 267 + .../org.mockito.plugins.MockMaker | 1 + MapboxGLAndroidSDKLint/.gitignore | 1 + MapboxGLAndroidSDKLint/build.gradle | 37 + .../com/mapbox/mapboxsdk/lint/KeepDetector.kt | 75 + .../mapboxsdk/lint/MapboxIssueRegistry.kt | 13 + .../mapbox/mapboxsdk/lint/KeepDetectorTest.kt | 28 + MapboxGLAndroidSDKTestApp/.gitignore | 2 + MapboxGLAndroidSDKTestApp/README.md | 11 + MapboxGLAndroidSDKTestApp/build.gradle | 93 + .../lint-baseline-local.xml | 166 + .../lint/lint-baseline-ci.xml | 177 + MapboxGLAndroidSDKTestApp/proguard-rules.pro | 14 + .../src/androidTest/AndroidManifest.xml | 4 + .../src/androidTest/assets/streets.json | 5331 + .../java/com/mapbox/mapboxsdk/AppCenter.kt | 15 + .../mapboxsdk/InstrumentationApplication.kt | 10 + .../mapbox/mapboxsdk/InstrumentationRunner.kt | 11 + .../camera/CameraUpdateFactoryTest.kt | 186 + .../integration/BaseIntegrationTest.kt | 40 + .../integration/FragmentBackStackTest.kt | 45 + .../integration/GLSurfaceViewReopenTest.kt | 29 + .../integration/GLSurfaceViewReuseTest.kt | 29 + .../integration/OrientationChangeTest.kt | 32 + .../QueryRenderedFeaturesBoxCountTest.kt | 30 + .../integration/TextureViewReopenTest.kt | 29 + .../integration/TextureViewReuseTest.kt | 29 + .../integration/ViewPagerScrollTest.kt | 36 + .../location/LocationComponentTest.kt | 1603 + .../location/LocationLayerControllerTest.kt | 609 + .../location/utils/LocationComponentAction.kt | 38 + .../utils/MapboxLocationTestingUtils.kt | 79 + .../utils/StyleChangeIdlingResource.kt | 40 + .../mapbox/mapboxsdk/maps/BaseLayerTest.kt | 27 + .../mapboxsdk/maps/GesturesUiTestUtils.kt | 516 + .../mapboxsdk/maps/IconManagerResolver.java | 42 + .../mapboxsdk/maps/MapGestureDetectorTest.kt | 193 + .../mapbox/mapboxsdk/maps/MapboxMapTest.java | 477 + .../com/mapbox/mapboxsdk/maps/MapboxTest.java | 111 + .../mapboxsdk/maps/NativeMapViewTest.kt | 404 + .../mapbox/mapboxsdk/maps/TransformTest.kt | 33 + .../mapboxsdk/maps/VisibleRegionTest.kt | 400 + .../mapboxsdk/offline/OfflineDownloadTest.kt | 85 + .../snapshotter/MapSnapshotterTest.kt | 57 + .../testapp/action/MapboxMapAction.java | 50 + .../testapp/action/OrientationAction.java | 78 + .../mapboxsdk/testapp/action/WaitAction.java | 39 + .../mapboxsdk/testapp/activity/BaseTest.java | 103 + .../testapp/activity/EspressoTest.java | 28 + .../testapp/activity/activity.junit.ejs | 32 + .../testapp/annotations/IconTest.java | 138 + .../testapp/annotations/MarkerTest.java | 63 + .../testapp/annotations/PolygonTest.java | 46 + .../testapp/annotations/PolylineTest.java | 42 + .../testapp/camera/CameraAnimateTest.java | 11 + .../testapp/camera/CameraEaseTest.java | 12 + .../testapp/camera/CameraForTest.java | 260 + .../testapp/camera/CameraMoveTest.java | 12 + .../mapboxsdk/testapp/camera/CameraTest.java | 307 + .../testapp/fragment/MapDialogFragmentTest.kt | 35 + .../geometry/GeoJsonConversionTest.java | 148 + .../testapp/geometry/LatLngBoundsTest.java | 65 + .../testapp/maps/ImageMissingTest.kt | 155 + .../testapp/maps/RemoveUnusedImagesTest.kt | 163 + .../mapboxsdk/testapp/maps/StyleLoadTest.kt | 31 + .../testapp/maps/widgets/AttributionTest.java | 217 + .../testapp/maps/widgets/CompassViewTest.java | 67 + .../testapp/maps/widgets/LogoTest.java | 60 + .../mapboxsdk/testapp/offline/CacheTest.kt | 88 + .../testapp/offline/OfflineManagerTest.kt | 124 + .../testapp/offline/OfflineUtilsTest.java | 41 + .../mapboxsdk/testapp/render/RenderTest.java | 74 + .../testapp/storage/FileSourceMapTest.kt | 59 + .../storage/FileSourceStandaloneTest.kt | 116 + .../testapp/storage/FileSourceTestUtils.kt | 59 + .../testapp/string/UppperLowerCaseTest.java | 47 + .../testapp/style/BackgroundLayerTest.java | 141 + .../testapp/style/CircleLayerTest.java | 453 + .../testapp/style/CustomGeometrySourceTest.kt | 69 + .../testapp/style/ExpressionTest.java | 786 + .../testapp/style/FillExtrusionLayerTest.java | 340 + .../testapp/style/FillLayerTest.java | 326 + .../testapp/style/GeoJsonSourceTests.java | 189 + .../testapp/style/HeatmapLayerTest.java | 215 + .../testapp/style/HillshadeLayerTest.java | 223 + .../mapboxsdk/testapp/style/ImageTest.kt | 75 + .../mapboxsdk/testapp/style/LightTest.java | 172 + .../testapp/style/LineLayerTest.java | 493 + .../testapp/style/RasterLayerTest.java | 240 + .../testapp/style/RuntimeStyleTests.java | 370 + .../style/RuntimeStyleTimingTests.java | 27 + .../testapp/style/StyleLoaderTest.java | 59 + .../testapp/style/SymbolLayerTest.java | 1313 + .../mapboxsdk/testapp/style/layer.junit.ejs | 195 + .../mapboxsdk/testapp/style/light.junit.ejs | 132 + .../utils/SnapshotterIdlingResource.java | 38 + .../testapp/utils/TestConstants.java | 12 + .../testapp/utils/TestingAsyncUtils.kt | 18 + .../mapbox/mapboxsdk/utils/FontUtilsTest.java | 41 + .../src/main/AndroidManifest.xml | 1029 + .../src/main/assets/Roboto-Medium.ttf | Bin 0 -> 127488 bytes .../src/main/assets/Roboto-Regular.ttf | Bin 0 -> 126072 bytes .../src/main/assets/commercial-24.png | Bin 0 -> 308 bytes .../src/main/assets/dog-park-24.png | Bin 0 -> 831 bytes .../src/main/assets/earthquakes.geojson | 6113 + .../src/main/assets/fill_filter_style.json | 10976 ++ .../0-255.pbf | Bin 0 -> 75287 bytes .../0-255.pbf | Bin 0 -> 73518 bytes .../0-255.pbf | Bin 0 -> 72149 bytes .../0-255.pbf | Bin 0 -> 75290 bytes .../8192-8447.pbf | Bin 0 -> 30553 bytes .../src/main/assets/heavy_style.json | 81857 +++++++++ .../src/main/assets/line_filter_style.json | 8746 + .../src/main/assets/london-underground-24.png | Bin 0 -> 729 bytes .../src/main/assets/numeric_filter_style.json | 8734 + .../src/main/assets/offline_test.db | Bin 0 -> 73728 bytes .../src/main/assets/points-sf.geojson | 115 + .../src/main/assets/points.geojson | 136346 +++++++++++++++ .../src/main/assets/small_line.geojson | 1 + .../src/main/assets/small_poly.geojson | 1 + .../src/main/assets/streets.json | 10398 ++ .../src/main/assets/town-hall-24.png | Bin 0 -> 552 bytes .../mapboxsdk/testapp/MapboxApplication.java | 95 + .../activity/FeatureOverviewActivity.java | 168 + .../AnimatedSymbolLayerActivity.java | 443 + .../annotation/BulkMarkerActivity.java | 200 + .../DynamicMarkerChangeActivity.java | 120 + .../activity/annotation/PolygonActivity.java | 219 + .../activity/annotation/PolylineActivity.java | 224 + .../annotation/PressForMarkerActivity.java | 152 + .../camera/CameraAnimationTypeActivity.java | 173 + .../camera/CameraAnimatorActivity.java | 281 + .../camera/CameraPositionActivity.java | 279 + .../camera/GestureDetectorActivity.java | 456 + .../activity/camera/LatLngBoundsActivity.kt | 165 + .../activity/camera/ManualZoomActivity.java | 120 + .../activity/camera/MaxMinZoomActivity.java | 99 + .../activity/camera/ScrollByActivity.java | 158 + .../customlayer/CustomLayerActivity.java | 143 + .../DeviceIndependentTestActivity.java | 79 + .../espresso/EspressoTestActivity.java | 66 + .../activity/espresso/PixelTestActivity.kt | 66 + ...QueryRenderedFeaturesBoxCountActivity.java | 154 + ...yRenderedFeaturesBoxHighlightActivity.java | 130 + ...enderedFeaturesBoxSymbolCountActivity.java | 145 + ...eryRenderedFeaturesPropertiesActivity.java | 244 + .../feature/QuerySourceFeaturesActivity.java | 134 + .../fragment/FragmentBackStackActivity.kt | 67 + .../fragment/MapFragmentActivity.java | 95 + .../activity/fragment/MultiMapActivity.java | 31 + .../fragment/NestedViewPagerActivity.kt | 166 + .../fragment/SupportMapFragmentActivity.java | 96 + .../activity/fragment/ViewPagerActivity.kt | 102 + .../imagegenerator/PrintActivity.java | 93 + .../imagegenerator/SnapshotActivity.kt | 89 + .../DynamicInfoWindowAdapterActivity.java | 158 + .../infowindow/InfoWindowActivity.java | 199 + .../infowindow/InfoWindowAdapterActivity.java | 128 + .../LocationComponentActivationActivity.java | 140 + .../location/LocationFragmentActivity.kt | 176 + .../location/LocationMapChangeActivity.java | 142 + .../location/LocationModesActivity.java | 432 + .../ManualLocationUpdatesActivity.java | 170 + .../testapp/activity/location/Utils.java | 59 + .../maplayout/BottomSheetActivity.java | 282 + .../activity/maplayout/DebugModeActivity.java | 306 + .../activity/maplayout/DoubleMapActivity.java | 160 + .../GLSurfaceRecyclerViewActivity.kt | 154 + .../LatLngBoundsForCameraActivity.java | 154 + .../maplayout/LocalGlyphActivity.java | 80 + .../activity/maplayout/MapChangeActivity.java | 89 + .../maplayout/MapInDialogActivity.java | 110 + .../maplayout/MapPaddingActivity.java | 131 + .../activity/maplayout/OverlayMapActivity.kt | 86 + .../activity/maplayout/SimpleMapActivity.java | 89 + .../maplayout/TextureRecyclerViewActivity.kt | 15 + .../maplayout/VisibilityChangeActivity.java | 144 + .../ChangeResourcesCachePathActivity.kt | 180 + .../offline/DeleteRegionActivity.java | 175 + .../offline/DownloadRegionActivity.kt | 319 + .../offline/MergeOfflineRegionsActivity.kt | 147 + .../activity/offline/OfflineActivity.java | 320 + .../offline/UpdateMetadataActivity.java | 187 + .../activity/render/RenderTestActivity.java | 366 + .../activity/render/RenderTestDefinition.java | 91 + .../render/RenderTestSnapshotter.java | 18 + .../render/RenderTestStyleDefinition.java | 131 + .../snapshot/MapSnapshotterActivity.java | 127 + .../MapSnapshotterLocalStyleActivity.java | 71 + .../MapSnapshotterMarkerActivity.java | 105 + .../snapshot/MapSnapshotterReuseActivity.java | 103 + .../storage/CacheManagementActivity.kt | 77 + .../storage/UrlTransformActivity.java | 99 + .../style/AnimatedImageSourceActivity.java | 143 + .../style/BuildingFillExtrusionActivity.java | 151 + .../activity/style/CircleLayerActivity.java | 284 + .../activity/style/CustomSpriteActivity.java | 144 + .../style/DataDrivenStyleActivity.java | 472 + .../activity/style/DraggableMarkerActivity.kt | 331 + .../activity/style/FillExtrusionActivity.java | 122 + .../style/FillExtrusionStyleTestActivity.java | 78 + .../style/GeoJsonClusteringActivity.java | 281 + .../activity/style/GradientLineActivity.java | 117 + .../activity/style/GridSourceActivity.java | 154 + .../activity/style/HeatmapLayerActivity.java | 228 + .../style/HillshadeLayerActivity.java | 87 + .../testapp/activity/style/NoStyleActivity.kt | 84 + .../style/RealTimeGeoJsonActivity.java | 129 + .../activity/style/RuntimeStyleActivity.java | 607 + .../style/RuntimeStyleTimingTestActivity.java | 99 + .../activity/style/StyleFileActivity.java | 177 + .../style/SymbolGeneratorActivity.java | 368 + .../activity/style/SymbolLayerActivity.java | 375 + .../ZoomFunctionSymbolLayerActivity.java | 199 + .../PerformanceMeasurementActivity.java | 189 + .../TextureViewAnimationActivity.java | 148 + .../TextureViewDebugModeActivity.java | 26 + .../TextureViewResizeActivity.java | 103 + ...tureViewTransparentBackgroundActivity.java | 108 + .../turf/PhysicalUnitCircleActivity.kt | 125 + .../testapp/adapter/FeatureAdapter.java | 61 + .../adapter/FeatureSectionAdapter.java | 174 + .../testapp/model/activity/Feature.java | 69 + .../model/annotations/CityStateMarker.java | 18 + .../annotations/CityStateMarkerOptions.java | 68 + .../model/annotations/CountryMarker.java | 24 + .../annotations/CountryMarkerOptions.java | 75 + .../model/customlayer/ExampleCustomLayer.java | 19 + .../other/OfflineDownloadRegionDialog.java | 47 + .../model/other/OfflineListRegionsDialog.java | 36 + .../mapboxsdk/testapp/utils/FileUtils.kt | 64 + .../mapboxsdk/testapp/utils/FontCache.java | 26 + .../mapboxsdk/testapp/utils/GeoParseUtil.java | 51 + .../mapboxsdk/testapp/utils/IconUtils.java | 31 + .../testapp/utils/IdleZoomListener.java | 25 + .../testapp/utils/ItemClickSupport.java | 95 + .../mapboxsdk/testapp/utils/NavUtils.java | 14 + .../mapboxsdk/testapp/utils/OfflineUtils.java | 37 + .../testapp/utils/ResourceUtils.java | 39 + .../utils/TileLoadingMeasurementUtils.java | 217 + .../mapboxsdk/testapp/utils/TimberLogger.java | 57 + .../mapboxsdk/testapp/utils/TimingLogger.java | 160 + .../mapboxsdk/testapp/utils/TokenUtils.java | 37 + .../testapp/utils/ViewToBitmapUtil.java | 22 + .../view/LockableBottomSheetBehavior.java | 74 + .../mapboxsdk/testapp/view/MapViewPager.java | 20 + .../src/main/res/animator/scale_down.xml | 11 + .../src/main/res/animator/scale_up.xml | 11 + .../src/main/res/drawable-hdpi/ic_android.png | Bin 0 -> 1711 bytes .../main/res/drawable-hdpi/ic_android_2.png | Bin 0 -> 1562 bytes .../res/drawable-hdpi/ic_launcher_round.png | Bin 0 -> 24697 bytes .../src/main/res/drawable-hdpi/icon.png | Bin 0 -> 19772 bytes .../main/res/drawable-hdpi/icon_burned.png | Bin 0 -> 1587 bytes .../src/main/res/drawable-mdpi/ic_android.png | Bin 0 -> 1217 bytes .../main/res/drawable-mdpi/ic_android_2.png | Bin 0 -> 1117 bytes .../res/drawable-mdpi/ic_launcher_round.png | Bin 0 -> 14115 bytes .../src/main/res/drawable-mdpi/icon.png | Bin 0 -> 11003 bytes .../main/res/drawable-mdpi/icon_burned.png | Bin 0 -> 913 bytes .../res/drawable-mdpi/southeast_radar_0.png | Bin 0 -> 172489 bytes .../res/drawable-mdpi/southeast_radar_1.png | Bin 0 -> 177163 bytes .../res/drawable-mdpi/southeast_radar_2.png | Bin 0 -> 179236 bytes .../res/drawable-mdpi/southeast_radar_3.png | Bin 0 -> 177074 bytes .../main/res/drawable-xhdpi/ic_android.png | Bin 0 -> 2202 bytes .../main/res/drawable-xhdpi/ic_android_2.png | Bin 0 -> 2142 bytes .../res/drawable-xhdpi/ic_launcher_round.png | Bin 0 -> 37126 bytes .../src/main/res/drawable-xhdpi/icon.png | Bin 0 -> 30669 bytes .../main/res/drawable-xhdpi/icon_burned.png | Bin 0 -> 1587 bytes .../main/res/drawable-xxhdpi/ic_android.png | Bin 0 -> 3639 bytes .../main/res/drawable-xxhdpi/ic_android_2.png | Bin 0 -> 3495 bytes .../res/drawable-xxhdpi/ic_launcher_round.png | Bin 0 -> 66853 bytes .../src/main/res/drawable-xxhdpi/ic_us.png | Bin 0 -> 418 bytes .../src/main/res/drawable-xxhdpi/icon.png | Bin 0 -> 58564 bytes .../main/res/drawable-xxhdpi/icon_burned.png | Bin 0 -> 2382 bytes .../main/res/drawable-xxxhdpi/ic_android.png | Bin 0 -> 4936 bytes .../res/drawable-xxxhdpi/ic_android_2.png | Bin 0 -> 4883 bytes .../main/res/drawable-xxxhdpi/ic_car_top.png | Bin 0 -> 19879 bytes .../drawable-xxxhdpi/ic_launcher_round.png | Bin 0 -> 103015 bytes .../main/res/drawable-xxxhdpi/ic_taxi_top.png | Bin 0 -> 11275 bytes .../src/main/res/drawable-xxxhdpi/icon.png | Bin 0 -> 93272 bytes .../main/res/drawable-xxxhdpi/icon_burned.png | Bin 0 -> 3018 bytes .../src/main/res/drawable-xxxhdpi/water.jpg | Bin 0 -> 165806 bytes .../main/res/drawable/custom_user_arrow.xml | 11 + .../main/res/drawable/custom_user_icon.xml | 10 + .../res/drawable/custom_user_puck_icon.xml | 23 + .../src/main/res/drawable/ic_ab_back.xml | 12 + .../src/main/res/drawable/ic_add.xml | 9 + .../res/drawable/ic_add_a_photo_black.xml | 9 + .../src/main/res/drawable/ic_add_white.xml | 9 + .../drawable/ic_airplanemode_active_black.xml | 12 + .../res/drawable/ic_animate_coordinates.xml | 9 + .../main/res/drawable/ic_arrow_downward.xml | 9 + .../src/main/res/drawable/ic_arrow_upward.xml | 9 + .../src/main/res/drawable/ic_check_box.xml | 9 + .../src/main/res/drawable/ic_delete.xml | 9 + .../res/drawable/ic_directions_bus_black.xml | 9 + .../res/drawable/ic_directions_car_black.xml | 9 + .../res/drawable/ic_directions_run_black.xml | 9 + .../src/main/res/drawable/ic_droppin.xml | 4 + .../res/drawable/ic_hearing_black_24dp.xml | 9 + .../src/main/res/drawable/ic_input.xml | 9 + .../src/main/res/drawable/ic_layers.xml | 9 + .../src/main/res/drawable/ic_layers_clear.xml | 9 + .../main/res/drawable/ic_location_city.xml | 9 + .../res/drawable/ic_location_disabled.xml | 9 + .../src/main/res/drawable/ic_my_location.xml | 9 + .../src/main/res/drawable/ic_paint.xml | 9 + .../main/res/drawable/ic_pause_black_24dp.xml | 9 + .../res/drawable/ic_play_arrow_black_24dp.xml | 9 + .../src/main/res/drawable/ic_print.xml | 9 + .../src/main/res/drawable/ic_refresh.xml | 9 + .../src/main/res/drawable/ic_stars.xml | 9 + .../src/main/res/drawable/line_divider.xml | 12 + .../mapbox_user_icon_shadow_0px_test.xml | 19 + .../main/res/layout/activity_add_sprite.xml | 28 + .../layout/activity_animated_image_source.xml | 16 + .../res/layout/activity_animated_marker.xml | 17 + .../layout/activity_backstack_fragment.xml | 17 + .../main/res/layout/activity_bottom_sheet.xml | 76 + .../res/layout/activity_building_layer.xml | 35 + .../res/layout/activity_cache_management.xml | 32 + .../activity_camera_animation_types.xml | 51 + .../res/layout/activity_camera_animator.xml | 26 + .../res/layout/activity_camera_position.xml | 33 + .../main/res/layout/activity_camera_test.xml | 16 + .../activity_change_resources_cache_path.xml | 21 + .../main/res/layout/activity_circle_layer.xml | 42 + .../main/res/layout/activity_custom_layer.xml | 25 + .../res/layout/activity_data_driven_style.xml | 24 + .../main/res/layout/activity_debug_mode.xml | 90 + .../src/main/res/layout/activity_default.xml | 12 + .../res/layout/activity_draggable_marker.xml | 19 + .../res/layout/activity_dynamic_marker.xml | 29 + .../res/layout/activity_espresso_test.xml | 15 + .../res/layout/activity_extrusion_test.xml | 17 + .../res/layout/activity_feature_overview.xml | 14 + .../layout/activity_fill_extrusion_layer.xml | 18 + .../layout/activity_geojson_clustering.xml | 23 + .../res/layout/activity_gesture_detector.xml | 27 + .../res/layout/activity_gradient_line.xml | 10 + .../main/res/layout/activity_grid_source.xml | 16 + .../main/res/layout/activity_heatmaplayer.xml | 13 + .../res/layout/activity_hillshade_layer.xml | 16 + .../main/res/layout/activity_infowindow.xml | 17 + .../layout/activity_infowindow_adapter.xml | 17 + .../main/res/layout/activity_latlngbounds.xml | 47 + .../main/res/layout/activity_local_glyph.xml | 17 + ...vity_location_layer_activation_builder.xml | 17 + .../activity_location_layer_fragment.xml | 24 + .../activity_location_layer_map_change.xml | 30 + .../layout/activity_location_layer_mode.xml | 78 + .../activity_location_manual_update.xml | 43 + .../main/res/layout/activity_manual_zoom.xml | 18 + .../main/res/layout/activity_map_fragment.xml | 8 + .../res/layout/activity_map_in_dialog.xml | 11 + .../main/res/layout/activity_map_padding.xml | 33 + .../main/res/layout/activity_map_simple.xml | 14 + .../res/layout/activity_map_snapshotter.xml | 14 + .../activity_map_snapshotter_marker.xml | 14 + .../layout/activity_map_snapshotter_reuse.xml | 24 + .../res/layout/activity_map_visibility.xml | 17 + .../main/res/layout/activity_marker_bulk.xml | 17 + .../main/res/layout/activity_maxmin_zoom.xml | 16 + .../layout/activity_merge_offline_regions.xml | 20 + .../res/layout/activity_metadata_update.xml | 22 + .../main/res/layout/activity_multi_map.xml | 69 + .../src/main/res/layout/activity_offline.xml | 47 + .../layout/activity_offline_region_delete.xml | 21 + .../src/main/res/layout/activity_overlay.xml | 13 + .../res/layout/activity_physical_circle.xml | 32 + .../main/res/layout/activity_pixel_test.xml | 16 + .../src/main/res/layout/activity_polyline.xml | 29 + .../res/layout/activity_press_for_marker.xml | 20 + .../src/main/res/layout/activity_print.xml | 23 + .../layout/activity_query_features_box.xml | 25 + .../layout/activity_query_features_point.xml | 16 + .../layout/activity_query_source_features.xml | 26 + .../main/res/layout/activity_recyclerview.xml | 12 + .../res/layout/activity_region_download.xml | 216 + .../res/layout/activity_restricted_bounds.xml | 17 + .../res/layout/activity_runtime_style.xml | 12 + .../main/res/layout/activity_scroll_by.xml | 106 + .../src/main/res/layout/activity_snapshot.xml | 29 + .../main/res/layout/activity_style_file.xml | 38 + .../res/layout/activity_symbol_generator.xml | 13 + .../main/res/layout/activity_symbollayer.xml | 8 + .../layout/activity_textureview_animate.xml | 29 + .../activity_textureview_debug_mode.xml | 98 + .../layout/activity_textureview_resize.xml | 29 + .../activity_textureview_transparent.xml | 16 + .../main/res/layout/activity_viewpager.xml | 21 + .../res/layout/activity_zoom_symbol_layer.xml | 16 + .../res/layout/dialog_camera_position.xml | 212 + .../res/layout/drawer_navigation_drawer.xml | 7 + .../main/res/layout/fragment_dialog_map.xml | 16 + .../main/res/layout/fragment_double_map.xml | 22 + .../main/res/layout/item_gesture_alert.xml | 14 + .../src/main/res/layout/item_main_feature.xml | 38 + .../src/main/res/layout/item_map_gl.xml | 10 + .../src/main/res/layout/item_map_texture.xml | 11 + .../src/main/res/layout/item_viewpager.xml | 17 + .../main/res/layout/section_main_layout.xml | 29 + .../main/res/layout/view_custom_marker.xml | 22 + .../src/main/res/layout/view_text_marker.xml | 17 + .../src/main/res/menu/menu_animator.xml | 20 + .../src/main/res/menu/menu_bounds.xml | 16 + .../src/main/res/menu/menu_building.xml | 12 + .../src/main/res/menu/menu_bulk_marker.xml | 9 + .../src/main/res/menu/menu_custom_layer.xml | 15 + .../main/res/menu/menu_data_driven_style.xml | 50 + .../src/main/res/menu/menu_debug.xml | 17 + .../main/res/menu/menu_generator_symbol.xml | 12 + .../src/main/res/menu/menu_gestures.xml | 27 + .../src/main/res/menu/menu_infowindow.xml | 19 + .../src/main/res/menu/menu_location_mode.xml | 39 + .../src/main/res/menu/menu_padding.xml | 8 + .../src/main/res/menu/menu_polygon.xml | 24 + .../src/main/res/menu/menu_polyline.xml | 25 + .../main/res/menu/menu_press_for_marker.xml | 8 + .../src/main/res/menu/menu_runtime_style.xml | 73 + .../src/main/res/menu/menu_symbol_layer.xml | 16 + .../src/main/res/menu/menu_symbols.xml | 12 + .../src/main/res/menu/menu_zoom.xml | 25 + .../src/main/res/raw/amsterdam.geojson | 2283 + .../src/main/res/raw/local_style.json | 7660 + .../src/main/res/raw/no_bg_style.json | 43 + .../src/main/res/raw/sat_style.json | 38 + .../res/raw/test_feature_collection.geojson | 26 + .../res/raw/test_feature_properties.geojson | 21 + .../raw/test_line_gradient_feature.geojson | 38 + .../res/raw/test_line_string_feature.geojson | 17 + .../test_multi_line_string_feature.geojson | 29 + .../res/raw/test_multi_point_feature.geojson | 17 + .../raw/test_multi_polygon_feature.geojson | 49 + .../main/res/raw/test_point_feature.geojson | 11 + .../main/res/raw/test_points_utrecht.geojson | 49 + .../main/res/raw/test_polygon_feature.geojson | 27 + .../test_polygon_with_hole_feature.geojson | 45 + .../src/main/res/raw/tiny_countries.geojson | 2741 + .../src/main/res/values-v21/attrs.xml | 6 + .../src/main/res/values-v21/styles.xml | 19 + .../src/main/res/values/actions.xml | 94 + .../src/main/res/values/arrays.xml | 15 + .../src/main/res/values/categories.xml | 21 + .../src/main/res/values/colors.xml | 13 + .../src/main/res/values/descriptions.xml | 89 + .../src/main/res/values/dimens.xml | 10 + .../src/main/res/values/ids.xml | 11 + .../src/main/res/values/strings.xml | 6 + .../src/main/res/values/styles.xml | 57 + .../src/main/res/values/titles.xml | 89 + README.md | 118 + TESTS.md | 117 + build.gradle | 34 + checkstyle.xml | 226 + gradle/android-nitpick.gradle | 66 + gradle/artifact-settings.gradle | 21 + gradle/dependencies.gradle | 90 + gradle/gradle-bintray.gradle | 117 + gradle/gradle-checkstyle.gradle | 26 + gradle/gradle-config.gradle | 23 + gradle/gradle-dependencies-graph.gradle | 29 + gradle/gradle-javadoc.gradle | 18 + gradle/gradle-lint.gradle | 30 + gradle/gradle-make.gradle | 18 + gradle/gradle-update-vendor-modules.gradle | 8 + gradle/jacoco-report.gradle | 33 + gradle/ktlint.gradle | 27 + gradle/native-build.gradle | 63 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54227 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 + gradlew.bat | 84 + resources/compass.svg | 34 + resources/direction-dot.svg | 28 + resources/dot-arrow.svg | 11 + resources/location-dot.svg | 15 + resources/location-static-dot.svg | 15 + scripts/exclude-activity-gen.json | 56 + scripts/generate-license.py | 39 + scripts/generate-style-code.js | 394 + scripts/generate-test-code.js | 73 + scripts/metrics.sh | 19 + scripts/parse-jacoco-report.py | 31 + scripts/release.py | 236 + scripts/run-render-test.py | 43 + scripts/validate-license.py | 17 + settings.gradle | 1 + src/android_renderer_backend.cpp | 67 + src/android_renderer_backend.hpp | 43 + src/android_renderer_frontend.cpp | 143 + src/android_renderer_frontend.hpp | 68 + src/annotation/marker.cpp | 23 + src/annotation/marker.hpp | 28 + src/annotation/multi_point.hpp | 34 + src/annotation/polygon.cpp | 40 + src/annotation/polygon.hpp | 21 + src/annotation/polyline.cpp | 31 + src/annotation/polyline.hpp | 21 + src/asset_manager.hpp | 14 + src/asset_manager_file_source.cpp | 57 + src/asset_manager_file_source.hpp | 29 + src/async_task.cpp | 62 + src/attach_env.cpp | 22 + src/attach_env.hpp | 31 + src/bitmap.cpp | 124 + src/bitmap.hpp | 43 + src/bitmap_factory.cpp | 25 + src/bitmap_factory.hpp | 20 + src/connectivity_listener.cpp | 41 + src/connectivity_listener.hpp | 29 + src/conversion/collection.cpp | 32 + src/conversion/collection.hpp | 18 + src/conversion/color.cpp | 17 + src/conversion/color.hpp | 18 + src/conversion/constant.cpp | 57 + src/conversion/constant.hpp | 104 + src/conversion/conversion.hpp | 50 + src/example_custom_layer.cpp | 238 + src/file_source.cpp | 202 + src/file_source.hpp | 81 + src/geojson/feature.cpp | 79 + src/geojson/feature.hpp | 23 + src/geojson/feature_collection.cpp | 37 + src/geojson/feature_collection.hpp | 26 + src/geojson/geometry.cpp | 97 + src/geojson/geometry.hpp | 28 + src/geojson/geometry_collection.cpp | 47 + src/geojson/geometry_collection.hpp | 26 + src/geojson/line_string.cpp | 55 + src/geojson/line_string.hpp | 36 + src/geojson/multi_line_string.cpp | 56 + src/geojson/multi_line_string.hpp | 34 + src/geojson/multi_point.cpp | 41 + src/geojson/multi_point.hpp | 33 + src/geojson/multi_polygon.cpp | 53 + src/geojson/multi_polygon.hpp | 32 + src/geojson/point.cpp | 34 + src/geojson/point.hpp | 27 + src/geojson/polygon.cpp | 55 + src/geojson/polygon.hpp | 35 + src/geojson/util.hpp | 52 + src/geometry/lat_lng.cpp | 29 + src/geometry/lat_lng.hpp | 29 + src/geometry/lat_lng_bounds.cpp | 30 + src/geometry/lat_lng_bounds.hpp | 26 + src/geometry/lat_lng_quad.cpp | 37 + src/geometry/lat_lng_quad.hpp | 27 + src/geometry/projected_meters.cpp | 17 + src/geometry/projected_meters.hpp | 21 + src/gl_functions.cpp | 156 + src/graphics/pointf.cpp | 25 + src/graphics/pointf.hpp | 24 + src/graphics/rectf.cpp | 35 + src/graphics/rectf.hpp | 28 + src/gson/json_array.cpp | 50 + src/gson/json_array.hpp | 24 + src/gson/json_element.cpp | 88 + src/gson/json_element.hpp | 24 + src/gson/json_object.cpp | 65 + src/gson/json_object.hpp | 24 + src/gson/json_primitive.cpp | 13 + src/gson/json_primitive.hpp | 21 + src/http_file_source.cpp | 194 + src/image.cpp | 21 + src/java/util.cpp | 19 + src/java/util.hpp | 81 + src/java_types.cpp | 49 + src/java_types.hpp | 43 + src/jni.cpp | 54 + src/jni.hpp | 24 + src/jni_native.cpp | 164 + src/jni_native.hpp | 13 + src/logger.cpp | 34 + src/logger.hpp | 21 + src/logging_android.cpp | 13 + src/main.cpp | 9 + src/map/camera_position.cpp | 68 + src/map/camera_position.hpp | 23 + src/map/image.cpp | 41 + src/map/image.hpp | 21 + src/map_renderer.cpp | 236 + src/map_renderer.hpp | 128 + src/map_renderer_runnable.cpp | 50 + src/map_renderer_runnable.hpp | 47 + src/native_map_view.cpp | 1153 + src/native_map_view.hpp | 261 + src/offline/offline_manager.cpp | 330 + src/offline/offline_manager.hpp | 109 + src/offline/offline_region.cpp | 300 + src/offline/offline_region.hpp | 92 + src/offline/offline_region_definition.cpp | 107 + src/offline/offline_region_definition.hpp | 43 + src/offline/offline_region_error.cpp | 44 + src/offline/offline_region_error.hpp | 19 + src/offline/offline_region_status.cpp | 38 + src/offline/offline_region_status.hpp | 19 + src/run_loop.cpp | 256 + src/run_loop_impl.hpp | 58 + src/snapshotter/map_snapshot.cpp | 65 + src/snapshotter/map_snapshot.hpp | 48 + src/snapshotter/map_snapshotter.cpp | 176 + src/snapshotter/map_snapshotter.hpp | 74 + src/string_util.cpp | 25 + src/style/android_conversion.hpp | 128 + src/style/conversion/filter.cpp | 26 + src/style/conversion/filter.hpp | 16 + src/style/conversion/position.cpp | 24 + src/style/conversion/position.hpp | 25 + src/style/conversion/property_expression.hpp | 23 + src/style/conversion/property_value.hpp | 62 + src/style/conversion/transition_options.cpp | 17 + src/style/conversion/transition_options.hpp | 21 + src/style/conversion/url_or_tileset.cpp | 30 + src/style/conversion/url_or_tileset.hpp | 16 + src/style/formatted.cpp | 54 + src/style/formatted.hpp | 23 + src/style/formatted_section.cpp | 11 + src/style/formatted_section.hpp | 18 + src/style/layers/background_layer.cpp | 145 + src/style/layers/background_layer.hpp | 58 + src/style/layers/circle_layer.cpp | 268 + src/style/layers/circle_layer.hpp | 84 + src/style/layers/custom_layer.cpp | 70 + src/style/layers/custom_layer.hpp | 44 + src/style/layers/fill_extrusion_layer.cpp | 220 + src/style/layers/fill_extrusion_layer.hpp | 74 + src/style/layers/fill_layer.cpp | 199 + src/style/layers/fill_layer.hpp | 70 + src/style/layers/heatmap_layer.cpp | 161 + src/style/layers/heatmap_layer.hpp | 62 + src/style/layers/hillshade_layer.cpp | 178 + src/style/layers/hillshade_layer.hpp | 66 + src/style/layers/layer.cpp | 198 + src/style/layers/layer.cpp.ejs | 137 + src/style/layers/layer.hpp | 126 + src/style/layers/layer.hpp.ejs | 62 + src/style/layers/layer_manager.cpp | 172 + src/style/layers/layer_manager.hpp | 57 + src/style/layers/line_layer.cpp | 307 + src/style/layers/line_layer.hpp | 94 + src/style/layers/raster_layer.cpp | 220 + src/style/layers/raster_layer.hpp | 74 + src/style/layers/symbol_layer.cpp | 592 + src/style/layers/symbol_layer.hpp | 180 + src/style/light.cpp | 143 + src/style/light.hpp | 53 + src/style/position.cpp | 35 + src/style/position.hpp | 24 + src/style/sources/custom_geometry_source.cpp | 210 + src/style/sources/custom_geometry_source.hpp | 50 + src/style/sources/geojson_source.cpp | 265 + src/style/sources/geojson_source.hpp | 73 + src/style/sources/image_source.cpp | 79 + src/style/sources/image_source.hpp | 39 + src/style/sources/raster_dem_source.cpp | 62 + src/style/sources/raster_dem_source.hpp | 29 + src/style/sources/raster_source.cpp | 61 + src/style/sources/raster_source.hpp | 29 + src/style/sources/source.cpp | 168 + src/style/sources/source.hpp | 61 + src/style/sources/unknown_source.cpp | 43 + src/style/sources/unknown_source.hpp | 27 + src/style/sources/vector_source.cpp | 84 + src/style/sources/vector_source.hpp | 33 + src/style/transition_options.cpp | 35 + src/style/transition_options.hpp | 26 + src/style/value.cpp | 83 + src/style/value.hpp | 43 + src/test/benchmark_runner.cpp | 10 + src/test/runtime.cpp | 74 + src/test/runtime.hpp | 9 + src/test/test_runner.cpp | 10 + src/text/collator.cpp | 199 + src/text/collator_jni.hpp | 64 + src/text/format_number.cpp | 81 + src/text/format_number_jni.hpp | 29 + src/text/local_glyph_rasterizer.cpp | 130 + src/text/local_glyph_rasterizer_jni.hpp | 33 + src/thread.cpp | 56 + src/timer.cpp | 75 + src/unaccent.cpp | 18 + vendor/mapbox-events-android | 1 + vendor/mapbox-gestures-android | 1 + vendor/mapbox-gl-native | 1 + vendor/mapbox-java | 1 + 1061 files changed, 403354 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CHANGELOG.md create mode 100644 DISTRIBUTE.md create mode 100644 LICENSE.md create mode 100644 MapboxGLAndroidSDK/.gitignore create mode 100644 MapboxGLAndroidSDK/build.gradle create mode 100644 MapboxGLAndroidSDK/lint.xml create mode 100644 MapboxGLAndroidSDK/proguard-rules.pro create mode 100644 MapboxGLAndroidSDK/src/main/AndroidManifest.xml create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/AccountsManager.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/LibraryLoader.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/LibraryLoaderProvider.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapStrictMode.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapStrictModeException.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/Mapbox.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/ModuleProvider.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/ModuleProviderImpl.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Annotation.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/ArrowDirection.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BaseMarkerOptions.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BasePointCollection.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Bubble.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BubbleLayout.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BubblePopupHelper.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Icon.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/IconFactory.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindow.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Marker.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MarkerOptions.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polygon.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolygonOptions.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polyline.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolylineOptions.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/Attribution.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/AttributionLayout.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/AttributionMeasure.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/AttributionParser.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraPosition.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdate.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactory.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeometryConstants.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/TelemetryConstants.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/CalledFromWorkerThreadException.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/ConversionException.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/IconBitmapChangedException.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidLatLngBoundsException.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidMarkerPositionException.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/MapboxConfigurationException.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/TooManyIconsException.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLng.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngQuad.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngSpan.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ProjectedMeters.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/VisibleRegion.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpIdentifier.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpLogger.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpRequest.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpRequestUrl.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpResponder.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/LocalRequestTask.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/NativeHttpRequest.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/AnimatorListenerHolder.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassEngine.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassListener.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LatLngEvaluator.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerBitmapProvider.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerFeatureProvider.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationCameraController.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptions.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentCompassEngine.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentNotInitializedException.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentPositionManager.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxCameraAnimatorAdapter.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxFloatAnimator.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxLatLngAnimator.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraMoveInvalidateListener.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraTrackingChangedListener.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationCameraTransitionListener.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationClickListener.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationLongClickListener.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationStaleListener.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnRenderModeChangedListener.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/StaleStateManager.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/Utils.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/CameraMode.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/RenderMode.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/Logger.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/LoggerDefinition.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationContainer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationManager.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Annotations.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AttributionDialogManager.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/CameraChangeDispatcher.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/FocalPointChangeListener.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/IconManager.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Image.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/InfoWindowManager.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapChangeReceiver.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapFragment.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapKeyListener.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMapOptions.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MarkerContainer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Markers.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMap.java create mode 100755 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/OnMapReadyCallback.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/PolygonContainer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Polygons.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/PolylineContainer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Polylines.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Projection.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotationContainer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotations.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Style.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/SupportMapFragment.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/TelemetryDefinition.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/UiSettings.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/MapRenderer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/MapRendererRunnable.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/MapRendererScheduler.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLConfigChooser.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/EGLConfigException.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/egl/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/glsurfaceview/GLSurfaceViewMapRenderer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/glsurfaceview/MapboxGLSurfaceView.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/glsurfaceview/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/textureview/TextureViewMapRenderer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/textureview/TextureViewRenderThread.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/textureview/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/widgets/CompassView.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/widgets/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/http/HttpRequestImpl.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/http/HttpRequestUtil.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/http/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/loader/LibraryLoaderProviderImpl.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/loader/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/telemetry/MapBaseEvent.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/telemetry/MapEventFactory.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/telemetry/MapLoadEvent.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/telemetry/OfflineDownloadEndEvent.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/telemetry/OfflineDownloadStartEvent.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/telemetry/PerformanceEvent.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/telemetry/PhoneState.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/telemetry/TelemetryImpl.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/module/telemetry/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/net/ConnectivityListener.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/net/ConnectivityReceiver.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/net/NativeConnectivityListener.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/net/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineGeometryRegionDefinition.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionDefinition.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionError.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionStatus.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineTilePyramidRegionDefinition.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnaphotUtil.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshot.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshotter.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/Resource.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/expressions/Expression.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/expressions/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/BackgroundLayer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/CannotAddLayerException.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/CircleLayer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/CustomLayer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/FillExtrusionLayer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/FillLayer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/HeatmapLayer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/HillshadeLayer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/Layer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/LayoutPropertyValue.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/LineLayer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/PaintPropertyValue.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/Property.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/PropertyFactory.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/PropertyValue.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/RasterLayer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/SymbolLayer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/TransitionOptions.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/layer.java.ejs create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/property.java.ejs create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/property_factory.java.ejs create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/light/Light.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/light/Position.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/light/light.java.ejs create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/light/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/CannotAddSourceException.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/CustomGeometrySource.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/CustomGeometrySourceOptions.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/GeoJsonOptions.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/GeoJsonSource.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/GeometryTileProvider.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/ImageSource.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/RasterDemSource.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/RasterSource.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/Source.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/TileSet.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/UnknownSource.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/VectorSource.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/sources/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/types/Formatted.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/types/FormattedSection.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/types/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/text/LocalGlyphRasterizer.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/text/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/AnimatorUtils.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/BitmapUtils.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ColorUtils.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/Compare.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/FileUtils.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/FontUtils.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/MapFragmentUtils.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/MathUtils.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/StringUtils.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ThreadUtils.java create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/package-info.java create mode 100644 MapboxGLAndroidSDK/src/main/java/overview.html create mode 100644 MapboxGLAndroidSDK/src/main/res-public/values/public.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-hdpi/mapbox_compass_icon.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-hdpi/mapbox_logo_helmet.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-hdpi/mapbox_logo_icon.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-hdpi/mapbox_marker_icon_default.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-hdpi/mapbox_markerview_icon_default.png create mode 100755 MapboxGLAndroidSDK/src/main/res/drawable-hdpi/mapbox_mylocation_icon_bearing.png create mode 100755 MapboxGLAndroidSDK/src/main/res/drawable-hdpi/mapbox_mylocation_icon_default.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-mdpi/mapbox_compass_icon.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-mdpi/mapbox_logo_helmet.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-mdpi/mapbox_logo_icon.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-mdpi/mapbox_marker_icon_default.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-mdpi/mapbox_markerview_icon_default.png create mode 100755 MapboxGLAndroidSDK/src/main/res/drawable-mdpi/mapbox_mylocation_icon_bearing.png create mode 100755 MapboxGLAndroidSDK/src/main/res/drawable-mdpi/mapbox_mylocation_icon_default.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xhdpi/mapbox_compass_icon.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xhdpi/mapbox_logo_helmet.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xhdpi/mapbox_logo_icon.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xhdpi/mapbox_marker_icon_default.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xhdpi/mapbox_markerview_icon_default.png create mode 100755 MapboxGLAndroidSDK/src/main/res/drawable-xhdpi/mapbox_mylocation_icon_bearing.png create mode 100755 MapboxGLAndroidSDK/src/main/res/drawable-xhdpi/mapbox_mylocation_icon_default.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xxhdpi/mapbox_compass_icon.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xxhdpi/mapbox_logo_helmet.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xxhdpi/mapbox_logo_icon.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xxhdpi/mapbox_marker_icon_default.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xxhdpi/mapbox_markerview_icon_default.png create mode 100755 MapboxGLAndroidSDK/src/main/res/drawable-xxhdpi/mapbox_mylocation_icon_bearing.png create mode 100755 MapboxGLAndroidSDK/src/main/res/drawable-xxhdpi/mapbox_mylocation_icon_default.png create mode 100755 MapboxGLAndroidSDK/src/main/res/drawable-xxxhdpi/mapbox_compass_icon.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xxxhdpi/mapbox_logo_helmet.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xxxhdpi/mapbox_logo_icon.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xxxhdpi/mapbox_marker_icon_default.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable-xxxhdpi/mapbox_markerview_icon_default.png create mode 100755 MapboxGLAndroidSDK/src/main/res/drawable-xxxhdpi/mapbox_mylocation_icon_bearing.png create mode 100755 MapboxGLAndroidSDK/src/main/res/drawable-xxxhdpi/mapbox_mylocation_icon_default.png create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable/mapbox_info_bg_selector.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable/mapbox_info_icon_default.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable/mapbox_info_icon_selected.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable/mapbox_mylocation_bg_shape.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable/mapbox_popup_window_transparent.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable/mapbox_rounded_corner.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable/mapbox_user_bearing_icon.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable/mapbox_user_icon.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable/mapbox_user_icon_shadow.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable/mapbox_user_icon_stale.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable/mapbox_user_puck_icon.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/drawable/mapbox_user_stroke_icon.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/layout/mapbox_attribution_list_item.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/layout/mapbox_infowindow_content.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/layout/mapbox_mapview_internal.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/layout/mapbox_view_image_marker.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-bg/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-ca/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-cs/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-es/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-fr/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-gl/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-he/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-hu/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-iw/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-ko/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-lt/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-nl/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-pl/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-pt-rPT/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-ru/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-sv/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-uk/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-vi/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-zh-rCN/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-zh-rHK/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values-zh-rTW/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values/attrs.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values/colors.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values/dimens.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values/strings.xml create mode 100644 MapboxGLAndroidSDK/src/main/res/values/styles.xml create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/AccountsManagerTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/MapboxInjector.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/MapboxTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/AnnotationTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/IconTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/InfoWindowTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/MarkerTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/PolygonTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/PolylineTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/attribution/AttributionParseTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/camera/CameraPositionTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/constants/AppConstant.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngSpanTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/ProjectedMetersTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/VisibleRegionTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/CompassEngineTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationCameraControllerTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptionsTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentOptionsTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentPositionManagerTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/MapboxAnimatorTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/UtilsTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/log/LoggerTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/AnnotationManagerTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapChangeReceiverTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapTouchListenersTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapboxMapOptionsAttrsTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapboxMapOptionsTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapboxMapTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/StyleBuilderTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/StyleTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/TransformTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/UiSettingsTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/module/http/HttpRequestUrlTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/module/http/HttpRequestUtilTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/module/telemetry/MapEventFactoryTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/module/telemetry/SchemaTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/style/expressions/ExpressionTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/telemetry/HttpTransportTest.java create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/utils/ColorUtilsTest.kt create mode 100644 MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/utils/MockParcel.java create mode 100644 MapboxGLAndroidSDK/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker create mode 100644 MapboxGLAndroidSDKLint/.gitignore create mode 100644 MapboxGLAndroidSDKLint/build.gradle create mode 100644 MapboxGLAndroidSDKLint/src/main/java/com/mapbox/mapboxsdk/lint/KeepDetector.kt create mode 100644 MapboxGLAndroidSDKLint/src/main/java/com/mapbox/mapboxsdk/lint/MapboxIssueRegistry.kt create mode 100644 MapboxGLAndroidSDKLint/src/test/java/com/mapbox/mapboxsdk/lint/KeepDetectorTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/.gitignore create mode 100644 MapboxGLAndroidSDKTestApp/README.md create mode 100644 MapboxGLAndroidSDKTestApp/build.gradle create mode 100644 MapboxGLAndroidSDKTestApp/lint-baseline-local.xml create mode 100644 MapboxGLAndroidSDKTestApp/lint/lint-baseline-ci.xml create mode 100644 MapboxGLAndroidSDKTestApp/proguard-rules.pro create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/AndroidManifest.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/assets/streets.json create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/AppCenter.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/InstrumentationApplication.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/InstrumentationRunner.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactoryTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/BaseIntegrationTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/FragmentBackStackTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/GLSurfaceViewReopenTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/GLSurfaceViewReuseTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/OrientationChangeTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/QueryRenderedFeaturesBoxCountTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/TextureViewReopenTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/TextureViewReuseTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/integration/ViewPagerScrollTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/utils/LocationComponentAction.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/utils/MapboxLocationTestingUtils.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/utils/StyleChangeIdlingResource.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/BaseLayerTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/GesturesUiTestUtils.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/IconManagerResolver.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapGestureDetectorTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxMapTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/NativeMapViewTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/TransformTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/VisibleRegionTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/offline/OfflineDownloadTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshotterTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/action/MapboxMapAction.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/action/OrientationAction.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/action/WaitAction.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/BaseTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/EspressoTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/activity.junit.ejs create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/annotations/IconTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/annotations/MarkerTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/annotations/PolygonTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/annotations/PolylineTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/camera/CameraAnimateTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/camera/CameraEaseTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/camera/CameraForTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/camera/CameraMoveTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/camera/CameraTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/fragment/MapDialogFragmentTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/geometry/GeoJsonConversionTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/geometry/LatLngBoundsTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/ImageMissingTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/RemoveUnusedImagesTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/StyleLoadTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/widgets/AttributionTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/widgets/CompassViewTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/widgets/LogoTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/CacheTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineUtilsTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/render/RenderTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceMapTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceStandaloneTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/storage/FileSourceTestUtils.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/string/UppperLowerCaseTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/BackgroundLayerTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/CircleLayerTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/CustomGeometrySourceTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/ExpressionTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/FillExtrusionLayerTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/FillLayerTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/GeoJsonSourceTests.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/HeatmapLayerTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/HillshadeLayerTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/ImageTest.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/LightTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/LineLayerTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/RasterLayerTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/RuntimeStyleTests.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/RuntimeStyleTimingTests.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/StyleLoaderTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/SymbolLayerTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/layer.junit.ejs create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/light.junit.ejs create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/utils/SnapshotterIdlingResource.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/utils/TestConstants.java create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/utils/TestingAsyncUtils.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/utils/FontUtilsTest.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/Roboto-Medium.ttf create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/Roboto-Regular.ttf create mode 100755 MapboxGLAndroidSDKTestApp/src/main/assets/commercial-24.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/dog-park-24.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/earthquakes.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/fill_filter_style.json create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/fonts/DIN Offc Pro Bold,Arial Unicode MS Bold/0-255.pbf create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/fonts/DIN Offc Pro Italic,Arial Unicode MS Regular/0-255.pbf create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/fonts/DIN Offc Pro Medium,Arial Unicode MS Regular/0-255.pbf create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/fonts/DIN Offc Pro Regular,Arial Unicode MS Regular/0-255.pbf create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/fonts/DIN Offc Pro Regular,Arial Unicode MS Regular/8192-8447.pbf create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/heavy_style.json create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/line_filter_style.json create mode 100755 MapboxGLAndroidSDKTestApp/src/main/assets/london-underground-24.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/numeric_filter_style.json create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/offline_test.db create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/points-sf.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/points.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/small_line.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/small_poly.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/assets/streets.json create mode 100755 MapboxGLAndroidSDKTestApp/src/main/assets/town-hall-24.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/MapboxApplication.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/FeatureOverviewActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/annotation/AnimatedSymbolLayerActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/annotation/BulkMarkerActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/annotation/DynamicMarkerChangeActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/annotation/PolygonActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/annotation/PolylineActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/annotation/PressForMarkerActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/CameraAnimationTypeActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/CameraAnimatorActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/CameraPositionActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/GestureDetectorActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/LatLngBoundsActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/ManualZoomActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/MaxMinZoomActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/ScrollByActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/customlayer/CustomLayerActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/espresso/DeviceIndependentTestActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/espresso/EspressoTestActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/espresso/PixelTestActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/feature/QueryRenderedFeaturesBoxCountActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/feature/QueryRenderedFeaturesBoxHighlightActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/feature/QueryRenderedFeaturesBoxSymbolCountActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/feature/QueryRenderedFeaturesPropertiesActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/feature/QuerySourceFeaturesActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/fragment/FragmentBackStackActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/fragment/MapFragmentActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/fragment/MultiMapActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/fragment/NestedViewPagerActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/fragment/SupportMapFragmentActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/fragment/ViewPagerActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/imagegenerator/PrintActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/imagegenerator/SnapshotActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/infowindow/DynamicInfoWindowAdapterActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/infowindow/InfoWindowActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/infowindow/InfoWindowAdapterActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationComponentActivationActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationFragmentActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationMapChangeActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationModesActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/ManualLocationUpdatesActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/Utils.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/BottomSheetActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/DebugModeActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/DoubleMapActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/GLSurfaceRecyclerViewActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/LatLngBoundsForCameraActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/LocalGlyphActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/MapChangeActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/MapInDialogActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/MapPaddingActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/OverlayMapActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/SimpleMapActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/TextureRecyclerViewActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/VisibilityChangeActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/ChangeResourcesCachePathActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/DeleteRegionActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/DownloadRegionActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/MergeOfflineRegionsActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/OfflineActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/UpdateMetadataActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/render/RenderTestActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/render/RenderTestDefinition.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/render/RenderTestSnapshotter.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/render/RenderTestStyleDefinition.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/snapshot/MapSnapshotterActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/snapshot/MapSnapshotterLocalStyleActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/snapshot/MapSnapshotterMarkerActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/snapshot/MapSnapshotterReuseActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/storage/CacheManagementActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/storage/UrlTransformActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/AnimatedImageSourceActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/BuildingFillExtrusionActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/CircleLayerActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/CustomSpriteActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/DataDrivenStyleActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/DraggableMarkerActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/FillExtrusionActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/FillExtrusionStyleTestActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/GeoJsonClusteringActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/GradientLineActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/GridSourceActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/HeatmapLayerActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/HillshadeLayerActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/NoStyleActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/RealTimeGeoJsonActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/RuntimeStyleActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/RuntimeStyleTimingTestActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/StyleFileActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolGeneratorActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolLayerActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/ZoomFunctionSymbolLayerActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/telemetry/PerformanceMeasurementActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/textureview/TextureViewAnimationActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/textureview/TextureViewDebugModeActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/textureview/TextureViewResizeActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/textureview/TextureViewTransparentBackgroundActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/turf/PhysicalUnitCircleActivity.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/adapter/FeatureAdapter.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/adapter/FeatureSectionAdapter.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/model/activity/Feature.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/model/annotations/CityStateMarker.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/model/annotations/CityStateMarkerOptions.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/model/annotations/CountryMarker.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/model/annotations/CountryMarkerOptions.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/model/customlayer/ExampleCustomLayer.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/model/other/OfflineDownloadRegionDialog.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/model/other/OfflineListRegionsDialog.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/FileUtils.kt create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/FontCache.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/GeoParseUtil.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/IconUtils.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/IdleZoomListener.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/ItemClickSupport.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/NavUtils.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/OfflineUtils.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/ResourceUtils.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/TileLoadingMeasurementUtils.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/TimberLogger.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/TimingLogger.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/TokenUtils.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/utils/ViewToBitmapUtil.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/view/LockableBottomSheetBehavior.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/view/MapViewPager.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/animator/scale_down.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/animator/scale_up.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-hdpi/ic_android.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-hdpi/ic_android_2.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-hdpi/ic_launcher_round.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-hdpi/icon.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-hdpi/icon_burned.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-mdpi/ic_android.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-mdpi/ic_android_2.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-mdpi/ic_launcher_round.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-mdpi/icon.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-mdpi/icon_burned.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-mdpi/southeast_radar_0.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-mdpi/southeast_radar_1.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-mdpi/southeast_radar_2.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-mdpi/southeast_radar_3.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xhdpi/ic_android.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xhdpi/ic_android_2.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xhdpi/ic_launcher_round.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xhdpi/icon.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xhdpi/icon_burned.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxhdpi/ic_android.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxhdpi/ic_android_2.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxhdpi/ic_launcher_round.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxhdpi/ic_us.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxhdpi/icon.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxhdpi/icon_burned.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxxhdpi/ic_android.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxxhdpi/ic_android_2.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxxhdpi/ic_car_top.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxxhdpi/ic_launcher_round.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxxhdpi/ic_taxi_top.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxxhdpi/icon.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxxhdpi/icon_burned.png create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable-xxxhdpi/water.jpg create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/custom_user_arrow.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/custom_user_icon.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/custom_user_puck_icon.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_ab_back.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_add.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_add_a_photo_black.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_add_white.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_airplanemode_active_black.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_animate_coordinates.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_arrow_downward.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_arrow_upward.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_check_box.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_delete.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_directions_bus_black.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_directions_car_black.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_directions_run_black.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_droppin.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_hearing_black_24dp.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_input.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_layers.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_layers_clear.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_location_city.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_location_disabled.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_my_location.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_paint.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_pause_black_24dp.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_play_arrow_black_24dp.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_print.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_refresh.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_stars.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/line_divider.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/drawable/mapbox_user_icon_shadow_0px_test.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_add_sprite.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_animated_image_source.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_animated_marker.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_backstack_fragment.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_bottom_sheet.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_building_layer.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_cache_management.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_camera_animation_types.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_camera_animator.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_camera_position.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_camera_test.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_change_resources_cache_path.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_circle_layer.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_custom_layer.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_data_driven_style.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_debug_mode.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_default.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_draggable_marker.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_dynamic_marker.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_espresso_test.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_extrusion_test.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_feature_overview.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_fill_extrusion_layer.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_geojson_clustering.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_gesture_detector.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_gradient_line.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_grid_source.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_heatmaplayer.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_hillshade_layer.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_infowindow.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_infowindow_adapter.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_latlngbounds.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_local_glyph.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_activation_builder.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_fragment.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_map_change.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_mode.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_manual_update.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_manual_zoom.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_fragment.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_in_dialog.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_padding.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_simple.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_snapshotter.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_snapshotter_marker.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_snapshotter_reuse.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_visibility.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_marker_bulk.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_maxmin_zoom.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_merge_offline_regions.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_metadata_update.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_multi_map.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_offline.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_offline_region_delete.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_overlay.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_physical_circle.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_pixel_test.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_polyline.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_press_for_marker.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_print.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_query_features_box.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_query_features_point.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_query_source_features.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_recyclerview.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_region_download.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_restricted_bounds.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_runtime_style.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_scroll_by.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_snapshot.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_style_file.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_symbol_generator.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_symbollayer.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_textureview_animate.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_textureview_debug_mode.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_textureview_resize.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_textureview_transparent.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_viewpager.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_zoom_symbol_layer.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/dialog_camera_position.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/drawer_navigation_drawer.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/fragment_dialog_map.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/fragment_double_map.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/item_gesture_alert.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/item_main_feature.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/item_map_gl.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/item_map_texture.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/item_viewpager.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/section_main_layout.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/view_custom_marker.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/view_text_marker.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_animator.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_bounds.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_building.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_bulk_marker.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_custom_layer.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_data_driven_style.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_debug.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_generator_symbol.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_gestures.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_infowindow.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_location_mode.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_padding.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_polygon.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_polyline.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_press_for_marker.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_runtime_style.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_symbol_layer.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_symbols.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_zoom.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/amsterdam.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/local_style.json create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/no_bg_style.json create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/sat_style.json create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/test_feature_collection.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/test_feature_properties.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/test_line_gradient_feature.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/test_line_string_feature.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/test_multi_line_string_feature.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/test_multi_point_feature.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/test_multi_polygon_feature.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/test_point_feature.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/test_points_utrecht.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/test_polygon_feature.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/test_polygon_with_hole_feature.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/raw/tiny_countries.geojson create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/values-v21/attrs.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/values-v21/styles.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/values/actions.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/values/arrays.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/values/categories.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/values/colors.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/values/dimens.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/values/ids.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/values/strings.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/values/styles.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml create mode 100644 README.md create mode 100644 TESTS.md create mode 100644 build.gradle create mode 100644 checkstyle.xml create mode 100644 gradle/android-nitpick.gradle create mode 100644 gradle/artifact-settings.gradle create mode 100644 gradle/dependencies.gradle create mode 100644 gradle/gradle-bintray.gradle create mode 100644 gradle/gradle-checkstyle.gradle create mode 100644 gradle/gradle-config.gradle create mode 100644 gradle/gradle-dependencies-graph.gradle create mode 100644 gradle/gradle-javadoc.gradle create mode 100644 gradle/gradle-lint.gradle create mode 100644 gradle/gradle-make.gradle create mode 100644 gradle/gradle-update-vendor-modules.gradle create mode 100644 gradle/jacoco-report.gradle create mode 100644 gradle/ktlint.gradle create mode 100644 gradle/native-build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100755 resources/compass.svg create mode 100755 resources/direction-dot.svg create mode 100644 resources/dot-arrow.svg create mode 100755 resources/location-dot.svg create mode 100755 resources/location-static-dot.svg create mode 100644 scripts/exclude-activity-gen.json create mode 100644 scripts/generate-license.py create mode 100755 scripts/generate-style-code.js create mode 100644 scripts/generate-test-code.js create mode 100755 scripts/metrics.sh create mode 100755 scripts/parse-jacoco-report.py create mode 100644 scripts/release.py create mode 100644 scripts/run-render-test.py create mode 100644 scripts/validate-license.py create mode 100644 settings.gradle create mode 100755 src/android_renderer_backend.cpp create mode 100755 src/android_renderer_backend.hpp create mode 100644 src/android_renderer_frontend.cpp create mode 100644 src/android_renderer_frontend.hpp create mode 100644 src/annotation/marker.cpp create mode 100644 src/annotation/marker.hpp create mode 100644 src/annotation/multi_point.hpp create mode 100644 src/annotation/polygon.cpp create mode 100644 src/annotation/polygon.hpp create mode 100644 src/annotation/polyline.cpp create mode 100644 src/annotation/polyline.hpp create mode 100644 src/asset_manager.hpp create mode 100644 src/asset_manager_file_source.cpp create mode 100644 src/asset_manager_file_source.hpp create mode 100644 src/async_task.cpp create mode 100644 src/attach_env.cpp create mode 100644 src/attach_env.hpp create mode 100644 src/bitmap.cpp create mode 100644 src/bitmap.hpp create mode 100644 src/bitmap_factory.cpp create mode 100644 src/bitmap_factory.hpp create mode 100644 src/connectivity_listener.cpp create mode 100644 src/connectivity_listener.hpp create mode 100644 src/conversion/collection.cpp create mode 100644 src/conversion/collection.hpp create mode 100644 src/conversion/color.cpp create mode 100644 src/conversion/color.hpp create mode 100644 src/conversion/constant.cpp create mode 100644 src/conversion/constant.hpp create mode 100644 src/conversion/conversion.hpp create mode 100644 src/example_custom_layer.cpp create mode 100644 src/file_source.cpp create mode 100644 src/file_source.hpp create mode 100644 src/geojson/feature.cpp create mode 100644 src/geojson/feature.hpp create mode 100644 src/geojson/feature_collection.cpp create mode 100644 src/geojson/feature_collection.hpp create mode 100644 src/geojson/geometry.cpp create mode 100644 src/geojson/geometry.hpp create mode 100644 src/geojson/geometry_collection.cpp create mode 100644 src/geojson/geometry_collection.hpp create mode 100644 src/geojson/line_string.cpp create mode 100644 src/geojson/line_string.hpp create mode 100644 src/geojson/multi_line_string.cpp create mode 100644 src/geojson/multi_line_string.hpp create mode 100644 src/geojson/multi_point.cpp create mode 100644 src/geojson/multi_point.hpp create mode 100644 src/geojson/multi_polygon.cpp create mode 100644 src/geojson/multi_polygon.hpp create mode 100644 src/geojson/point.cpp create mode 100644 src/geojson/point.hpp create mode 100644 src/geojson/polygon.cpp create mode 100644 src/geojson/polygon.hpp create mode 100644 src/geojson/util.hpp create mode 100644 src/geometry/lat_lng.cpp create mode 100644 src/geometry/lat_lng.hpp create mode 100644 src/geometry/lat_lng_bounds.cpp create mode 100644 src/geometry/lat_lng_bounds.hpp create mode 100644 src/geometry/lat_lng_quad.cpp create mode 100644 src/geometry/lat_lng_quad.hpp create mode 100644 src/geometry/projected_meters.cpp create mode 100644 src/geometry/projected_meters.hpp create mode 100644 src/gl_functions.cpp create mode 100644 src/graphics/pointf.cpp create mode 100644 src/graphics/pointf.hpp create mode 100644 src/graphics/rectf.cpp create mode 100644 src/graphics/rectf.hpp create mode 100644 src/gson/json_array.cpp create mode 100644 src/gson/json_array.hpp create mode 100644 src/gson/json_element.cpp create mode 100644 src/gson/json_element.hpp create mode 100644 src/gson/json_object.cpp create mode 100644 src/gson/json_object.hpp create mode 100644 src/gson/json_primitive.cpp create mode 100644 src/gson/json_primitive.hpp create mode 100644 src/http_file_source.cpp create mode 100644 src/image.cpp create mode 100644 src/java/util.cpp create mode 100644 src/java/util.hpp create mode 100644 src/java_types.cpp create mode 100644 src/java_types.hpp create mode 100755 src/jni.cpp create mode 100644 src/jni.hpp create mode 100755 src/jni_native.cpp create mode 100644 src/jni_native.hpp create mode 100644 src/logger.cpp create mode 100644 src/logger.hpp create mode 100644 src/logging_android.cpp create mode 100644 src/main.cpp create mode 100644 src/map/camera_position.cpp create mode 100644 src/map/camera_position.hpp create mode 100644 src/map/image.cpp create mode 100644 src/map/image.hpp create mode 100644 src/map_renderer.cpp create mode 100644 src/map_renderer.hpp create mode 100644 src/map_renderer_runnable.cpp create mode 100644 src/map_renderer_runnable.hpp create mode 100755 src/native_map_view.cpp create mode 100755 src/native_map_view.hpp create mode 100644 src/offline/offline_manager.cpp create mode 100644 src/offline/offline_manager.hpp create mode 100644 src/offline/offline_region.cpp create mode 100644 src/offline/offline_region.hpp create mode 100644 src/offline/offline_region_definition.cpp create mode 100644 src/offline/offline_region_definition.hpp create mode 100644 src/offline/offline_region_error.cpp create mode 100644 src/offline/offline_region_error.hpp create mode 100644 src/offline/offline_region_status.cpp create mode 100644 src/offline/offline_region_status.hpp create mode 100644 src/run_loop.cpp create mode 100644 src/run_loop_impl.hpp create mode 100644 src/snapshotter/map_snapshot.cpp create mode 100644 src/snapshotter/map_snapshot.hpp create mode 100644 src/snapshotter/map_snapshotter.cpp create mode 100644 src/snapshotter/map_snapshotter.hpp create mode 100644 src/string_util.cpp create mode 100644 src/style/android_conversion.hpp create mode 100644 src/style/conversion/filter.cpp create mode 100644 src/style/conversion/filter.hpp create mode 100644 src/style/conversion/position.cpp create mode 100644 src/style/conversion/position.hpp create mode 100644 src/style/conversion/property_expression.hpp create mode 100644 src/style/conversion/property_value.hpp create mode 100644 src/style/conversion/transition_options.cpp create mode 100644 src/style/conversion/transition_options.hpp create mode 100644 src/style/conversion/url_or_tileset.cpp create mode 100644 src/style/conversion/url_or_tileset.hpp create mode 100644 src/style/formatted.cpp create mode 100644 src/style/formatted.hpp create mode 100644 src/style/formatted_section.cpp create mode 100644 src/style/formatted_section.hpp create mode 100644 src/style/layers/background_layer.cpp create mode 100644 src/style/layers/background_layer.hpp create mode 100644 src/style/layers/circle_layer.cpp create mode 100644 src/style/layers/circle_layer.hpp create mode 100644 src/style/layers/custom_layer.cpp create mode 100644 src/style/layers/custom_layer.hpp create mode 100644 src/style/layers/fill_extrusion_layer.cpp create mode 100644 src/style/layers/fill_extrusion_layer.hpp create mode 100644 src/style/layers/fill_layer.cpp create mode 100644 src/style/layers/fill_layer.hpp create mode 100644 src/style/layers/heatmap_layer.cpp create mode 100644 src/style/layers/heatmap_layer.hpp create mode 100644 src/style/layers/hillshade_layer.cpp create mode 100644 src/style/layers/hillshade_layer.hpp create mode 100644 src/style/layers/layer.cpp create mode 100644 src/style/layers/layer.cpp.ejs create mode 100644 src/style/layers/layer.hpp create mode 100644 src/style/layers/layer.hpp.ejs create mode 100644 src/style/layers/layer_manager.cpp create mode 100644 src/style/layers/layer_manager.hpp create mode 100644 src/style/layers/line_layer.cpp create mode 100644 src/style/layers/line_layer.hpp create mode 100644 src/style/layers/raster_layer.cpp create mode 100644 src/style/layers/raster_layer.hpp create mode 100644 src/style/layers/symbol_layer.cpp create mode 100644 src/style/layers/symbol_layer.hpp create mode 100644 src/style/light.cpp create mode 100644 src/style/light.hpp create mode 100644 src/style/position.cpp create mode 100644 src/style/position.hpp create mode 100644 src/style/sources/custom_geometry_source.cpp create mode 100644 src/style/sources/custom_geometry_source.hpp create mode 100644 src/style/sources/geojson_source.cpp create mode 100644 src/style/sources/geojson_source.hpp create mode 100644 src/style/sources/image_source.cpp create mode 100644 src/style/sources/image_source.hpp create mode 100644 src/style/sources/raster_dem_source.cpp create mode 100644 src/style/sources/raster_dem_source.hpp create mode 100644 src/style/sources/raster_source.cpp create mode 100644 src/style/sources/raster_source.hpp create mode 100644 src/style/sources/source.cpp create mode 100644 src/style/sources/source.hpp create mode 100644 src/style/sources/unknown_source.cpp create mode 100644 src/style/sources/unknown_source.hpp create mode 100644 src/style/sources/vector_source.cpp create mode 100644 src/style/sources/vector_source.hpp create mode 100644 src/style/transition_options.cpp create mode 100644 src/style/transition_options.hpp create mode 100644 src/style/value.cpp create mode 100644 src/style/value.hpp create mode 100644 src/test/benchmark_runner.cpp create mode 100644 src/test/runtime.cpp create mode 100644 src/test/runtime.hpp create mode 100644 src/test/test_runner.cpp create mode 100644 src/text/collator.cpp create mode 100644 src/text/collator_jni.hpp create mode 100644 src/text/format_number.cpp create mode 100644 src/text/format_number_jni.hpp create mode 100644 src/text/local_glyph_rasterizer.cpp create mode 100644 src/text/local_glyph_rasterizer_jni.hpp create mode 100644 src/thread.cpp create mode 100644 src/timer.cpp create mode 100644 src/unaccent.cpp create mode 160000 vendor/mapbox-events-android create mode 160000 vendor/mapbox-gestures-android create mode 160000 vendor/mapbox-gl-native create mode 160000 vendor/mapbox-java diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..8a6c90a0d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{kt,kts}] +indent_size=2 +insert_final_newline=false +disabled_rules=no-wildcard-imports \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..ff3db8735 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Gradle files +.gradle/ + +# IntelliJ files +.idea/ +*.iml + +# Build files +build/ +.externalNativeBuild +**/.cxx +*.so +*.apk + +# Local settings +local.properties +gradle/configuration.gradle + +# Token file +MapboxGLAndroidSDKTestApp/src/main/res/values/developer-config.xml + +# Capture files +captures/ + +# Generated test cases +MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/gen/ + +# Generated list files from code generation +/scripts/generate-style-code.list + +# Input files for running render tests +MapboxGLAndroidSDKTestApp/src/main/assets/integration/ + +# Local AS configuration file +gradle.properties \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..e59e6f17d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "vendor/mapbox-gl-native"] + path = vendor/mapbox-gl-native + url = git@github.com:mapbox/mapbox-gl-native.git +[submodule "vendor/mapbox-events-android"] + path = vendor/mapbox-events-android + url = git@github.com:mapbox/mapbox-events-android.git +[submodule "vendor/mapbox-java"] + path = vendor/mapbox-java + url = git@github.com:mapbox/mapbox-java.git +[submodule "vendor/mapbox-gestures-android"] + path = vendor/mapbox-gestures-android + url = git@github.com:mapbox/mapbox-gestures-android.git diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..5fb33f7ca --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1767 @@ +# Changelog for the Mapbox Maps SDK for Android + +Mapbox welcomes participation and contributions from everyone. If you'd like to do so please see the [`Contributing Guide`](https://github.com/mapbox/mapbox-gl-native/blob/master/CONTRIBUTING.md) first to get started. + +## master +### Bug fixes + - Fixed an issue of integer overflow when converting `tileCoordinates` to `LatLon`, which caused issues such as `queryRenderedFeatures` and `querySourceFeatures` returning incorrect coordinates at zoom levels 20 and higher. [#15560](https://github.com/mapbox/mapbox-gl-native/pull/15560) + +## 8.4.0-alpha.1 - September 4, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.3.0...android-v8.4.0-alpha.1) since [Mapbox Maps SDK for Android v8.3.0](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.3.0): + +### Features + - Introduce `clusterProperties` option for aggregated cluster properties. [#15425](https://github.com/mapbox/mapbox-gl-native/pull/15425) + - Expose the `CameraPosition#padding` field and associated utility camera position builders. This gives a choice to set a persisting map padding immediately during a transition instead of setting it lazily `MapboxMap#setPadding`, which required scheduling additional transition to be applied. This also deprecates `MapboxMap#setPadding` as there should be no need for a lazy padding setter. [#15444](https://github.com/mapbox/mapbox-gl-native/pull/15444) + - Add number-format expression that allows to format a number to a string, with configurations as minimal/maximal fraction and locale/currency. [#15424](https://github.com/mapbox/mapbox-gl-native/pull/15424) + - Enable using of `text-offset` option together with `text-variable-anchor` (if `text-radial-offset` option is not provided). If used with `text-variable-anchor`, input values will be taken as absolute values. Offsets along the x- and y-axis will be applied automatically based on the anchor position. [#15542](https://github.com/mapbox/mapbox-gl-native/pull/15542) + +### Bug fixes +- Fixed a rendering issue of `collisionBox` when `text-translate` or `icon-translate` is enabled. [#15467](https://github.com/mapbox/mapbox-gl-native/pull/15467) +- Fixed unnecessary layers blink when the same components are re-added after the style reload [#15513](https://github.com/mapbox/mapbox-gl-native/pull/15513) +- Fixed an issue in JNI marshaling code that converted a float to long that produced invalid expressions [#15557](https://github.com/mapbox/mapbox-gl-native/pull/15557) +- Call callback onFinish() immediately when trying to move camera to current position [#15558](https://github.com/mapbox/mapbox-gl-native/pull/15558) + +### Performance improvements + - Mark used offline region resources in batches. [#15521](https://github.com/mapbox/mapbox-gl-native/pull/15521) + +### Other Changes +- JNI binding refactor [#15462](https://github.com/mapbox/mapbox-gl-native/pull/15462) +- Javadoc changes to clarify OfflineManager methods [#15519](https://github.com/mapbox/mapbox-gl-native/pull/15519) + +## 8.3.0 - August 28, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.3.0-beta.1...android-v8.3.0) since [Mapbox Maps SDK for Android v8.3.0-beta.1](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.3.0-beta.1): + +This release changes how offline tile requests are billed — they are now billed on a pay-as-you-go basis and all developers are able raise the offline tile limit for their users. Offline requests were previously exempt from monthly active user (MAU) billing and increasing the offline per-user tile limit to more than 6,000 tiles required the purchase of an enterprise license. By upgrading to this release, you are opting into the changes outlined in [this blog post](https://blog.mapbox.com/offline-maps-for-all-bb0fc51827be) and [#15380](https://github.com/mapbox/mapbox-gl-native/pull/15380). + +### Features + - Allow ability to pass a string array resource into `localIdeographFontFamily` for enabling/disabling the feature and specifying fallback fonts. [#15488](https://github.com/mapbox/mapbox-gl-native/pull/15488) + +### Bug fixes + - Fixed a rendering issue caused by all icons being treated as SDFs if an SDF and non-SDF icon were in the same layer. [#15456](https://github.com/mapbox/mapbox-gl-native/pull/15456) + - Fixed an issue where changing location's render mode when the`LocationComponent` is disable wouldn't invalidate the foreground icon when it's back enabled. [#15507](https://github.com/mapbox/mapbox-gl-native/pull/15507) + +## 8.2.2 - August 23, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.2.1...android-v8.2.2) since [Mapbox Maps SDK for Android v8.2.1](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.2.1): + +### Bug fixes + - Fixed a possible crash that could be caused by invoking the wrong layer implementation casting function. [#15398](https://github.com/mapbox/mapbox-gl-native/pull/15398) + - Fixed use of objects after moving, potentially causing crashes. [#15408](https://github.com/mapbox/mapbox-gl-native/pull/1540) + - Fixed a wrong calculation of visibility in high pitch scenarios which might've led to tiles in the viewport not being rendered. [#15461](https://github.com/mapbox/mapbox-gl-native/pull/15461) + +### Other Changes + - Do not invalidate the camera immediately when map padding is set. Reverts a change introduced in the `v8.2.0`. [#15437](https://github.com/mapbox/mapbox-gl-native/pull/15437) + +## 8.3.0-beta.1 - August 22, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.3.0-alpha.3...android-v8.3.0-beta.1) since [Mapbox Maps SDK for Android v8.3.0-alpha.3](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.3.0-alpha.3): + +### Bug fixes + - Fixed a bug where glyphs generated through the LocalGlyphRasterizer interface were changing fonts during some zoom changes. [#15407](https://github.com/mapbox/mapbox-gl-native/pull/15407) + - Fixed use of objects after moving, potentially causing crashes. [#15408](https://github.com/mapbox/mapbox-gl-native/pull/15408) + - Fixed a possible crash that could be caused by invoking the wrong layer implementation casting function [#15398](https://github.com/mapbox/mapbox-gl-native/pull/15398). + - Font lookup on pre lollipop devices failed, provide default font list instead [#15410](https://github.com/mapbox/mapbox-gl-native/pull/15410). + - Fixed rendering and collision detection issues with using `text-variable-anchor` and `icon-text-fit` properties on the same layer [#15367](https://github.com/mapbox/mapbox-gl-native/pull/15367). + - Fixed a bug where quick-scale was registered during a move gesture that followed a double-tap. Bumped gestures library to `v0.5.1`. [#15427](https://github.com/mapbox/mapbox-gl-native/pull/15427) + - Fixed symbol overlap when zooming out quickly. [#15416](https://github.com/mapbox/mapbox-gl-native/pull/15416) + +### Other Changes + - Do not invalidate the camera immediately when map padding is set. Reverts a change introduced in the `v8.2.0`. [#15437](https://github.com/mapbox/mapbox-gl-native/pull/15437) + +### Docs + - Add javadoc to OnStyleImageMissingListener indicating the required synchronous addition images, document workaround [#15418](https://github.com/mapbox/mapbox-gl-native/pull/15418) + +## 8.3.0-alpha.3 - August 15, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.3.0-alpha.2...android-v8.3.0-alpha.3) since [Mapbox Maps SDK for Android v8.3.0-alpha.2](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.3.0-alpha.2): + +### Gestures handling + - Bumped gestures library version to v0.5.0 which replaces the compat gesture detector with a custom scale gesture detector implementation. This is a softly breaking change because the underlying scale gesture detector reference has been removed. This brings a set of improvements to gestures without removing any functionality from the previous implementation. [#15136](https://github.com/mapbox/mapbox-gl-native/pull/15136) + - Improved scale and rotate gestures by refining sensitivity thresholds and animation velocity. [#15136](https://github.com/mapbox/mapbox-gl-native/pull/15136) + - Improved quick-zoom gestures by making zoom changes linear. Quick-zoom scale changes are now executed based on y-axis delta changes. [#15386](https://github.com/mapbox/mapbox-gl-native/pull/15386) + +### Features + - Introduced the text-writing-mode layout property for symbol layers. This property allows labels to be placed horizontally or vertically. The property accepts an array of enumeration values from a ( horizontal | vertical ) set. [#14932](https://github.com/mapbox/mapbox-gl-native/pull/14932) + - Added support for fallback fonts in `localIdeographFontFamily`. [#15255](https://github.com/mapbox/mapbox-gl-native/pull/15255) + +### Bug fixes + - Fixed an issue where it was possible to set the map’s content insets then tilt the map enough to see the horizon, causing performance issues [#15195](https://github.com/mapbox/mapbox-gl-native/pull/15195) + - Allow loading of a map without a style URI or style JSON [#15293](https://github.com/mapbox/mapbox-gl-native/pull/15293) + - Fixed an issue where animated camera transitions zoomed in or out too dramatically [#15281](https://github.com/mapbox/mapbox-gl-native/pull/15281) + - Enable variable label placement when `text-allow-overlap` property is set to true [#15354](https://github.com/mapbox/mapbox-gl-native/pull/15354) + +## 8.3.0-alpha.2 - August 7, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.3.0-alpha.1...android-v8.3.0-alpha.2) since [Mapbox Maps SDK for Android v8.3.0-alpha.1](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.3.0-alpha.1): + +### Bug fixes + - Load a style without an URI or JSON [#15293](https://github.com/mapbox/mapbox-gl-native/pull/15293) + - Do not try to wake up the RunLoop if a wake is already pending. Fixes offline downloads that could freeze after resuming. [#15330](https://github.com/mapbox/mapbox-gl-native/pull/15330) + +## 8.0.2 - July 31, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.0.1...android-v8.0.2) since [Mapbox Maps SDK for Android v8.0.1](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.0.1): + +### Bug fixes + - Fixed rendering layers after fill-extrusion regression caused by optimization of fill-extrusion rendering [#15065](https://github.com/mapbox/mapbox-gl-native/pull/15065) + - Fixed a map update bug caused by the render tiles and the render passes getting unsynchronized [#15092](https://github.com/mapbox/mapbox-gl-native/pull/15092) + - Fixed a custom geometry source bug caused by using the outdated tiles after style update [#15112](https://github.com/mapbox/mapbox-gl-native/pull/15112) + - Ensure location shadow's gradient radius is greater than 0 [#15099](https://github.com/mapbox/mapbox-gl-native/pull/15099) + +## 7.4.1 - July 31, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v7.4.0...android-v7.4.1) since [Mapbox Maps SDK for Android v7.4.0](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v7.4.0): + +### Bug fixes + - Fixed a map update bug caused by the render tiles and the render passes getting unsynchronized [#15092](https://github.com/mapbox/mapbox-gl-native/pull/15092) + - Ensure location shadow's gradient radius is greater than 0 [#15099](https://github.com/mapbox/mapbox-gl-native/pull/15099) + - Fixed a custom geometry source bug caused by using the outdated tiles after style update [#15112](https://github.com/mapbox/mapbox-gl-native/pull/15112) + +## 8.3.0-alpha.1 - July 31, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.2.0...android-v8.3.0-alpha.1) since [Mapbox Maps SDK for Android v8.2.0](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.2.0): + +### Features + - Do not include CJK ideographs in the offline packs by default. This decreases overall offline download size but changes appearance by a default set local glyph generation [#14269](https://github.com/mapbox/mapbox-gl-native/pull/14269) + - Update target SDK to 28, update support library and fix gradle warnings [#15135](https://github.com/mapbox/mapbox-gl-native/pull/15135) + - Introduce VertexVector::extend() and use it in placement code [#15194](https://github.com/mapbox/mapbox-gl-native/pull/15194) + +### Bug fixes + - Save location animation timestamp only when fed. Fixes an issue where external camera updates impacted location animation duration[#15265](https://github.com/mapbox/mapbox-gl-native/pull/15265) + - Fixed flickering on style change for the same tile set [#15127](https://github.com/mapbox/mapbox-gl-native/pull/15127) + - Fix location render/camera modes being reinitialized even when the new mode is the same [#15266](https://github.com/mapbox/mapbox-gl-native/pull/15266) + - Ensure default local ideographs font family is not overwrote. Fix a bug that prevented local CJK glyphs generation. The local generation is now enabled by default and the font family used for generation is set to "sans-serif. [#15253](https://github.com/mapbox/mapbox-gl-native/pull/15253) + - Fix int overflow issue in GridIndex [#15245](https://github.com/mapbox/mapbox-gl-native/pull/15245) + - Align gesture animation reason for onCameraMoveStarted [#15218](https://github.com/mapbox/mapbox-gl-native/pull/15218) + - Remove layers first when clearing the style, fixes unnecessary log dumps [#15191](https://github.com/mapbox/mapbox-gl-native/pull/15191) + - Remove unused field from icon buffer [#15189](https://github.com/mapbox/mapbox-gl-native/pull/15189) + +### Performance improvements + - Release quad data after vertex buffers are created [#15189](https://github.com/mapbox/mapbox-gl-native/pull/15189) + - Decrease size of SymbolBucket [#15178](https://github.com/mapbox/mapbox-gl-native/pull/15178) + - Avoid geometry collections copying [#15201](https://github.com/mapbox/mapbox-gl-native/pull/15201) + +## 8.2.1 - July 31, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.2.0...android-v8.2.1) since [Mapbox Maps SDK for Android v8.2.0](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.2.0): + +### Bug fixes + - Save location animation timestamp only when fed [#15265](https://github.com/mapbox/mapbox-gl-native/pull/15265) + +## 8.1.1 - July 26, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.1.0...android-v8.1.1) since [Mapbox Maps SDK for Android v8.1.0](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.1.0): + +### Bug fixes + - Fixed rendering layers after fill-extrusion regression caused by optimization of fill-extrusion rendering [#15065](https://github.com/mapbox/mapbox-gl-native/pull/15065) + - Fixed a map update bug caused by the render tiles and the render passes getting unsynchronized [#15092](https://github.com/mapbox/mapbox-gl-native/pull/15092) + - Fixed collision with content insets [#15130](https://github.com/mapbox/mapbox-gl-native/pull/15130) + - Fixed a custom geometry source bug caused by using the outdated tiles after style update [#15112](https://github.com/mapbox/mapbox-gl-native/pull/15112) + - Allow map panning after quick zoom is disabled but a phantom gesture is executed [#15093](https://github.com/mapbox/mapbox-gl-native/pull/15093) + - Ensure location shadow's gradient radius is greater than 0 [#15099](https://github.com/mapbox/mapbox-gl-native/pull/15099) + - Ensure that move detector is enabled if double-tap is interrupted [#15103](https://github.com/mapbox/mapbox-gl-native/pull/15103) + +## 8.2.0 - July 24, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.2.0-beta.1...android-v8.2.0) since [Mapbox Maps SDK for Android v8.2.0-beta.1](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.2.0-beta.1): + +### Bug fixes + - Fix rendering of layers that are on top of fill-extrusion layers [#15065](https://github.com/mapbox/mapbox-gl-native/pull/15065) + +## 8.2.0-beta.1 - July 18, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.2.0-alpha.3...android-v8.2.0-beta.1) since [Mapbox Maps SDK for Android v8.2.0-alpha.3](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.2.0-alpha.3): + +### Bug fixes + - Fix `CustomGeometrySource` bug caused by usage of outdated tiles after a style update [#15112](https://github.com/mapbox/mapbox-gl-native/pull/15112) + - Fix bug where previous anchors set in `text-variable-anchor` were still preferred after anchor options were reset [#15129](https://github.com/mapbox/mapbox-gl-native/pull/15129) + - Fix issue where annotations could not be selected due to a regression in content insets [#15130](https://github.com/mapbox/mapbox-gl-native/pull/15130) + +## 8.2.0-alpha.3 - July 11, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.2.0-alpha.2...android-v8.2.0-alpha.3) since [Mapbox Maps SDK for Android v8.2.0-alpha.2](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.2.0-alpha.2): + +### Bug fixes + - Fix map update bug caused by render tiles and render passes becoming unsynchronized [#15092](https://github.com/mapbox/mapbox-gl-native/pull/15092) + - Fix fill-extrusion layers always rendered above other layers [#15065](https://github.com/mapbox/mapbox-gl-native/pull/15065) + - Fix a bug where map panning would be blocked after a quick zoom gesture was attempted when quick zoom is set to be ignored by `UiSettings` preferences [#15093](https://github.com/mapbox/mapbox-gl-native/pull/15093) + - Fix a bug where map panning would be blocked if double-tap is interrupted [#15103](https://github.com/mapbox/mapbox-gl-native/pull/15103) + +### Minor features and improvements + - Improve performance of queryRenderedFeatures by removing unnecessary iterations over vectors and unneeded layers [#14930](https://github.com/mapbox/mapbox-gl-native/pull/14930) + - Decrease the minimum pixel distance between the current camera position and the target required to interpolate during camera animation [#15073](https://github.com/mapbox/mapbox-gl-native/pull/15073) + - Introduce `MapboxMapOptions` facility creation method [#15069](https://github.com/mapbox/mapbox-gl-native/pull/15069) + - Throw configuration exception when accessing Mapbox services with an invalid token [#15081](https://github.com/mapbox/mapbox-gl-native/pull/15081) + - Allow setting a null access token in the runtime [#15081](https://github.com/mapbox/mapbox-gl-native/pull/15081) + - Ensure location shadow's gradient radius is greater than 0 [#15099](https://github.com/mapbox/mapbox-gl-native/pull/15099) + +### Other Changes + - Update okhttp to latest stable version that support pre lollipop (`3.12.3`) [#15087](https://github.com/mapbox/mapbox-gl-native/pull/15087) + - Fix up "match" expressions docs with stop arguments [#15102](https://github.com/mapbox/mapbox-gl-native/pull/15102) + +## 8.2.0-alpha.2 - July 3, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.2.0-alpha.1...android-v8.2.0-alpha.2) since [Mapbox Maps SDK for Android v8.2.0-alpha.1](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.2.0-alpha.1): + +### Major features + - Cache management API [#14962](https://github.com/mapbox/mapbox-gl-native/pull/14962) + +### Bug fixes + - Fix style change transition regression caused by delayed setting of the updated layer properties [#15016](https://github.com/mapbox/mapbox-gl-native/pull/15016) + +### Minor features and improvements + - Include Hiragana and Katakana glyph ranges to LocalGlyphRasterizer [#15009](https://github.com/mapbox/mapbox-gl-native/pull/15009) + +### Other Changes + - Update CI image with NDK r20 [#15005](https://github.com/mapbox/mapbox-gl-native/pull/15005) + +## 8.2.0-alpha.1 - June 26, 2019 +[Changes](https://github.com/mapbox/mapbox-gl-native/compare/android-v8.1.0...android-v8.2.0-alpha.1) since [Mapbox Maps SDK for Android v8.1.0](https://github.com/mapbox/mapbox-gl-native/releases/tag/android-v8.1.0): + +### Bug fixes + - Fix `queryRenderedFeatues` bug caused by incorrect sort feature index calculation [#14884](https://github.com/mapbox/mapbox-gl-native/pull/14884) + - Fix performance regression by switching back to a more compact line attributes layout [#14851](https://github.com/mapbox/mapbox-gl-native/pull/14851) + - Fix a bug that restricted camera viewport after camera bounds were reset [#14882](https://github.com/mapbox/mapbox-gl-native/pull/14882) + - Fix a bug that ignored floating point values during core to platform color conversion [#14954](https://github.com/mapbox/mapbox-gl-native/pull/14954) + - Implemented asymmetric center of perspective: fixed an issue that caused the focal point to be always based on the view's horizontal center when setting [MapboxMap setPadding](https://docs.mapbox.com/android/api/map-sdk/8.0.0/com/mapbox/mapboxsdk/maps/MapboxMap.html#setPadding-int-int-int-int-). [#14664](https://github.com/mapbox/mapbox-gl-native/pull/14664) + +### Minor features and improvements + - Allow null updates to GeoJsonSource [#14898](https://github.com/mapbox/mapbox-gl-native/pull/14898) + +### Other Changes + - Add resource usage to HttpRequest, add offline query flag to HttpRequest [#14837](https://github.com/mapbox/mapbox-gl-native/pull/14837) + +## 8.1.0 - June 20, 2019 + +### Bugs + - Accept floating numbers during core->platform color conversion [#14954](https://github.com/mapbox/mapbox-gl-native/pull/14954) + - Ensure to re-enable move when quickzoom finishes [#14965](https://github.com/mapbox/mapbox-gl-native/pull/14965) + +### Features + - Limit concurrent request for Android 4.4 and below to 10 [#14971](https://github.com/mapbox/mapbox-gl-native/pull/14971) + +_Note about #14971:_ This pr makes an adjustment as a precautionary measure towards a rare bug that was discovered via instrumented unit tests on Android 4.4 (aka KitKat). This bug isn’t tied to a code change in the Mapbox Maps SDK for Android. Rather, the root cause is on the Android OS (and how it interacts with OkHttp). + +The Maps SDK doesn’t directly communicate with this system library that the bug originates from, but goes through an external dependency that handles the http communication for the Maps SDK. Capturing from the upstream repository that the code path to this system library isn’t thread safe in Android 4.4 and can result in the native crash above. The only way to fix this issue is by upgrading the end-user device to Android 5.0 or higher which isn’t possible if the device manufacturer hasn’t released an update for it. + +Please see https://github.com/square/okhttp/issues/2679 for more information. + +## 8.1.0-beta.1 - June 13, 2019 + +### Bugs + - Fix compilation warning with updated clang [#14865](https://github.com/mapbox/mapbox-gl-native/pull/14865) + - Fix reset behavior for setLatLngBoundsForCameraTarget [#14882](https://github.com/mapbox/mapbox-gl-native/pull/14882) + +### Features + - Add resource usage to HttpRequest, add offline query param [#14837](https://github.com/mapbox/mapbox-gl-native/pull/14837) + +## 8.0.1 - June 11, 2019 + +### Bugs + - Reset the native renderer only when the GL thread exits [#14841](https://github.com/mapbox/mapbox-gl-native/pull/14841) + - Change library loader from Relinker to SoLoader [#14890](https://github.com/mapbox/mapbox-gl-native/pull/14890) + +## 8.1.0-alpha.2 - June 6, 2019 + +### Bugs + - Copy features array before passing them to core [#14804](https://github.com/mapbox/mapbox-gl-native/pull/14804) + - Improve test stability [#14853](https://github.com/mapbox/mapbox-gl-native/pull/14853) + +### Features + - Rename URL API to URI [#14836](https://github.com/mapbox/mapbox-gl-native/pull/14836) + +### Build + - Bumping Android telemetry to 4.5.1 [#14874](https://github.com/mapbox/mapbox-gl-native/pull/14874) + +### Docs + - Add external links to javadoc generation, fix warnings [#14792](https://github.com/mapbox/mapbox-gl-native/pull/14792) + +## 8.1.0-alpha.1 - June 3, 2019 + +### Bugs + - Return color string values in the [0-255] range [#14233](https://github.com/mapbox/mapbox-gl-native/pull/14233) + - Resume file source to complete resources cache path change [#14546](https://github.com/mapbox/mapbox-gl-native/pull/14546) + - Remove binary shader support [#14707](https://github.com/mapbox/mapbox-gl-native/pull/14707) + - Deprecate MapboxMapOptions empty constructor [#14748](https://github.com/mapbox/mapbox-gl-native/pull/14748) + + +### Features + - Break location camera tracking when a developer invoked animation starts [#14745](https://github.com/mapbox/mapbox-gl-native/pull/14745) + - Use URI instead of URL to enable asset loading [#14750](https://github.com/mapbox/mapbox-gl-native/pull/14750) + - Option to change the zoom rate. Decrease the default zoom rate by 35% [#14774](https://github.com/mapbox/mapbox-gl-native/pull/14774) + - Provide API to control eviction of cached images [#14610](https://github.com/mapbox/mapbox-gl-native/pull/14610) + - Android specific thread pool that keeps threads attached to JVM [#14450](https://github.com/mapbox/mapbox-gl-native/pull/14450) + - Asymmetric viewport when edge insets are specified [#14664](https://github.com/mapbox/mapbox-gl-native/pull/14664) + +### Build + - Update maven publishing script to enable aar packaging [#14778](https://github.com/mapbox/mapbox-gl-native/pull/14778) + - Do not try to package duplicate files in the publication [#14822](https://github.com/mapbox/mapbox-gl-native/pull/14822) + +### Docs + - Document that elevation(0) turns off the location shadow [#14723](https://github.com/mapbox/mapbox-gl-native/pull/14723) + +## 8.0.0 - May 22, 2019 + - This release improves the method for counting monthly active users. By upgrading to this release, you are opting into the changes outlined [in this blog post](https://www.mapbox.com/52219). [#14404](https://github.com/mapbox/mapbox-gl-native/pull/14404) + +## 6.8.2 - May 17, 2019 + +### Bugs + - Avoid binary shader load race condition with using multiple maps, disable binary shaders [#14298](https://github.com/mapbox/mapbox-gl-native/pull/14298) + +## 7.5.0-beta.1 - May 17, 2019 + +### Features + - Option to provide custom values for location camera transition [#14606](https://github.com/mapbox/mapbox-gl-native/pull/14606) + +### Bugs + - Expose trackingGesturesManagement attribute [#14659](https://github.com/mapbox/mapbox-gl-native/pull/14659) + - Harden onLowMemory call, use atomic boolean for thread safe management of the render surface [#14622](https://github.com/mapbox/mapbox-gl-native/pull/14622) + - Normalize previous rotation values of the location animator [#14626](https://github.com/mapbox/mapbox-gl-native/pull/14626) + +### Performance + - Remove telemetry gesture event handling code [#14627](https://github.com/mapbox/mapbox-gl-native/pull/14627) + +### Localisation + - Pull in Polish translation and updates to Czech and Hungarian [#14679](https://github.com/mapbox/mapbox-gl-native/pull/14679) + +### Docs + - Add javadoc to highlight incompatible onIdle event with snapshot api [#14621](https://github.com/mapbox/mapbox-gl-native/pull/14621) + +## 7.4.0 - May 15, 2019 +### Features + - Revert SKU token logic [#14594](https://github.com/mapbox/mapbox-gl-native/pull/14594) + +### Bugs + - Keep strong references to the FileSource/OfflineManager operations callbacks [#14601](https://github.com/mapbox/mapbox-gl-native/pull/14601) + - Failed sprite requests do not block tiles rendering [#14605](https://github.com/mapbox/mapbox-gl-native/pull/14605) + - Remove missing image log [#14600](https://github.com/mapbox/mapbox-gl-native/pull/14600) + +## 7.5.0-alpha.2 - May 8, 2019 +### Bugs + - Keep shared_ptr to thread pool in CustomGeometrySource [#14562](https://github.com/mapbox/mapbox-gl-native/pull/14562) + - Changed placement order of symbols to match the viewport-y order when the `PropertyValue#symbolZOrder` is set to `SYMBOL_Z_ORDER_VIEWPORT_Y`, improving collision detection and rendering of symbol layers when icons are allowed to overlap but not text. [#14486](https://github.com/mapbox/mapbox-gl-native/pull/14486) + +## 7.5.0-alpha.1 - May 2, 2019 +### Bugs + - Use same OfflineRegion#isComplete logic as core [#14500](https://github.com/mapbox/mapbox-gl-native/pull/14500) + - Add toString, hashcode and equals to OfflineRegionError [#14499](https://github.com/mapbox/mapbox-gl-native/pull/14499) + +### Features + - Add "layer-above" option to the LocationComponent [#14497](https://github.com/mapbox/mapbox-gl-native/pull/14497) + +### Build + - Update mapbox java sdk to v4.7.0 [#14480](https://github.com/mapbox/mapbox-gl-native/pull/14480) + - Update gestures library to v0.4.2 [#14524](https://github.com/mapbox/mapbox-gl-native/pull/14524) + +### Docs + - Clear up LocationComponent's z-index positioning docs [#14497](https://github.com/mapbox/mapbox-gl-native/pull/14497) + +## 7.4.0-beta.3 - May 2, 2019 +### Features +- Less strict rules for adding render layers to sources [#14540](https://github.com/mapbox/mapbox-gl-native/pull/14540) +- Store type of renderable segment when sorting symbols by key [#14517](https://github.com/mapbox/mapbox-gl-native/pull/14517) + +### Build + - Bump gestures library to v0.4.2 [#14524](https://github.com/mapbox/mapbox-gl-native/pull/14524) + +## 7.4.0-beta.2 - April 23, 2019 +### Features + - Enable/Disable SKU token handling [#14476](https://github.com/mapbox/mapbox-gl-native/pull/14476) + +## 7.4.0-beta.1 - April 19, 2019 +### Bugs + - Order render items properly across several sources [#14408](https://github.com/mapbox/mapbox-gl-native/pull/14408) + - Ignore equal offline region's download state updates [#14399](https://github.com/mapbox/mapbox-gl-native/pull/14399) + - Activate the ConnectivityReceiver when starting the download [#14397](https://github.com/mapbox/mapbox-gl-native/pull/14397) + +### Features + - Port symbol-sort-key symbol layout property [#14386](https://github.com/mapbox/mapbox-gl-native/pull/14386) + - Add style image missing binding integration [#14320](https://github.com/mapbox/mapbox-gl-native/pull/14320) + - Allow snapshot generation without overlain view content [#14413](https://github.com/mapbox/mapbox-gl-native/pull/14413) + - Introduce AccountsManager to support SKU tokens in API requests [#14404](https://github.com/mapbox/mapbox-gl-native/pull/14404) + - Add MapEventFactory [#14309](https://github.com/mapbox/mapbox-gl-native/pull/14309) + - Only perform thread checking in debug builds [#14293](https://github.com/mapbox/mapbox-gl-native/pull/14293) + +### Build + - Bump gestures library to v0.4.1 [#14435](https://github.com/mapbox/mapbox-gl-native/pull/14435) + - Update mapbox java sdk to v4.6.0 [#14424](https://github.com/mapbox/mapbox-gl-native/pull/14424) + - Bump telemetry to 4.4.1 [#14309](https://github.com/mapbox/mapbox-gl-native/pull/14309) + - Program against NativeMap [#14412](https://github.com/mapbox/mapbox-gl-native/pull/14412) + +## 7.3.2 - April 17, 2019 +## Bugs + - Unify has surface state handling, allow querying after map resume [#14426](https://github.com/mapbox/mapbox-gl-native/pull/14426) + +## 7.3.1 - April 12, 2019 +## Bugs + - Harden renderer invocation when Activity is resumed [#14395](https://github.com/mapbox/mapbox-gl-native/pull/14395) + - Check for Mapbox#INSTANCE when initializing the MapView [#14366](https://github.com/mapbox/mapbox-gl-native/pull/14366) + - Disable binary programs shaders [#14298](https://github.com/mapbox/mapbox-gl-native/pull/14298) + +## 7.4.0-alpha.2 - April 10, 2019 +### Bugs + - Check for Mapbox#INSTANCE when initializing the MapView [#14366](https://github.com/mapbox/mapbox-gl-native/pull/14366) + - Clear previous style loaded listener when setting a new style[#14338](https://github.com/mapbox/mapbox-gl-native/pull/14338) + +### Features + - Allow resetting a custom OkHttp client to the default impl [#14349](https://github.com/mapbox/mapbox-gl-native/pull/14349) + - Expose LocationComponentConstants class [#14333](https://github.com/mapbox/mapbox-gl-native/pull/14333) + +### Build + - Fix build with mismatching glShaderSource signature[#14339](https://github.com/mapbox/mapbox-gl-native/pull/14339) + +## 7.4.0-alpha.1 - April 3, 2019 + +### Bugs + - Clean up location permissions annotation [#14311](https://github.com/mapbox/mapbox-gl-native/pull/14311) + - Clear camera callbacks' message queue when the map is destroyed [#14292](https://github.com/mapbox/mapbox-gl-native/pull/14292) + - Use a valid gestures focal point when resetting a manager [#14284](https://github.com/mapbox/mapbox-gl-native/pull/14284) + - Disable move gesture detector foreseeing the quickzoom [#14268](https://github.com/mapbox/mapbox-gl-native/pull/14268) + - Remove request render from MapboxMap#onStart [#14245](https://github.com/mapbox/mapbox-gl-native/pull/14245) + - Use TurfMeasurement#distance in LatLng#distanceTo [#14220](https://github.com/mapbox/mapbox-gl-native/pull/14220) + +### Features + - Add pixel dimension annotation to public UiSettings methods [#14281](https://github.com/mapbox/mapbox-gl-native/pull/14281) + - Add #toString override for formatted sections [#14247](https://github.com/mapbox/mapbox-gl-native/pull/14247) + - Harden fetching camera for bounds when padding is excessive [#14221](https://github.com/mapbox/mapbox-gl-native/pull/14221) + - Traverse expression tree when checking for property overrides [#14259](https://github.com/mapbox/mapbox-gl-native/pull/14259) + - Variable label placement [#14184](https://github.com/mapbox/mapbox-gl-native/pull/14184) + +### Build + - Disable binary programs until we fix [#14294]((https://github.com/mapbox/mapbox-gl-native/pull/14298) + - Disable leak canary during instrumentation tests [#14296](https://github.com/mapbox/mapbox-gl-native/pull/14296) + - Remove Android v7 support library [#14265](https://github.com/mapbox/mapbox-gl-native/pull/14265) + +## 7.3.0 - March 28, 2019 + +### Bugs + - Fix MapView reuse issues [#14127](https://github.com/mapbox/mapbox-gl-native/pull/14127) + - Don't call OnSurfaceCreated from the main thread [#14244](https://github.com/mapbox/mapbox-gl-native/pull/14244) + +## 7.3.0-beta.1 - March 20, 2019 + +### Features + - Expose "text-color" option for formatted sections [#14128](https://github.com/mapbox/mapbox-gl-native/pull/14128) + - Expose LocationComponent's layer IDs [#14155](https://github.com/mapbox/mapbox-gl-native/pull/14155) + +### Bugs + - Cache location layer IDs in a set instead of a list [#14141](https://github.com/mapbox/mapbox-gl-native/pull/14141) + - Clear the style object when the map is destroyed [#14171](https://github.com/mapbox/mapbox-gl-native/pull/14171) + - Cache source/layer only when successfully added [#14171](https://github.com/mapbox/mapbox-gl-native/pull/14171) + +### Build + - Bump telemetry version to 4.3.0 [#14140](https://github.com/mapbox/mapbox-gl-native/pull/14140) + +## 7.3.0-alpha.2 - March 13, 2019 + +### Features +- Option to change path of the resources cache db [#13947](https://github.com/mapbox/mapbox-gl-native/pull/13947) +- Throw an exception if the component is accessed before the activation [#14068](https://github.com/mapbox/mapbox-gl-native/pull/14068) +- Add Galician localization [#14095](https://github.com/mapbox/mapbox-gl-native/pull/14095) + +### Bugs +- Set a blank style when an empty builder is provided [#14085](https://github.com/mapbox/mapbox-gl-native/pull/14085) +- Fix quick-zoom + double-tap gestures combo regression [#14084](https://github.com/mapbox/mapbox-gl-native/pull/14084) +- Explicitly declare MapboxIssueRegistry's lint version [#14086](https://github.com/mapbox/mapbox-gl-native/pull/14086) + +### Build +- Publish SNAPSHOTs to oss.jfrog.org [#14067](https://github.com/mapbox/mapbox-gl-native/pull/14067) + +## 7.3.0-alpha.1 - March 7, 2019 + +### Features +- Make ReLinker default library loader [#13974](https://github.com/mapbox/mapbox-gl-native/pull/13974) +- Add plural style builder methods [#14012](https://github.com/mapbox/mapbox-gl-native/pull/14012) +- Adding areAllGesturesEnabled() boolean retreival method to UiSettings class [#14022](https://github.com/mapbox/mapbox-gl-native/pull/14022) +- Adding builder pattern for LocationComponent activation [#13941](https://github.com/mapbox/mapbox-gl-native/pull/13941) +- Get vector drawables using AppCompatResources and set them programmatically [#14058](https://github.com/mapbox/mapbox-gl-native/pull/14058) + +### Bugs +- LatLngBounds: Fix constrain when crossing antimeridian [#13958](https://github.com/mapbox/mapbox-gl-native/pull/13958) +- Verify no native method is called when saving state [#13980](https://github.com/mapbox/mapbox-gl-native/pull/13980) +- Clear fragment's onMapReady callbacks as part of OnDestroy, not OnDestroyView [#14035](https://github.com/mapbox/mapbox-gl-native/pull/14035) +- Fix dialog fragment setup, add regression test [#14036](https://github.com/mapbox/mapbox-gl-native/pull/14036) +- Remove unused dem memory [#13550](https://github.com/mapbox/mapbox-gl-native/pull/13550) + +### Build +- Adding Bintray Gradle plugin for artifact publishing [#13999](https://github.com/mapbox/mapbox-gl-native/pull/13999) + +## 7.2.0 - February 27, 2019 + +### Bugs +- fix surface creation after app backgrounding for surfaceview[#13976](https://github.com/mapbox/mapbox-gl-native/pull/13976) + +## 7.2.0-beta.1 - February 21, 2019 + +### Bugs +- Keep location component's layers hidden when new style with the "layer-below" option is applied[#13936](https://github.com/mapbox/mapbox-gl-native/pull/13936) + +### Features +- Set localIdeographFontFamily default to sans-serif[#13925](https://github.com/mapbox/mapbox-gl-native/pull/13925) + +## 7.2.0-alpha.2 - February 14, 2019 + +### Bugs +- Invalidate the camera position when gesture finishes[#13920](https://github.com/mapbox/mapbox-gl-native/pull/13920) +- Hook into surface holder to cleanup renderer on the right thread before the surface is destroyed [#13926](https://github.com/mapbox/mapbox-gl-native/pull/13926) +- Fix deadlock in http request on hitting tile limit[#13858](https://github.com/mapbox/mapbox-gl-native/pull/13858) + +## 7.2.0-alpha.1 - February 7, 2019 + +### Build +- Update downstream circle-ci job name [#13872](https://github.com/mapbox/mapbox-gl-native/pull/13872) +- Prioritize running benchmarks when building master [#13801](https://github.com/mapbox/mapbox-gl-native/pull/13801) +- Revert modules vendorization and keep submodules only [#13806](https://github.com/mapbox/mapbox-gl-native/pull/13806) +- Run vendor submodule check [#13816](https://github.com/mapbox/mapbox-gl-native/pull/13816) +- Fetch tags during nitpick, bump gestures library to v0.4.0 [#13847](https://github.com/mapbox/mapbox-gl-native/pull/13847) + +### Bugs +- Test app fixes 7.1.0 QA [#13773](https://github.com/mapbox/mapbox-gl-native/pull/13773) +- Remove deprecated javacompile, resolve AS 3.3.0 warning on deprecation [#13777](https://github.com/mapbox/mapbox-gl-native/pull/13777) +- Update project readme with buck source file ignore configuration [#13788](https://github.com/mapbox/mapbox-gl-native/pull/13788) +- Fix LocationEngine's annotation [#13792](https://github.com/mapbox/mapbox-gl-native/pull/13792) +- Fix core-only layer creation [#13798](https://github.com/mapbox/mapbox-gl-native/pull/13798) +- Remove faulty cast to uint64_t, explict casting for cluster API [#13888](https://github.com/mapbox/mapbox-gl-native/pull/13888) + +### Features +- Separate quick zoom gesture option [#13791](https://github.com/mapbox/mapbox-gl-native/pull/13791) +- Location animators memory and CPU optimizations [#13678](https://github.com/mapbox/mapbox-gl-native/pull/13678) +- Added performance event [#13795](https://github.com/mapbox/mapbox-gl-native/pull/13795) +- Adding new variation of LocationComponent#activateLocationComponent [#13829](https://github.com/mapbox/mapbox-gl-native/pull/13829) +- Expose symbol placement transition duration option [#13868](https://github.com/mapbox/mapbox-gl-native/pull/13868) +- Add experimental pointer [#13889](https://github.com/mapbox/mapbox-gl-native/pull/13889) + +## 7.1.2 - February 7, 2019 + +### Bugs + - Fix layer not rendering correctly when property value's negative [#13888](https://github.com/mapbox/mapbox-gl-native/pull/13888) + +## 7.1.1 - February 1, 2019 + +### Bugs + - Fix a crash caused by the missing `Timber` dependency [#13847](https://github.com/mapbox/mapbox-gl-native/pull/13847) + +## 7.1.0 - January 30, 2019 + +### Build + - Revert Android vendorization, add submodule pinning [#13815](https://github.com/mapbox/mapbox-gl-native/pull/13815) + +## 7.1.0-beta.1 - January 23, 2019 + +### Bugs + - Ignore unused C++ arguments for all build environments [#13774](https://github.com/mapbox/mapbox-gl-native/pull/13774) + - Import gesture detector dimens using the correct package structure [#13750](https://github.com/mapbox/mapbox-gl-native/pull/13750) + - Restore location layer visibility with new "layer-below" [#13771](https://github.com/mapbox/mapbox-gl-native/pull/13771) + - Rename required TransitionOptions field [#13769](https://github.com/mapbox/mapbox-gl-native/pull/13769) + +### Build + - Ignore unused C++ arguments for all build environments [#13774](https://github.com/mapbox/mapbox-gl-native/pull/13774) + - Update to NDK 19, migrate STL experimental/optional [#13049](https://github.com/mapbox/mapbox-gl-native/pull/13049) + +## 7.1.0-alpha.3 - January 21, 2019 + +### Features + - Add library loading to peer objects [#13758](https://github.com/mapbox/mapbox-gl-native/pull/13758) + - Remove tile sorting from the clip and mask algorithms [#13756](https://github.com/mapbox/mapbox-gl-native/pull/13756) + - Ability to disable any layer using pre-processing flags [#13717](https://github.com/mapbox/mapbox-gl-native/pull/13717) + - Make library loader load the native library once [#13748](https://github.com/mapbox/mapbox-gl-native/pull/13748) + - Allow offline and snapshotter to be disabled [#13730](https://github.com/mapbox/mapbox-gl-native/pull/13730) + - Sorted render tiles for TilePyramid [#13739](https://github.com/mapbox/mapbox-gl-native/pull/13739) + +### Bugs + - Adjust maven pom file with vendorized libraries versions [#13765](https://github.com/mapbox/mapbox-gl-native/pull/13765) + - Update attribution feedback URL in code and tests [#13710](https://github.com/mapbox/mapbox-gl-native/pull/13710) + - Re-apply location components "layer-below" when changed [#13749](https://github.com/mapbox/mapbox-gl-native/pull/13749) + - Avoid leaking CompassView context through animator [#13743](https://github.com/mapbox/mapbox-gl-native/pull/13743) + - Allow resetting fps listener [#13733](https://github.com/mapbox/mapbox-gl-native/pull/13733) + +## 7.1.0-alpha.2 - January 16, 2019 + - Do not invoke #onCancel when animation is scheduled from #onFinish block [#13737](https://github.com/mapbox/mapbox-gl-native/pull/13737) + - Updated licenses after Android vendorization [#13738](https://github.com/mapbox/mapbox-gl-native/pull/13738) + - Use QueryFeatureExtension to query the map for supercluster v0.3.0 properties, add api for getLeaves, getChildren and getNextExpansionZoom [#13631](https://github.com/mapbox/mapbox-gl-native/pull/13631) + - Restore correct Snapshot attribution, make attribution parser backwards compatible [#13732](https://github.com/mapbox/mapbox-gl-native/pull/13732) + - Vendorize Android Mapbox dependencies [#13712](https://github.com/mapbox/mapbox-gl-native/pull/13712) + - Introduce NativeMap abstraction, remove static code block removal utility [#13681](https://github.com/mapbox/mapbox-gl-native/pull/13681) + - Notify requests about network reachability in priority order [#13721](https://github.com/mapbox/mapbox-gl-native/pull/13721) + - SymbolBucket, use single map for paint properties data [#13724](https://github.com/mapbox/mapbox-gl-native/pull/13724) + - Avoid NullPointerException when quickly closing activity for attribution click handling [#13723](https://github.com/mapbox/mapbox-gl-native/pull/13723) + - Return constexpr strings from functions to avoid ODR compile failures [#13720](https://github.com/mapbox/mapbox-gl-native/pull/13720) + - Split layer type specific code in mbgl::Programs [#13577](https://github.com/mapbox/mapbox-gl-native/pull/13577) + - Shader program must always match bucket in render symbol layer [#13667](https://github.com/mapbox/mapbox-gl-native/pull/13667) + - Implement Google benchmark runner for Android platform [#13628](https://github.com/mapbox/mapbox-gl-native/pull/13628) + - Avoid leaking attribution dialog when hosting context is stopped [#13673](https://github.com/mapbox/mapbox-gl-native/pull/13673) + - Android test runner for core unit tests [#13617](https://github.com/mapbox/mapbox-gl-native/pull/13617) + - Throw IllegalStateException when when setting an maximum fps while the mapRenderer isn't created yet [#13706](https://github.com/mapbox/mapbox-gl-native/pull/13706) + - Lift Parceable interface to OfflineRegionDefinition [#13688](https://github.com/mapbox/mapbox-gl-native/pull/13688) + - Espresso test rework [#13576](https://github.com/mapbox/mapbox-gl-native/pull/13576) + + +## 7.0.1 - January 11, 2019 + - Experimental setMaximumFps configuration on MapView [#13498](https://github.com/mapbox/mapbox-gl-native/pull/13498) + +## 7.1.0-alpha.1 - January 9, 2019 + - Add a Buck build and BUCK file for Android [#13584](https://github.com/mapbox/mapbox-gl-native/pull/13584) + - Instrumented unit tests, add map tests, refactor generated layer tests [#13443](https://github.com/mapbox/mapbox-gl-native/pull/13443) + - Add setMaximumFps configuration on MapView [#13498](https://github.com/mapbox/mapbox-gl-native/pull/13498) + - Fix deduping rings in quering [#11984](https://github.com/mapbox/mapbox-gl-native/pull/11984) + - Updated script detection for Unicode 11 [#13651](https://github.com/mapbox/mapbox-gl-native/pull/13651) + - Drop layer type and remaining usages [#13624](https://github.com/mapbox/mapbox-gl-native/pull/13624) + - Save debug cache state vs map state [#13638](https://github.com/mapbox/mapbox-gl-native/pull/13638) + - Add "localIdeographs" option to OfflineRegionDefinition [#13607](https://github.com/mapbox/mapbox-gl-native/pull/13607) + - Remove timber in favor of logger [#13625](https://github.com/mapbox/mapbox-gl-native/pull/13625) + +## 7.0.0 - January 7, 2019 + - Update core styles based on the streets-v8 source. This results in breaking changes for any code that references internal layers and sources of the style [#13615](https://github.com/mapbox/mapbox-gl-native/pull/13615) + - Cancel camera transitions only when set CameraMode is not None [#13662](https://github.com/mapbox/mapbox-gl-native/pull/13662) + - Fix location interval workaround initialization [#13670](https://github.com/mapbox/mapbox-gl-native/pull/13670) + +## 7.0.0-beta.2 - December 21, 2018 + - Mapbox services sdk version bump to 4.3.0 [#13610](https://github.com/mapbox/mapbox-gl-native/pull/13610) + - Added Style object parameter mention to LocationComponent methods [#13597](https://github.com/mapbox/mapbox-gl-native/pull/13597) + - Clean up remaining deprecated methods for 7.0.0 [#13613](https://github.com/mapbox/mapbox-gl-native/pull/13613) + - Dismiss the foreground color only when the style is loaded [#13582](https://github.com/mapbox/mapbox-gl-native/pull/13582) + - Location update fastest interval workaround[#13592](https://github.com/mapbox/mapbox-gl-native/pull/13592) + - CameraMode transition animation and result listeners[#13523](https://github.com/mapbox/mapbox-gl-native/pull/13523) + - Validate if weakreference is still valid [#13555](https://github.com/mapbox/mapbox-gl-native/pull/13555) + +## 6.8.1 - December 21, 2018 + - Telemetry bump to 3.5.7 [#13645](https://github.com/mapbox/mapbox-gl-native/pull/13645) + - Revert core styles back to streets-v7 source [#13648](https://github.com/mapbox/mapbox-gl-native/pull/13648) + +## 6.8.0 - December 19, 2018 + - Update core styles based on the streets-v8 source. This results in breaking changes for any code that references internal layers and sources of the style [#13615](https://github.com/mapbox/mapbox-gl-native/pull/13615) + - Fixed compass update time not getting updated [#13569](https://github.com/mapbox/mapbox-gl-native/pull/13569) + - Register compass sensor listener only if necessary [#13573](https://github.com/mapbox/mapbox-gl-native/pull/13573) + - Immediate location animation based on map's projection [#13545](https://github.com/mapbox/mapbox-gl-native/pull/13545) + - Option to disable smooth animation of compass and accuracy values [#13574](https://github.com/mapbox/mapbox-gl-native/pull/13574) + - Add onDidBecomeIdle to Map events [#13513](https://github.com/mapbox/mapbox-gl-native/pull/13513) + - Expose setAccessToken on Mapbox.java [#13397](https://github.com/mapbox/mapbox-gl-native/pull/13397) + - Expose endpoint change option for snapshotter [#13508](https://github.com/mapbox/mapbox-gl-native/pull/13508) + - Use locale sensitive DecimalFormat for color alpha conversion [#13393](https://github.com/mapbox/mapbox-gl-native/pull/13393) + - Enabled RecyclerView integration [#13132](https://github.com/mapbox/mapbox-gl-native/pull/13132) + +## 7.0.0-beta.1 - December 12, 2018 + - Style builder and Style.java [#13484](https://github.com/mapbox/mapbox-gl-native/pull/13484) + - LatLng bounds cannot be wrapped [#13419](https://github.com/mapbox/mapbox-gl-native/pull/13419) + - Expose endpoint change option for snapshotter [#13508](https://github.com/mapbox/mapbox-gl-native/pull/13508) + - Formatted "text-field" property setter [#13358](https://github.com/mapbox/mapbox-gl-native/pull/13358) + - Expose setAccesToken on Mapbox.java [#13397](https://github.com/mapbox/mapbox-gl-native/pull/13397) + - Immediately notify core about forced connectivity state [#13485](https://github.com/mapbox/mapbox-gl-native/pull/13485) + - Handle transition duration of 0ms for symbols [#13535](https://github.com/mapbox/mapbox-gl-native/pull/13535) + - Returning boolean when removing sources and layers [#13428](https://github.com/mapbox/mapbox-gl-native/pull/13428) + - Telemetry v4.1.1 bump [#13499](https://github.com/mapbox/mapbox-gl-native/pull/13499) + - mapbox-java 4.2.0 bump [#13507](https://github.com/mapbox/mapbox-gl-native/pull/13507) + +## 7.0.0-alpha.3 - November 30, 2018 + - Add `fill-extrusion-vertical-gradient` fill paint property [#13463](https://github.com/mapbox/mapbox-gl-native/pull/13463) + - Don't cancel core transitions on MotionEvent#ACTION_CANCEL [#13465](https://github.com/mapbox/mapbox-gl-native/pull/13465) + - Require context in AttributionParser.Options [#13444](https://github.com/mapbox/mapbox-gl-native/pull/13444) + - Deprecated annotations package [#13468](https://github.com/mapbox/mapbox-gl-native/pull/13468) + - Send turnstile event while create MapSnapshotter instance [#13475](https://github.com/mapbox/mapbox-gl-native/pull/13475) + - Remove default zoom level restrictions when activating the location component [#13425](https://github.com/mapbox/mapbox-gl-native/pull/13425) + +## 6.7.2 - November 30, 2018 + - Telemetry v3.5.6 [#13486](https://github.com/mapbox/mapbox-gl-native/pull/13486) + - Add "localIdeographFontFamily" setting to MapSnapshotter to reduce font data usage while snapshotting CJK maps [#13427](https://github.com/mapbox/mapbox-gl-native/pull/13427) + - Send turnstile event while creating MapSnapshotter instance [#13475](https://github.com/mapbox/mapbox-gl-native/pull/13475) + +## 7.0.0-alpha.2 - November 21, 2018 + - Add overlay example with custom drawing using paint and canvas [#13431](https://github.com/mapbox/mapbox-gl-native/pull/13431) + - Use --info argument to show details during test run [#13415](https://github.com/mapbox/mapbox-gl-native/pull/13415) + - Add specific name logging in failure scenarios of PropertyValue [#13409](https://github.com/mapbox/mapbox-gl-native/pull/13409) + - Fixed constant layer filter [#13406](https://github.com/mapbox/mapbox-gl-native/pull/13406) + - Update map feedback URL [#13396](https://github.com/mapbox/mapbox-gl-native/pull/13396) + - Use locale sensitive DecimalFormat for color alpha conversion [#13393](https://github.com/mapbox/mapbox-gl-native/pull/13393) + - Fixed `{prefix}` in tile resource URLs [#13429](https://github.com/mapbox/mapbox-gl-native/pull/13429) + +## 6.7.1 - November 16, 2018 + - Telemetry v3.5.4 [#13330](https://github.com/mapbox/mapbox-gl-native/pull/13330) + +## 7.0.0-alpha.1 - November 14, 2018 + - Added Nullable and NonNull annotations [#13274](https://github.com/mapbox/mapbox-gl-native/pull/13274) + - Catch and re-throw throwables thrown inside map state callbacks [#13351](https://github.com/mapbox/mapbox-gl-native/pull/13351) + - Removed deprecated code and notices [#13244](https://github.com/mapbox/mapbox-gl-native/pull/13244) + - Update to mapbox java services 4.1.0 [#13357](https://github.com/mapbox/mapbox-gl-native/pull/13357) + - Removed deprecated color conversion from PropertyFactory [#13337](https://github.com/mapbox/mapbox-gl-native/pull/13337) + - Removed deprecated OnMapChange interface and implementation [#13240](https://github.com/mapbox/mapbox-gl-native/pull/13240) + - Cleanup isDismissible flag [#13197](https://github.com/mapbox/mapbox-gl-native/pull/13179) + - Removed java 8 language features [#13193](https://github.com/mapbox/mapbox-gl-native/pull/13193) + - Removed ZoomButtonsController support [#13198](https://github.com/mapbox/mapbox-gl-native/pull/13198) + - Changed CustomGeometrySource() signature to be Kotlin friendly [#13178](https://github.com/mapbox/mapbox-gl-native/pull/13178) + - Removed ILatLng and IProjectedMeteres [#13176](https://github.com/mapbox/mapbox-gl-native/pull/13176) + - Option to consume map clicks, consuming location clicks [#13205](https://github.com/mapbox/mapbox-gl-native/pull/13205) + - Consolidated OfflineRegionDefinitions [#13180](https://github.com/mapbox/mapbox-gl-native/pull/13180) + - Added scrollBy() to MapboxMap, depricated CameraUpdate.scrollBy() [#13223](https://github.com/mapbox/mapbox-gl-native/pull/13223) + - Improved CustomGeometrySource constructor typing [#13200](https://github.com/mapbox/mapbox-gl-native/pull/13200) + - Removed deprecated MarkerView[#13194](https://github.com/mapbox/mapbox-gl-native/pull/13194) + - Annotate onMapReady with @NonNull [#13307](https://github.com/mapbox/mapbox-gl-native/pull/13307) + +## 6.6.7 - November 8, 2018 + - Telemetry v3.5.4 [#13330](https://github.com/mapbox/mapbox-gl-native/pull/13330) + +## 6.6.6 - November 8, 2018 + - This release should be skipped in favor of the `v6.6.7`. + +## 6.7.0 - November 7, 2018 + - Handle null getMapAsync invocations, deliver onMapReady only once [#13301](https://github.com/mapbox/mapbox-gl-native/pull/13301) + - Verify if `text-field` is not null before performing compatibility operations [#13294](https://github.com/mapbox/mapbox-gl-native/pull/13294) + - Log Java exception and fail silently instead of throwing a native exception in the map state callbacks[#13294](https://github.com/mapbox/mapbox-gl-native/pull/13294) + - Reintroduce OnMapChange invocation [#13289](https://github.com/mapbox/mapbox-gl-native/pull/13289) + - Null-check nativeMapView in case it's destroyed [#13281](https://github.com/mapbox/mapbox-gl-native/pull/13281) + +## 6.6.5 - November 2, 2018 + - Telemetry v3.5.2 [#13258](https://github.com/mapbox/mapbox-gl-native/pull/13258) + +## 6.6.4 - November 1, 2018 + - Fix crash with non AppCompatActivities [#13222](https://github.com/mapbox/mapbox-gl-native/pull/13222) + - Telemetry bump 3.5.1 [#13204](https://github.com/mapbox/mapbox-gl-native/pull/13204) + - Gestures library 0.3.0 [#13234](https://github.com/mapbox/mapbox-gl-native/pull/13234) + +## 6.7.0-beta.1 - October 31, 2018 + - Remove map initialised check [#13224](https://github.com/mapbox/mapbox-gl-native/pull/13224) + - Upgrade GeoJSONVT to v6.6.2 [#13231](https://github.com/mapbox/mapbox-gl-native/pull/13231) + - Deliver OnMapReady at end of initialisation block [#13220](https://github.com/mapbox/mapbox-gl-native/pull/13220) + - Rework MapCallback to execute in correct order [#13203](https://github.com/mapbox/mapbox-gl-native/pull/13203) + - Deprecate map zoom button controller [#13197](https://github.com/mapbox/mapbox-gl-native/pull/13197) + - RenderState API [#13170](https://github.com/mapbox/mapbox-gl-native/pull/13170) + - Avoid wrapping longitude values at dateline [#13006](https://github.com/mapbox/mapbox-gl-native/pull/13006) + +## 6.7.0-alpha.2 - October 24, 2018 + - Format expression support [#12985](https://github.com/mapbox/mapbox-gl-native/pull/12985) + - Remove view tree observer [#13133](https://github.com/mapbox/mapbox-gl-native/pull/13133) + - Revert infer nullity [#13147](https://github.com/mapbox/mapbox-gl-native/pull/13147) + - Add priorities to requests [#13019](https://github.com/mapbox/mapbox-gl-native/pull/13019) + - Avoid using and [#13135](https://github.com/mapbox/mapbox-gl-native/pull/13135) + - Introduce mgbl::RenderState [#13123](https://github.com/mapbox/mapbox-gl-native/pull/13123) + - Ensure queryRenderedFeatures accounts for icon-rotate [#13105](https://github.com/mapbox/mapbox-gl-native/pull/13105) + - Add 'putResourceForURL' for pre-warming ambient cache [#13119](https://github.com/mapbox/mapbox-gl-native/pull/13119) + +## 6.6.2/6.6.3 - October 24, 2018 + - Remove requirement on vectorDrawables.useSupportLibrary [#13168](https://github.com/mapbox/mapbox-gl-native/pull/13168) + - File source deactivate called before callbacks [#13146](https://github.com/mapbox/mapbox-gl-native/pull/13168) + +## 6.7.0-alpha.1 - October 17, 2018 + - Core format expression integration [#12624](https://github.com/mapbox/mapbox-gl-native/pull/12624) + - Remove obsolete catch block in getLayerAs [#13091](https://github.com/mapbox/mapbox-gl-native/pull/13091) + - Include Mapbox SDK Services proguard configuration [#13103](https://github.com/mapbox/mapbox-gl-native/pull/13103) + - Avoid printing exception for valid code path when loading FileSource [#13102](https://github.com/mapbox/mapbox-gl-native/pull/13102) + - Add equals, hashcode and toString to camera update types [#13089](https://github.com/mapbox/mapbox-gl-native/pull/13089) + - Infer nullity, optimise codebase for consumption by kotlin [#13071](https://github.com/mapbox/mapbox-gl-native/pull/13071) + - Replace OnMapchange with specific callbacks [#13050](https://github.com/mapbox/mapbox-gl-native/pull/13050) + - Offline region creation telemetry event [#12544](https://github.com/mapbox/mapbox-gl-native/pull/12544) + +## 6.6.1 - October 17, 2018 + - Use appcompat resource configuration for vector drawables [#13114](https://github.com/mapbox/mapbox-gl-native/pull/13114) + - Reverse order of prioritising shape vs marker click handling [#13059](https://github.com/mapbox/mapbox-gl-native/pull/13059) + +## 6.6.0 - October 11, 2018 + - Fixed an issue where fill and line layers would occasionally flicker on zoom [#12982](https://github.com/mapbox/mapbox-gl-native/pull/12982) + +## 6.6.0-beta.1 - October 3, 2018 + - Add configuration to change the animation duration multiplier when tracking location [#13011](https://github.com/mapbox/mapbox-gl-native/pull/13011) + - Move camera and puck immediately when starting to track and displacement is big [#13013](https://github.com/mapbox/mapbox-gl-native/pull/13013) + +## 6.6.0-alpha.2 - September 26, 2018 + - Expose offline database merge API [#12860](https://github.com/mapbox/mapbox-gl-native/pull/12860) + - Remove unnecessary camera callbacks loops [#12973](https://github.com/mapbox/mapbox-gl-native/pull/12973) + - Fix an issue where `OnCameraMoveListener#onCameraMove` would not get invoked while using `LocationComponent` [#12932](https://github.com/mapbox/mapbox-gl-native/pull/12932) + - Try not to re-draw tiles when coming back from background [#12844](https://github.com/mapbox/mapbox-gl-native/pull/12844) + - Proguard config revision [#12929](https://github.com/mapbox/mapbox-gl-native/pull/12929) + - Improve compatibility of offline packs between devices [#12933](https://github.com/mapbox/mapbox-gl-native/pull/12933) + - Update telemetry dependency to 3.2.1 [#12974](https://github.com/mapbox/mapbox-gl-native/pull/12974) + - Fix an issue where some tiles weren't rendered correctly when no internet connectivity was available [#12931](https://github.com/mapbox/mapbox-gl-native/pull/12931) + +## 6.6.0-alpha.1 - September 19, 2018 + - Merge LocationLayerPlugin to the Maps SDK [#12786](https://github.com/mapbox/mapbox-gl-native/pull/12786) + - Add `symbol-z-order` symbol layout property to style spec [#12783](https://github.com/mapbox/mapbox-gl-native/pull/12783) + - Add "crossSourceCollisions" map option [#12842](https://github.com/mapbox/mapbox-gl-native/pull/12842) + - "is-supported-script" expression support [#12845](https://github.com/mapbox/mapbox-gl-native/pull/12845) + - Update isEmulator check [#12832](https://github.com/mapbox/mapbox-gl-native/pull/12832) + - Prevent crash when expression evaluates to default, no pattern [#12896](https://github.com/mapbox/mapbox-gl-native/pull/12896) + - Fixed bugs in coercion expression operators ("to-array" applied to empty arrays, "to-color" applied to colors, and "to-number" applied to null) [#12864](https://github.com/mapbox/mapbox-gl-native/pull/12864) + - Update proguard config to not warn about AutoValue [#12920](https://github.com/mapbox/mapbox-gl-native/pull/12920) + - Remove nunicode from android binding [#12497](https://github.com/mapbox/mapbox-gl-native/pull/12497) + - Correct LatLngBounds exception message [#12816](https://github.com/mapbox/mapbox-gl-native/pull/12816) + +## 6.5.0 - September 11, 2018 + - Fixed a cubic-bezier interpolation bug. [#12812](https://github.com/mapbox/mapbox-gl-native/issues/12812) + - Fixed an issue that could cause "allow-overlap" symbols to fade in during pan operations instead of always showing. [#12683](https://github.com/mapbox/mapbox-gl-native/issues/12683) + +## 6.5.0-beta.1 - September 5, 2018 + - Retain shared thread pool reference [#12811](https://github.com/mapbox/mapbox-gl-native/pull/12811) + - MapStrictMode configuration [#12817](https://github.com/mapbox/mapbox-gl-native/pull/12817) + - Provide correct expression integration for newly created properties [#12809](https://github.com/mapbox/mapbox-gl-native/pull/12809) + - Get source id for supported layer types [#12801](https://github.com/mapbox/mapbox-gl-native/pull/12801) + - Check if map is destroyed before saving state in a fragment [#12804](https://github.com/mapbox/mapbox-gl-native/pull/12804) + - Bump leak canary to 1.6.1 [#12767](https://github.com/mapbox/mapbox-gl-native/pull/12767) + - Add quotes to string representation of string literal [#12768](https://github.com/mapbox/mapbox-gl-native/pull/12768) + - Make improve this map translatable [#12785](https://github.com/mapbox/mapbox-gl-native/pull/12785) + - Add Keep annotation to abstract components [#12796](https://github.com/mapbox/mapbox-gl-native/pull/12796) + - Gradle tools 3.1.4 [#12795](https://github.com/mapbox/mapbox-gl-native/pull/12795) + - Explicitly remove local references [#12774](https://github.com/mapbox/mapbox-gl-native/pull/12774) + - Add more languages to tx config file [#12719](https://github.com/mapbox/mapbox-gl-native/pull/12719) + - Add support for feature expressions in `line-pattern`, `fill-pattern`, and `fill-extrusion-pattern` properties. [#12284](https://github.com/mapbox/mapbox-gl-native/pull/12284) + +## 6.5.0-alpha.2 - August 29, 2018 + - Keep arbitrary region fields and constructor of OfflineGeometryRegionDefinition while obfuscating [#12734](https://github.com/mapbox/mapbox-gl-native/pull/12734) + - Modularise the SDK [#12468](https://github.com/mapbox/mapbox-gl-native/pull/12468) + - Invalidate InfoWindow's y offset when content is set [#12715](https://github.com/mapbox/mapbox-gl-native/pull/12715) + - Synchronize and coalesce requests targeting the same TileID in the CustomGeometrySource [#12509](https://github.com/mapbox/mapbox-gl-native/pull/12509) + - Line gradient support [#12575](https://github.com/mapbox/mapbox-gl-native/pull/12575) + +## 6.5.0-alpha.1 - August 22, 2018 + - Don't default-show text/icons that depend on the placement of a paired icon/text [#12483](https://github.com/mapbox/mapbox-gl-native/issues/12483) + - Fix symbol querying for markers near tile boundaries at high zoom. ([#12472](https://github.com/mapbox/mapbox-gl-native/issues/12472)) + - Hide peer constructors [#12639](https://github.com/mapbox/mapbox-gl-native/pull/12639) + - Shutting down thread pool of the CustomGeometrySource when the source is destroyed [#12517](https://github.com/mapbox/mapbox-gl-native/pull/12517) + - Harden offline database [#12224](https://github.com/mapbox/mapbox-gl-native/pull/12224) + - Obtain paths to file directories on a worker thread [#12657](https://github.com/mapbox/mapbox-gl-native/pull/12657) + - Arbitrary offline region shapes [#11447](https://github.com/mapbox/mapbox-gl-native/pull/11447) + - Make AndroidRendererFrontend to request render once per event loop [#12586](https://github.com/mapbox/mapbox-gl-native/issues/12586) + - Converting GeoJsonSource features asynchronously [#12580](https://github.com/mapbox/mapbox-gl-native/pull/12580) + - StyleJson configuration on MapboxMapOptions [#12664](https://github.com/mapbox/mapbox-gl-native/pull/12664) + - Update MapInitializer for OfflineRegionDefinition [#12686](https://github.com/mapbox/mapbox-gl-native/pull/12686) + - Offline symbol render example using AssetFileSource [#12676](https://github.com/mapbox/mapbox-gl-native/pull/12676) + - Added Chinese translations [#12696](https://github.com/mapbox/mapbox-gl-native/pull/12696) + - Collator and resolved-locale expressions [#12688](https://github.com/mapbox/mapbox-gl-native/pull/12688) + - Update telemetry user-agent [#12706](https://github.com/mapbox/mapbox-gl-native/pull/12706) + - Replace hardcoded telemetry settings string with resources [#12707](https://github.com/mapbox/mapbox-gl-native/pull/12707) + - Update GeoJsonOptions javadoc [#12709](https://github.com/mapbox/mapbox-gl-native/pull/12709) + +## 6.4.0 - August 15, 2018 + - Use unconverted bearing value for LatLngBounds calculation [#12616](https://github.com/mapbox/mapbox-gl-native/pull/12616) + - Store release so files with debugging information [#12628](https://github.com/mapbox/mapbox-gl-native/pull/12628) + - Close a security vulnerability introduced in v6.2.0 that would potentially allow the owner of a style to compromise apps loading that style. + +## 6.4.0-beta.1 - August 9, 2018 + - Don't prefetch tiles for geojson sources [#12529](https://github.com/mapbox/mapbox-gl-native/pull/12529) + - Enable LTO in release builds [#12546](https://github.com/mapbox/mapbox-gl-native/pull/12546) + - Update Java Services to v3.4.0 [#12564](https://github.com/mapbox/mapbox-gl-native/pull/12564) + - Telemetry bump to 3.1.5 [#12589](https://github.com/mapbox/mapbox-gl-native/pull/12589) + +## 6.4.0-alpha.2 - August 1, 2018 + - Compress shader source code [#12477](https://github.com/mapbox/mapbox-gl-native/pull/12477) + - Add minimal touch target to marker click detection [#12482](https://github.com/mapbox/mapbox-gl-native/pull/12482) + - Improve resulting APK size by using @Keep annotation [#12490](https://github.com/mapbox/mapbox-gl-native/pull/12490) + - Reuse Bitmap, Paint & Canvas instances in LocalGlyphRasterizer [#12488](https://github.com/mapbox/mapbox-gl-native/pull/12488) + - Remove black surface during map start up [#12377](https://github.com/mapbox/mapbox-gl-native/pull/12377) + - Corrected VisibleRegion on rotated map [#12255](https://github.com/mapbox/mapbox-gl-native/pull/12255) + - Tweak conversions to reduce binary size [#12511](https://github.com/mapbox/mapbox-gl-native/pull/12511) + +## 6.4.0-alpha.1 - July 25, 2018 + - Validate if gesture detector is able to handle gesture [#12375](https://github.com/mapbox/mapbox-gl-native/pull/12375) + - Add a convenience method to check whether the map is in a destroyed state, MapView#isDestroyed [#12373](https://github.com/mapbox/mapbox-gl-native/pull/12373) + - Add GeoJSON regression tests for java method look up from JNI [#12354](https://github.com/mapbox/mapbox-gl-native/pull/12354) + - Bump kotlin, timber and okhttp dependencies [#12393](https://github.com/mapbox/mapbox-gl-native/pull/12393) + - Harden map events creation [#12405](https://github.com/mapbox/mapbox-gl-native/pull/12405) + - Fix possibly race on undefined behavior [#12408](https://github.com/mapbox/mapbox-gl-native/pull/12408) + - Option to request the visible region restricted by the content padding [#12407](https://github.com/mapbox/mapbox-gl-native/pull/12407) + - Expose getMap [#12444](https://github.com/mapbox/mapbox-gl-native/pull/12444) + - Correct access token validation [#12458](https://github.com/mapbox/mapbox-gl-native/pull/12458) + - Port `symbol-placement: line-center` to native [#12337](https://github.com/mapbox/mapbox-gl-native/pull/12337) + - Token string syntax (`"{token}"`) in `SymbolLayer` `textField` and `iconImage` property values is now correctly converted to the appropriate expression equivalent. ([#11659](https://github.com/mapbox/mapbox-gl-native/issues/11659)) + - Fixed a crash when switching between two styles having layers with the same identifier but different layer types. ([#12432](https://github.com/mapbox/mapbox-gl-native/issues/12432)) + +## 6.3.0 - July 18, 2018 + - Harden map events creation [#12406](https://github.com/mapbox/mapbox-gl-native/pull/12406) + +## 6.3.0-beta.1 - July 11, 2018 + - Fix JNI lookup of FeatureCollection, add regression test [#12344](https://github.com/mapbox/mapbox-gl-native/pull/12344) + - Added draggable marker example activity, add kotlin support to test app [@12326](https://github.com/mapbox/mapbox-gl-native/pull/12326) + - Telemetry bump to v3.1.4 [#12349](https://github.com/mapbox/mapbox-gl-native/pull/12349) + - Remove unnecessary memory use when collision debug mode is not enabled ([#12294](https://github.com/mapbox/mapbox-gl-native/issues/12294)) + +## 6.3.0-alpha.2 - July 5, 2018 + - Add LatLngForScreenCoordinate to MapSnapshotter API, This allows to convert a LatLng value to the x,y position on the MapSnasphot image [#12221](https://github.com/mapbox/mapbox-gl-native/pull/12221) + - Expose multiple getCameraFor equivalent methods to convert a geometry or a bounds to a camera position with taking in account padding, tilt and bearing [#12290](https://github.com/mapbox/mapbox-gl-native/pull/12290) + - Avoid race condition when calling getMapAsync from a non-UI thread when running instrumentation tests [#12308](https://github.com/mapbox/mapbox-gl-native/pull/12308) + +## 6.2.1 - June 27, 2018 + - Backport range alpha values from 0 to 1 with int color conversion [#12235](https://github.com/mapbox/mapbox-gl-native/pull/12235) + +## 6.3.0-alpha.1 - June 27, 2018 + - Range alpha values from 0 to 1 with int color conversion [#12210](https://github.com/mapbox/mapbox-gl-native/pull/12210) + - Remove reflection lookup for MarkerViewAdapter intialisation [#12218](https://github.com/mapbox/mapbox-gl-native/pull/12218) + - GeoJSON clustering expressions logic change [#12211](https://github.com/mapbox/mapbox-gl-native/pull/12211) + - Add SDF support, allow template images to be tinted [#12181](https://github.com/mapbox/mapbox-gl-native/pull/12181) + - Add android licenses tool, update licenses [#12153](https://github.com/mapbox/mapbox-gl-native/pull/12153) + +## 6.2.0 - June 20, 2018 + - Remove support for Fabric [#12126](https://github.com/mapbox/mapbox-gl-native/pull/12126) + - Dispatch camera start callback only when actually moving [#12172](https://github.com/mapbox/mapbox-gl-native/pull/12172) + +## 6.2.0-beta.3 - June 15, 2018 + - Always commit symbol placement [#12076](https://github.com/mapbox/mapbox-gl-native/pull/12076) + - Ceil map dimensions [#12116](https://github.com/mapbox/mapbox-gl-native/pull/12116) + - Fix match stop conversion [#12128](https://github.com/mapbox/mapbox-gl-native/pull/12128) + - Telemetry bump to v3.1.3 [#12156](https://github.com/mapbox/mapbox-gl-native/pull/12156) + +## 6.2.0-beta.2 - June 7, 2018 + - Ensure that camera is invalidated before generating telemetry event [#12042](https://github.com/mapbox/mapbox-gl-native/pull/12042) + - Offline download batches [#11284](https://github.com/mapbox/mapbox-gl-native/pull/11284) + - Parse Fragment's xml attributes [#12078](https://github.com/mapbox/mapbox-gl-native/pull/12078) + +## 6.2.0-beta.1 - May 31, 2018 + - Bump mapbox-java to 3.2.0 [#12036](https://github.com/mapbox/mapbox-gl-native/pull/12036) + - Optional camera position for map snapshotter [#12028](https://github.com/mapbox/mapbox-gl-native/pull/12029) + - Expose enableOnUserRequest and disableOnUserRequest on Telemetry [#12024](https://github.com/mapbox/mapbox-gl-native/pull/12024) + - Add style json setter/getter to map snapshotter [#12031](https://github.com/mapbox/mapbox-gl-native/pull/12031) + - Fix literal wrapping in comparison expressions [#12022](https://github.com/mapbox/mapbox-gl-native/pull/12022) + - Null check access token in filesource initializer [#12023](https://github.com/mapbox/mapbox-gl-native/pull/12023) + - Allow literal expression property arguments [#12018](https://github.com/mapbox/mapbox-gl-native/pull/12018) + - Raw expression support [#12007](https://github.com/mapbox/mapbox-gl-native/pull/12007) + +## 6.2.0-alpha.2 - May 25, 2018 + - UI Thread checking [#12000](https://github.com/mapbox/mapbox-gl-native/pull/12000) + - Don't force having an Mapbox access token [#12001](https://github.com/mapbox/mapbox-gl-native/pull/12001) + - Unknown tokens in URLs are now preserved, rather than replaced with an empty string [#11787](https://github.com/mapbox/mapbox-gl-native/issues/11787) + - Update onMapChange Listener javadoc [#11972](https://github.com/mapbox/mapbox-gl-native/pull/11972) + - Set Tile loaded/rendered instead of marking tile as optional [#11985](https://github.com/mapbox/mapbox-gl-native/pull/11985) + - Wrap glGetString in `MBGL_CHECK_ERROR` too [#11106](https://github.com/mapbox/mapbox-gl-native/pull/11106) + - Accept constant expression in non-dds properties [#11960](https://github.com/mapbox/mapbox-gl-native/pull/11960) + - Style JSON configuration in Snapshotter [#11976](https://github.com/mapbox/mapbox-gl-native/pull/11976) + - Remove mips and armeabi as supported ABI, update to NDK 17 [#11458](https://github.com/mapbox/mapbox-gl-native/pull/11458) + - Re-assign ids when lng jumps to avoid flicker [#11938](https://github.com/mapbox/mapbox-gl-native/pull/11938) + - Change modifier MapView#initialize to allow subclassing [#11969](https://github.com/mapbox/mapbox-gl-native/pull/11969) + - Avoid symbol flickering when longitude is wrapped [#11938](https://github.com/mapbox/mapbox-gl-native/pull/11938) + - Fix hang when parsing very large angles for hue in hsl colors [#11968](https://github.com/mapbox/mapbox-gl-native/pull/11968) + - Clamp TileJSON.bounds latitudes to [-90, 90] [#11964](https://github.com/mapbox/mapbox-gl-native/pull/11964) + - Align URL token replacement with GL-JS [#11953](https://github.com/mapbox/mapbox-gl-native/pull/11953) + - Align match behaviour [#11935](https://github.com/mapbox/mapbox-gl-native/pull/11935) + +## 6.1.3 - May 23, 2018 + - Circle querying fixes [#11571](https://github.com/mapbox/mapbox-gl-native/pull/11571) + - Global symbol query [#11742](https://github.com/mapbox/mapbox-gl-native/pull/11742) + - Change MapView#initialize modifier to allow overriding [#11969](https://github.com/mapbox/mapbox-gl-native/pull/11969) + - Update telemetry to 3.1.2 [#11987](https://github.com/mapbox/mapbox-gl-native/pull/11987) + +## 6.1.2 - May 18, 2018 + - Update telemetry to 3.1.1 [#11942](https://github.com/mapbox/mapbox-gl-native/pull/11942) + +## 6.2.0-alpha.1 - May 17, 2018 + - `"to-string"` expression operator converts `null` to empty string rather than to `"null"` [#11904](https://github.com/mapbox/mapbox-gl-native/pull/11904) + - Expose MapView#setOfflineRegion [#1922](https://github.com/mapbox/mapbox-gl-native/pull/11922) + - Add nullability annotations to public API for kotlin language integration [#11925](https://github.com/mapbox/mapbox-gl-native/pull/11925) + - Expose MapView created callbacks on MapFragment and SupportMapFragment [#11934](https://github.com/mapbox/mapbox-gl-native/pull/11934) + - Update mapbox-java to 3.1.0 [#11916](https://github.com/mapbox/mapbox-gl-native/pull/11916) + +## 6.1.1 - May 7, 2018 + - Update telemetry to 3.1.0 [#11855](https://github.com/mapbox/mapbox-gl-native/pull/11855) + +## 6.1.0 - May 4, 2018 + - Unwrap LatLngBounds during JNI conversion [#11807](https://github.com/mapbox/mapbox-gl-native/pull/11807) + - Check if renderer is not destroyed before delivering snapshot [#11800](https://github.com/mapbox/mapbox-gl-native/pull/11800) + - Null-check source before removing [#11789](https://github.com/mapbox/mapbox-gl-native/pull/11789) + - Flutter support: promote pixel-ratio to public API NativeMapView [#11772](https://github.com/mapbox/mapbox-gl-native/pull/11772) + - Unwrap LatLngBounds for the shortest path when requesting camera [#11759](https://github.com/mapbox/mapbox-gl-native/pull/11759) + - Flutter support: integrate view callback abstraction [#11706](https://github.com/mapbox/mapbox-gl-native/pull/11706) + - Match expression doc tweaks [#11691](https://github.com/mapbox/mapbox-gl-native/pull/11691) + - Improve stop javadoc to include interpolate [#11677](https://github.com/mapbox/mapbox-gl-native/pull/11677) + - Reduce per-frame render CPU time [#11811](https://github.com/mapbox/mapbox-gl-native/issues/11811) + - Add Korean localization [#11792](https://github.com/mapbox/mapbox-gl-native/pull/11792) + - Add Danish localization; update Hungarian, Russian, Swedish translations [#11136](https://github.com/mapbox/mapbox-gl-native/pull/11136) + +## 5.5.3 - May 4, 2018 + - Check if renderer is not destroyed before delivering snapshot [#11800](https://github.com/mapbox/mapbox-gl-native/pull/11800) + +## 6.0.1 - April 17, 2018 + - Bump telemetry version to 3.0.2 [#11710](https://github.com/mapbox/mapbox-gl-native/pull/11710) + +## 6.0.0 - April 17, 2018 + - Bump telemetry version to 3.0.1 [#11700](https://github.com/mapbox/mapbox-gl-native/pull/11700) + - Update layer when changing its min/max zoom [#11687](https://github.com/mapbox/mapbox-gl-native/pull/11687) + +## 6.0.0-beta.7 - April 12, 2018 + - Add abs, round, floor, ceil expression operators [#11653](https://github.com/mapbox/mapbox-gl-native/pull/11653) + - LatLngBounds correct center calculation [#11650](https://github.com/mapbox/mapbox-gl-native/pull/11650) + - Bump telemetry to 3.0.0 final [#11658](https://github.com/mapbox/mapbox-gl-native/pull/11658) + - Correctly calculate LatLngBounds [#11647](https://github.com/mapbox/mapbox-gl-native/pull/11647) + - Add convenience step expression [#11641](https://github.com/mapbox/mapbox-gl-native/pull/11641) + - Add javadoc examples for Android [#11540](https://github.com/mapbox/mapbox-gl-native/pull/11540) + - Add paused state to map renderer, don't render snapshots after onPause [#11358](https://github.com/mapbox/mapbox-gl-native/pull/11358) + - Rework internal expression conversion [#11490](https://github.com/mapbox/mapbox-gl-native/pull/11490) + - Fixed gesture event listeners javadoc [#11630](https://github.com/mapbox/mapbox-gl-native/pull/11630) + - Add delete local reference on jni strings [#11608](https://github.com/mapbox/mapbox-gl-native/pull/11608) + - Release local references early [#11599](https://github.com/mapbox/mapbox-gl-native/pull/11599) + - Re-bind uniform locations after re-linking program [#11618](https://github.com/mapbox/mapbox-gl-native/pull/11618) + - Bump mapbox-sdk-services to 3.0.1 [#11593](https://github.com/mapbox/mapbox-gl-native/pull/11593) + +## 5.5.2 - April 10, 2018 + - Correct animation scale point [#11643](https://github.com/mapbox/mapbox-gl-native/pull/11643) + - Re-bind uniform locations after re-linking program [#11583](https://github.com/mapbox/mapbox-gl-native/pull/11583) + - Invalidate camera position before delivering onMapReady [#11585](https://github.com/mapbox/mapbox-gl-native/pull/11585) + - Null java peer callback [#11358](https://github.com/mapbox/mapbox-gl-native/pull/11358) + - Add missing delete local reference [#11608](https://github.com/mapbox/mapbox-gl-native/pull/11608) + - Release local refs [#11599](https://github.com/mapbox/mapbox-gl-native/pull/11599) + +## 6.0.0-beta.6 - April 4, 2018 + - Fix race condition crash for heavily modified annotations [#11551](https://github.com/mapbox/mapbox-gl-native/pull/11551) + - Throw exception when converting PropertyValue with an expression [#11572](https://github.com/mapbox/mapbox-gl-native/pull/11572) + - Invalidate camera position before delivering onMapReady [#11585](https://github.com/mapbox/mapbox-gl-native/pull/11585) + - Rework logical condition convenience expressions [#11555](https://github.com/mapbox/mapbox-gl-native/pull/11555) + - Telemetry library version update to 3.0.0-beta.4 [#11590](https://github.com/mapbox/mapbox-gl-native/pull/11590) + +## 6.0.0-beta.5 - March 27, 2018 + - Avoid flashing on pitched overzoomed tiles [#11488](https://github.com/mapbox/mapbox-gl-native/pull/11488) + - Literal array conversion of primitive arrays [#11500](https://github.com/mapbox/mapbox-gl-native/pull/11500) + - Make default output from step more descriptive [#11501](https://github.com/mapbox/mapbox-gl-native/pull/11501) + - Expose public Telemetry API [#11503](https://github.com/mapbox/mapbox-gl-native/pull/11503) + - Convert Android int colors with to-color expression [#11506](https://github.com/mapbox/mapbox-gl-native/pull/11506) + - Prevent default style reload when string style json was set [#11520](https://github.com/mapbox/mapbox-gl-native/pull/11520) + - String, number and bool Expressions with multiple values [#11512](https://github.com/mapbox/mapbox-gl-native/pull/11512) + - Telemetry library 3.0.0-beta.3 [#11534](https://github.com/mapbox/mapbox-gl-native/pull/11534) + - Gestures library v0.2.0 [#11535](https://github.com/mapbox/mapbox-gl-native/pull/11535) + +## 5.5.1 - March 26, 2018 + - Verify optional access of FileSource deactivation [#11480](https://github.com/mapbox/mapbox-gl-native/pull/11480) + - Prevent default style loading when style json was set [#11519](https://github.com/mapbox/mapbox-gl-native/pull/11519) + - Delete local reference when convering Image.java [#11350](https://github.com/mapbox/mapbox-gl-native/pull/11350) + - Use float for pixel ratio when creating a snapshotter [#11367](https://github.com/mapbox/mapbox-gl-native/pull/11367) + - Validate if width and height aren't 0 when creating a snapshot [#11364](https://github.com/mapbox/mapbox-gl-native/pull/11364) + - Null check body of http request [#11413](https://github.com/mapbox/mapbox-gl-native/pull/11413) + - Clamp TileJSON bounds [#11425](https://github.com/mapbox/mapbox-gl-native/pull/11425) + +## 6.0.0-beta.4 - March 20, 2018 + - Gesture library 0.1.0 stable [#11483](https://github.com/mapbox/mapbox-gl-native/pull/11483) + - Filters with expressions [#11429](https://github.com/mapbox/mapbox-gl-native/pull/11429) + - Telemetry 3.0.0-beta.2 [#11474](https://github.com/mapbox/mapbox-gl-native/pull/11474) + - High level JNI conversion for geojson [#11471](https://github.com/mapbox/mapbox-gl-native/pull/11471) + - Update to MAS 3.0.0-beta.4 [#11468](https://github.com/mapbox/mapbox-gl-native/pull/11468) + - Support for expression literal on arrays [#11457](https://github.com/mapbox/mapbox-gl-native/pull/11457) + - Fix telemetry integration for two finger tap gesture [#11460](https://github.com/mapbox/mapbox-gl-native/pull/11460) + - Revisit proguard configuration [#11434](https://github.com/mapbox/mapbox-gl-native/pull/11434) + - Expressions accessor support [#11352](https://github.com/mapbox/mapbox-gl-native/pull/11352) + - Calculate camera's LatLng for bounds without map padding [#11410](https://github.com/mapbox/mapbox-gl-native/pull/11410) + - Check for null on http body request [#11413](https://github.com/mapbox/mapbox-gl-native/pull/11413) + - Expose more gesture settings [#11407](https://github.com/mapbox/mapbox-gl-native/pull/11407) + - Revert java 8 language support [#11398](https://github.com/mapbox/mapbox-gl-native/pull/11398) + - Update to MAS 3.0.0-beta.3 [#11373](https://github.com/mapbox/mapbox-gl-native/pull/11373) + - Rework match expression to style specification syntax [#11388](https://github.com/mapbox/mapbox-gl-native/pull/11388) + - Update javadoc configuration for Gradle 4.4 [#11384](https://github.com/mapbox/mapbox-gl-native/pull/11384) + - Rework zoomIn and zoomOut to use ValueAnimators [#11382](https://github.com/mapbox/mapbox-gl-native/pull/11382) + - Delete LocalRef when converting Image.java [#11350](https://github.com/mapbox/mapbox-gl-native/pull/11350) + - Use float for pixelratio when creating a snapshotter [#11367](https://github.com/mapbox/mapbox-gl-native/pull/11367) + - Validate width/height when creating a snapshot [#11364](https://github.com/mapbox/mapbox-gl-native/pull/11364) + +## 6.0.0-beta.3 - March 2, 2018 + - Added missing local reference deletes [#11243](https://github.com/mapbox/mapbox-gl-native/pull/11243), [#11272](https://github.com/mapbox/mapbox-gl-native/pull/11272) + - Remove obsolete camera api [#11201](https://github.com/mapbox/mapbox-gl-native/pull/11201) + - Fix UTF-8 encoding, add missing package-info.java files [#11261](https://github.com/mapbox/mapbox-gl-native/pull/11261) + - Rework expression api [#11210](https://github.com/mapbox/mapbox-gl-native/pull/11210) + - LatLngBounds fixes [#11333](https://github.com/mapbox/mapbox-gl-native/pull/11333), [#11307](https://github.com/mapbox/mapbox-gl-native/pull/11307), [#11308](https://github.com/mapbox/mapbox-gl-native/pull/11308), [#11309](https://github.com/mapbox/mapbox-gl-native/pull/11309), [#11226](https://github.com/mapbox/mapbox-gl-native/pull/11226) + - New gestures library [#11221](https://github.com/mapbox/mapbox-gl-native/pull/11221) + - Expose ImageSource coordinates setter [#11262](https://github.com/mapbox/mapbox-gl-native/pull/11262) + - Add heatmap color property [#11220](https://github.com/mapbox/mapbox-gl-native/pull/11220) + - Add support for mapzen terrarium raster-dem encoding [#11339](https://github.com/mapbox/mapbox-gl-native/pull/11339) + +## 5.5.0 - March 1, 2018 + - TileJSON Bounds allows values inclusive of world extents [#11178](https://github.com/mapbox/mapbox-gl-native/pull/11178) + - LatLngBounds returned by VisibleRegion when map is rotated [#11226](https://github.com/mapbox/mapbox-gl-native/pull/11226) + - Custom Layer fixes & black list VAO on mali t720 [#11239](https://github.com/mapbox/mapbox-gl-native/pull/11239) + - Check if Activity isn't finishing before showing dialog [#11244](https://github.com/mapbox/mapbox-gl-native/pull/11244) + - Decouple MapPadding from overlain views [#11258](https://github.com/mapbox/mapbox-gl-native/pull/11258) + - Don't disable zoom button controller zooming with gesture disabled zoom [#11259](https://github.com/mapbox/mapbox-gl-native/pull/11259) + - Expose ImageSource coordinates setter [#11262](https://github.com/mapbox/mapbox-gl-native/pull/11262) + - Add missing DeleteLocalRefs [#11272](https://github.com/mapbox/mapbox-gl-native/pull/11272) + - Continue loading style even if we mutate it [#11294](https://github.com/mapbox/mapbox-gl-native/pull/11294) + - Update telemetry version for OkHttp [#11338](https://github.com/mapbox/mapbox-gl-native/pull/11338) + +## 6.0.0-beta.2 - February 13, 2018 + - Deprecate LocationEngine [#11185](https://github.com/mapbox/mapbox-gl-native/pull/11185) + - Remove LOST from SDK [11186](https://github.com/mapbox/mapbox-gl-native/pull/11186) + - Transparent surface configuration on TextureView [#11065](https://github.com/mapbox/mapbox-gl-native/pull/11065) + - Constrained setLatLng documentation, expose setLatLngZoom method [#11184](https://github.com/mapbox/mapbox-gl-native/pull/11184) + - Integration of new events library [#10999](https://github.com/mapbox/mapbox-gl-native/pull/10999) + - AddImage performance improvement [#11111](https://github.com/mapbox/mapbox-gl-native/pull/11111) + - Migrate MAS to 3.0.0, refactor GeoJson integration [#11149](https://github.com/mapbox/mapbox-gl-native/pull/11149) + - Remove @jar and @aar dependency suffixes [#11161](https://github.com/mapbox/mapbox-gl-native/pull/11161) + +## 5.4.1 - February 9, 2018 + - Don't recreate TextureView surface as part of view resizing, solves OOM crashes [#11148](https://github.com/mapbox/mapbox-gl-native/pull/11148) + - Don't invoke OnLowMemory before map is ready, solves startup crash on low memory devices [#11109](https://github.com/mapbox/mapbox-gl-native/pull/11109) + - Programmatically create GLSurfaceView, solves fragment bug [#11124](https://github.com/mapbox/mapbox-gl-native/pull/11124) + - Proguard config for optional location provider, solves obfuscation warnings [#11127](https://github.com/mapbox/mapbox-gl-native/pull/11127) + - MapView weak reference in global layout listener, solves memory leak [#11128](https://github.com/mapbox/mapbox-gl-native/pull/11128) + +## 5.4.0 - January 30, 2018 + - Blacklist Adreno 2xx GPU for VAO support [#11047](https://github.com/mapbox/mapbox-gl-native/pull/11047) + - Bearing tracking mode GPS_NORTH_FACING [#11095](https://github.com/mapbox/mapbox-gl-native/pull/11095) + - Disable logging for missing location permissions when location is disabled [#11084](https://github.com/mapbox/mapbox-gl-native/pull/11084) + - Create offline handler using the main thread looper [#11021](https://github.com/mapbox/mapbox-gl-native/pull/11021) + +## 6.0.0-beta.1 - January 26, 2018 + - Binding integration for expressions [#10654](https://github.com/mapbox/mapbox-gl-native/pull/10654) + - CustomGeometrySource [#9983](https://github.com/mapbox/mapbox-gl-native/pull/9983) + - HillshadeLayer and RasterDemSource [#11031](https://github.com/mapbox/mapbox-gl-native/pull/11031) + - Revisit marker placement for snapshot [#11029](https://github.com/mapbox/mapbox-gl-native/pull/11029) + - SafeVarargs annotation for expressions [#11027](https://github.com/mapbox/mapbox-gl-native/pull/11027) + - Expression#toString [#11024](https://github.com/mapbox/mapbox-gl-native/pull/11024) + - Rename initRenderSurface to onSurfaceCreated [#11023](https://github.com/mapbox/mapbox-gl-native/pull/11023) + - Expose attribution manager as public API [#10942](https://github.com/mapbox/mapbox-gl-native/pull/10942) + - Replace Mapzen vector source example with Mapillary [#10931](https://github.com/mapbox/mapbox-gl-native/pull/10931) + - Add Hebrew localization [#10967](https://github.com/mapbox/mapbox-gl-native/pull/10967) + - Cleanup gradle configuration files [#10903](https://github.com/mapbox/mapbox-gl-native/pull/10903) + - Send double tap event only once [#10855](https://github.com/mapbox/mapbox-gl-native/pull/10855) + - Parameter validation for LatLngBounds#from [#10831](https://github.com/mapbox/mapbox-gl-native/pull/10831) + - Replace JSON parsing [#10815](https://github.com/mapbox/mapbox-gl-native/pull/10815) + - Orientation change regression test [#10814](https://github.com/mapbox/mapbox-gl-native/pull/10814) + - Max & min LatLng constants [#10780](https://github.com/mapbox/mapbox-gl-native/pull/10780) + - LatLng#wrap return new instance of LatLng [#10769](https://github.com/mapbox/mapbox-gl-native/pull/10769) + - Custom library loader [#10733](https://github.com/mapbox/mapbox-gl-native/pull/10733) + - Inconsistent parameters for LatLngBounds.union [#10728](https://github.com/mapbox/mapbox-gl-native/pull/10728) + - Gradle 4.1 / AS 3.0 [#10549](https://github.com/mapbox/mapbox-gl-native/pull/10549) + +## 5.3.2 - January 22, 2018 + - Validate surface creation before destroying [#10890](https://github.com/mapbox/mapbox-gl-native/pull/10890) + - Add filesource activation ot OfflineRegion [#10904](https://github.com/mapbox/mapbox-gl-native/pull/10904) + - Save configuration of UiSettings [#10908](https://github.com/mapbox/mapbox-gl-native/pull/10908) + - Do not overwrite user-set focal point [#10910](https://github.com/mapbox/mapbox-gl-native/pull/10910) + - Camera callbacks for velocity animated movements [#10925](https://github.com/mapbox/mapbox-gl-native/pull/10925) + - Allow changing the used OkHttpClient [#10948](https://github.com/mapbox/mapbox-gl-native/pull/10948) + - Validate zoom level before creating telemetry event [#10959](https://github.com/mapbox/mapbox-gl-native/pull/10959) + - Handle null call instances in HttpRequest [#10987](https://github.com/mapbox/mapbox-gl-native/pull/10987) + +## 5.3.1 - January 10, 2018 + - Blacklist binary program loading for Vivante GC4000 GPUs [#10862](https://github.com/mapbox/mapbox-gl-native/pull/10862) + - Support Genymotion [#10841](https://github.com/mapbox/mapbox-gl-native/pull/10841) + - Getting bitmap from drawable fix [#10763](https://github.com/mapbox/mapbox-gl-native/pull/10763) + - Use US locale when converting between int color and RGBA string [#10810](https://github.com/mapbox/mapbox-gl-native/pull/10810) + - Harden map destruction [#10811](https://github.com/mapbox/mapbox-gl-native/pull/10811) + - RTL support [#10828](https://github.com/mapbox/mapbox-gl-native/pull/10828) + - Allow configuring Http url logging when a request fails [#10830](https://github.com/mapbox/mapbox-gl-native/pull/10830) + - Don't send double tap event multiple times for telemetry [#10854](https://github.com/mapbox/mapbox-gl-native/pull/10854) + - Fix code generation [#10856](https://github.com/mapbox/mapbox-gl-native/pull/10856) + - Use the correct cancelable callback after posting cancel [#10871](https://github.com/mapbox/mapbox-gl-native/pull/10871) + +## 5.3.0 - December 20, 2017 + - Add support for TinySDF [#10706](https://github.com/mapbox/mapbox-gl-native/pull/10706) + - Save restore MyLocationViewSettings [#10746](https://github.com/mapbox/mapbox-gl-native/pull/10746) + - Post animation callback invocation [#10664](https://github.com/mapbox/mapbox-gl-native/pull/10664) + - Allow configuring Http logging [#10681](https://github.com/mapbox/mapbox-gl-native/pull/10681) + - Fix reverse scale gesture [#10688](https://github.com/mapbox/mapbox-gl-native/pull/10688) + - Update offline region metadata documentation [#10693](https://github.com/mapbox/mapbox-gl-native/pull/10693) + - Post camera listener invocation [#10690](https://github.com/mapbox/mapbox-gl-native/pull/10690) + - Activate filesource for offline region creation [#10718](https://github.com/mapbox/mapbox-gl-native/pull/10718) + - Update Spanish/Vietnamese translations [#10740](https://github.com/mapbox/mapbox-gl-native/pull/10740) + - Update instrumented make target [#10724](https://github.com/mapbox/mapbox-gl-native/pull/10724) + - Remove black flash on start for fragments [#10717](https://github.com/mapbox/mapbox-gl-native/pull/10717) + - CompassView decode crash [#10717](https://github.com/mapbox/mapbox-gl-native/pull/10717) + - Android SDK renaming [#10609](https://github.com/mapbox/mapbox-gl-native/pull/10609) + - Map touch listener based lists [#10749](https://github.com/mapbox/mapbox-gl-native/pull/10749) + +## 5.2.1 - December 6, 2017 + - Close race condition in RunLoop [#10537](https://github.com/mapbox/mapbox-gl-native/pull/10537) + - OkHttp 3.9.1 [#10515](https://github.com/mapbox/mapbox-gl-native/pull/10515) + - Attribution anchor point fix [#10558](https://github.com/mapbox/mapbox-gl-native/pull/10558) + - Pre API 19 VerifyError [#10579](https://github.com/mapbox/mapbox-gl-native/pull/10579) + - Set larger Http request limit [#10567](https://github.com/mapbox/mapbox-gl-native/pull/10567) + - Remove jar generation from maven publish [#10625](https://github.com/mapbox/mapbox-gl-native/pull/10625) + - Enable Map Rendering when paused for multiple window support [#10509](https://github.com/mapbox/mapbox-gl-native/pull/10509) + - Activate FileSource when listing offline regions [#10531](https://github.com/mapbox/mapbox-gl-native/pull/10531) + - Harden MarkerView integration by checking for null bitmap [#10532](https://github.com/mapbox/mapbox-gl-native/pull/10532) + - Use concurrent lists for camera change listeners [#10542](https://github.com/mapbox/mapbox-gl-native/pull/10542) + - Handle destroy activity as part of theme switching [#10589](https://github.com/mapbox/mapbox-gl-native/pull/10589) + - add FileSource activation/deactivation to MapSnapshotter [#10556](https://github.com/mapbox/mapbox-gl-native/pull/10556) + +## 5.2.0 - November 17, 2017 + +- Monkey crashes [#10472](https://github.com/mapbox/mapbox-gl-native/pull/10472) + +## 5.2.0-beta.5 - November 14, 2017 + +- MapSnapshot attribution [#10362](https://github.com/mapbox/mapbox-gl-native/pull/10362) +- Downgrade min sdk to 14 [#10355](https://github.com/mapbox/mapbox-gl-native/pull/10355) +- Harden deselection mechanism for markers [#10403](https://github.com/mapbox/mapbox-gl-native/pull/10403) +- Cherry picks to agua [#10442](https://github.com/mapbox/mapbox-gl-native/pull/10442) +- Rework test activity gen script setup [#10365](https://github.com/mapbox/mapbox-gl-native/pull/10365) +- Fix broken android unit tests, update test make target to SDK [#10387](https://github.com/mapbox/mapbox-gl-native/pull/10387) +- Check for null value when calling mapboxMap.clear [#10388](https://github.com/mapbox/mapbox-gl-native/pull/10388) + +## 5.2.0-beta.4 - November 3, 2017 + +- Revert adding mapbox-android-core dependency (#10354) [#10380](https://github.com/mapbox/mapbox-gl-native/pull/10380) +- Asynchronous TextureView [#10370](https://github.com/mapbox/mapbox-gl-native/pull/10370) +- Workaround OkHttp bug on Android O [10366](https://github.com/mapbox/mapbox-gl-native/pull/10366) +- Revisit logo resize [10553](https://github.com/mapbox/mapbox-gl-native/pull/10353) +- Logo resize for MapSnapshotter [#10312](https://github.com/mapbox/mapbox-gl-native/pull/10312) +- Make location provider optional [#10354](https://github.com/mapbox/mapbox-gl-native/pull/10354) +- Check for positive animation value [#10348](https://github.com/mapbox/mapbox-gl-native/pull/10348) +- Fix IAE of ease/animate [#10338](https://github.com/mapbox/mapbox-gl-native/pull/10338) +- Run full test suite on CI [#10333](https://github.com/mapbox/mapbox-gl-native/pull/10333) +- Make sure camera position gets updated in onFinish() callback after camera.move [#10324](https://github.com/mapbox/mapbox-gl-native/pull/10324) +- throw IAE in animate() and easeCamera() when duration <= 0 [#10321](https://github.com/mapbox/mapbox-gl-native/pull/10321) +- Don't save state if map hasn't been initialised [#10320](https://github.com/mapbox/mapbox-gl-native/pull/10320) +- Make map snapshot optional [#10310](https://github.com/mapbox/mapbox-gl-native/pull/10310) +- Synchronise locationlastions with Transifex [#10309](https://github.com/mapbox/mapbox-gl-native/pull/10309) +- MapboxMap#addImages [#10281](https://github.com/mapbox/mapbox-gl-native/pull/10281) +- Move shape annotation click handling to core [#10267](https://github.com/mapbox/mapbox-gl-native/pull/10267) +- Map snapshotter additions [#10163](https://github.com/mapbox/mapbox-gl-native/pull/10163) +- Add velocity to gestures / port animations to SDK animators [#10202](https://github.com/mapbox/mapbox-gl-native/pull/10202) +- Don't save state if map hasn't been initialised [#10320](https://github.com/mapbox/mapbox-gl-native/pull/10320) +- android.hardware.location.gps feature should not be required [#10347](https://github.com/mapbox/mapbox-gl-native/pull/10347) + +## 5.2.0-beta.3 - October 26, 2017 + +- Reorganize dependencies [#10268](https://github.com/mapbox/mapbox-gl-native/pull/10268) +- Blacklist VAO usage on adreno 3xx [#10291](https://github.com/mapbox/mapbox-gl-native/pull/10291) +- On stop null check [#10259](https://github.com/mapbox/mapbox-gl-native/pull/10259) + +## 5.2.0-beta.2 - October 19, 2017 + +- Wire up MapZoomButtonController with camera change events [#10221](https://github.com/mapbox/mapbox-gl-native/pull/10221) +- Execute callbacks only when not idle [#10220](https://github.com/mapbox/mapbox-gl-native/pull/10220) +- Cleanup unused gradle plugins [#10211](https://github.com/mapbox/mapbox-gl-native/pull/10211) +- add FileSource pause/resume [#9977](https://github.com/mapbox/mapbox-gl-native/pull/9977) +- add make target for ndk-stack [#10185](https://github.com/mapbox/mapbox-gl-native/pull/10185) +- Add interpolator examples [#10067](https://github.com/mapbox/mapbox-gl-native/pull/10067) +- Add an UnsatisfiedLinkError safeguard [#10180](https://github.com/mapbox/mapbox-gl-native/pull/10180) +- Hold off handling hover events untill map has been created [#10142](https://github.com/mapbox/mapbox-gl-native/pull/10142) +- Added `MapboxMap.getCameraForGeometry()` to get a camera with zoom level and center coordinate computed to fit a shape [#10107](https://github.com/mapbox/mapbox-gl-native/pull/10107) +- Fine tune gesture zoom & rotation [#10134](https://github.com/mapbox/mapbox-gl-native/pull/10134) + +## 5.2.0-beta.1 - October 6, 2017 + +- Allow multiple listeners for camera events, deprecate old API [#10141](https://github.com/mapbox/mapbox-gl-native/pull/10141) +- Update symbol layer example with location [#10092](https://github.com/mapbox/mapbox-gl-native/pull/10092) +- Make OfflineTilePyramidRegionDefinition parceable [#10080](https://github.com/mapbox/mapbox-gl-native/pull/10080) +- Fix 5.2.0-SNAPSHOT CI build failing [#10079](https://github.com/mapbox/mapbox-gl-native/pull/10079) +- Deprecate MarkerView [#9782](https://github.com/mapbox/mapbox-gl-native/pull/9782) +- Hide overlain views on initalisation [#10068](https://github.com/mapbox/mapbox-gl-native/pull/10068) +- API for platform side animations [#10001](https://github.com/mapbox/mapbox-gl-native/pull/10001) +- Android asynchronous rendering [#9576](https://github.com/mapbox/mapbox-gl-native/pull/9576) +- Set error handler when starting snapshotter [#10035](https://github.com/mapbox/mapbox-gl-native/pull/10035) +- Hook camera events into compass [#10019](https://github.com/mapbox/mapbox-gl-native/pull/10019) +- Testapp cleanup [#10006](https://github.com/mapbox/mapbox-gl-native/pull/10006) +- Update zoom function example with selected state [#9987](https://github.com/mapbox/mapbox-gl-native/pull/9987) +- Add style inspection to debug activity [#9773](https://github.com/mapbox/mapbox-gl-native/pull/9773) +- Bump external dependencies [#9972](https://github.com/mapbox/mapbox-gl-native/pull/9972) +- Don't recycle bitmap for icon reuse. [#9966](https://github.com/mapbox/mapbox-gl-native/pull/9966) +- Android snapshotter [#9748](https://github.com/mapbox/mapbox-gl-native/pull/9748) +- Revert #9764 [#9851](https://github.com/mapbox/mapbox-gl-native/pull/9851) +- Update docs replacing Bitrise mentions with CircleCI [#9515](https://github.com/mapbox/mapbox-gl-native/pull/9515) +- Style image accessor [#9763](https://github.com/mapbox/mapbox-gl-native/pull/9763) +- Update readme with checkstyle and ndk-stack [#9788](https://github.com/mapbox/mapbox-gl-native/pull/9788) +- make android-check [#9787](https://github.com/mapbox/mapbox-gl-native/pull/9787) +- Deprecate MyLocationView in favor of LocationLayer plugin [#9771](https://github.com/mapbox/mapbox-gl-native/pull/9771) +- Increase firebase timeout for CI testing [#9774](https://github.com/mapbox/mapbox-gl-native/pull/9774) +- Restore max zoom to 25.5 [#9765](https://github.com/mapbox/mapbox-gl-native/pull/9765) +- Update example of camera zoom function on a symbol layer. [#9743](https://github.com/mapbox/mapbox-gl-native/pull/9743) +- Optimise icon management [#9643](https://github.com/mapbox/mapbox-gl-native/pull/9643) +- Expose setStyleJson and getStyleJson [#9714](https://github.com/mapbox/mapbox-gl-native/pull/9714) +- update LatLngBounds activity with BottomSheet interaction [#9736](https://github.com/mapbox/mapbox-gl-native/pull/9736) +- post updating InfoWindow update for InfoWindowAdapter [#9716](https://github.com/mapbox/mapbox-gl-native/pull/9716) +- Annotate MapboxMap class with UiThread [#9712](https://github.com/mapbox/mapbox-gl-native/pull/9712) +- Move ZoomButtonController creation to view initalisation [#9587](https://github.com/mapbox/mapbox-gl-native/pull/9587) +- Solve lint issues, reduce baseline [#9627](https://github.com/mapbox/mapbox-gl-native/pull/9627) +- Remove wear module from project [#9618](https://github.com/mapbox/mapbox-gl-native/pull/9618) +- Add zMediaOverlay configuration + bottom sheet integration [#9592](https://github.com/mapbox/mapbox-gl-native/pull/9592) +- Forward getMapAsync to map for fragment [#9621](https://github.com/mapbox/mapbox-gl-native/pull/9621) +- Make target for dumping system gfx information [#9616](https://github.com/mapbox/mapbox-gl-native/pull/9616) +- Make target documentation [#9617](https://github.com/mapbox/mapbox-gl-native/pull/9617) +- onGlobalLayout hook for map creation [#9607](https://github.com/mapbox/mapbox-gl-native/pull/9607) +- Custom viewpager for horizontal swiping [#9601](https://github.com/mapbox/mapbox-gl-native/pull/9601) +- Disable program caching on Adreno 3xx, 4xx, and 5xx GPUs due to known bugs [#9574](https://github.com/mapbox/mapbox-gl-native/pull/9574) +- Avoid creating InfoWindow iterator if no InfoWindows are shown [#9477](https://github.com/mapbox/mapbox-gl-native/pull/9477) +- Rewire map initialisation [#9462](https://github.com/mapbox/mapbox-gl-native/pull/9462) +- Trying to update non-existent polyline fix [#9544](https://github.com/mapbox/mapbox-gl-native/pull/9544) +- Location accuracy threshold [#9472](https://github.com/mapbox/mapbox-gl-native/pull/9472) +- Rewire gesture handling and telemetry event push [#9494](https://github.com/mapbox/mapbox-gl-native/pull/9494) +- run style instrumentation tests on CI [#9353](https://github.com/mapbox/mapbox-gl-native/pull/9353) +- Fix javadoc comment for public setOfflineMapboxTileCountLimit method [#9454](https://github.com/mapbox/mapbox-gl-native/pull/9454) +- add Map change & visibility test activities [#9425](https://github.com/mapbox/mapbox-gl-native/pull/9425) +- build release package once during ci build [#9351](https://github.com/mapbox/mapbox-gl-native/pull/9351) +- Add support for ImageSource [#9110](https://github.com/mapbox/mapbox-gl-native/pull/9110) +- Increased the default maximum zoom level from 20 to 22. [#9835](https://github.com/mapbox/mapbox-gl-native/pull/9835) + +## 5.1.5 - October 31, 2017 + +* Remove obsolete terminate context/display calls [#10162](https://github.com/mapbox/mapbox-gl-native/pull/10162) +* Determine need for clip ID based on actual layers/tiles [#10216](https://github.com/mapbox/mapbox-gl-native/pull/10216) +* Correctly alter sprite URLs [#10217](https://github.com/mapbox/mapbox-gl-native/pull/10217) +* Russian and Ukrainian localizations [#9945](https://github.com/mapbox/mapbox-gl-native/pull/9945) + +## 5.1.4 - September 25, 2017 + +* Update translations [#10033](https://github.com/mapbox/mapbox-gl-native/pull/10033) & [#9945](https://github.com/mapbox/mapbox-gl-native/pull/9945) +* Continue rendering tiles despite erros [#10012](https://github.com/mapbox/mapbox-gl-native/pull/10012) +* Fix layer z-fighting [#9942](https://github.com/mapbox/mapbox-gl-native/pull/9942) +* Align line vertex to 4-byte boundary [#9943](https://github.com/mapbox/mapbox-gl-native/pull/9943) +* Bump proguard config for OkHttp [#9970](https://github.com/mapbox/mapbox-gl-native/pull/9970) +* Remove database on schema downgrade [#9837](https://github.com/mapbox/mapbox-gl-native/pull/9837) +* Disable rotation gesture when pinch zooming [#10026](https://github.com/mapbox/mapbox-gl-native/pull/10026) +* Do not check if connection is local request [#9968](https://github.com/mapbox/mapbox-gl-native/pull/9968) +* Harden offline region deletion [#9967](https://github.com/mapbox/mapbox-gl-native/pull/9967) +* Clear out mapCallback's OnMapReadyListeners on destroy [#9957](https://github.com/mapbox/mapbox-gl-native/pull/9957) +* Avoid adding duplicate points to bounds [#9955](https://github.com/mapbox/mapbox-gl-native/pull/9955) +* Download is complete fix [#9913](https://github.com/mapbox/mapbox-gl-native/pull/9913) +* MAS 2.2.3 [#9901](https://github.com/mapbox/mapbox-gl-native/pull/9901) +* Russian and Ukrainian localizations [#9945](https://github.com/mapbox/mapbox-gl-native/pull/9945) + +## 5.1.3 - August 18, 2017 + +* Use separate attribute component for line normals [#9753](https://github.com/mapbox/mapbox-gl-native/pull/9753) +* Track state of initial overlain views margins [#9391](https://github.com/mapbox/mapbox-gl-native/pull/9391) +* Compability for Samsung devices forcing 3-4 array vector length [#9746](https://github.com/mapbox/mapbox-gl-native/pull/9746) +* Smallest LatLngBounds when visible region crosses dateline [#9747](https://github.com/mapbox/mapbox-gl-native/pull/9747) +* Readd ProjectMeters [#9766](https://github.com/mapbox/mapbox-gl-native/pull/9766) +* Enable location change animation of MyLocationView by default [#9779](https://github.com/mapbox/mapbox-gl-native/pull/9779) +* Avoid IndexOutOfBoundsException when destroying map object [#9789](https://github.com/mapbox/mapbox-gl-native/pull/9789) +* MAS 2.2.1 [#9796](https://github.com/mapbox/mapbox-gl-native/pull/9796) + +## 5.1.2 - August 2, 2017 + +* Disable program caching on Ardreno GPU 3xx, 4xx and 5xx [#9625](https://github.com/mapbox/mapbox-gl-native/pull/9625) +* GeoJSON proguard issue [#9577](https://github.com/mapbox/mapbox-gl-native/pull/9577) +* Harden click events of shape annotations [#9585](https://github.com/mapbox/mapbox-gl-native/pull/9585) +* Validate Marker before opening InfoWindow [#9586](https://github.com/mapbox/mapbox-gl-native/pull/9586) +* Fix added to map check [#9602](https://github.com/mapbox/mapbox-gl-native/pull/9602) +* Don't query shape annotations if none were added [#9606](https://github.com/mapbox/mapbox-gl-native/pull/9606) +* Fix compass direction [#9632](https://github.com/mapbox/mapbox-gl-native/pull/9632) +* Remove preview image integration [#9657](https://github.com/mapbox/mapbox-gl-native/pull/9657) +* Try/catch initialization of telemetry [#9658](https://github.com/mapbox/mapbox-gl-native/pull/9658) +* Fix typo OnCameraMoveStartedListener [#9664](https://github.com/mapbox/mapbox-gl-native/pull/9664) +* Bump MAS dependency to 5.2.0 [#9671](https://github.com/mapbox/mapbox-gl-native/pull/9671) + +## 5.1.1 - July 21, 2017 + +* Rework attribution binding [#9433](https://github.com/mapbox/mapbox-gl-native/pull/9433) +* BackendScope changes [#9538](https://github.com/mapbox/mapbox-gl-native/pull/9538) +* Invisible MarkerView performance fix [#9420](https://github.com/mapbox/mapbox-gl-native/pull/9420) +* Polyline/Polygon click listeners [#9443](https://github.com/mapbox/mapbox-gl-native/pull/9443) +* Hit test Marker/MarkerViews [#9424](https://github.com/mapbox/mapbox-gl-native/pull/9424) +* Fine tune fling gesture [#9532](https://github.com/mapbox/mapbox-gl-native/pull/9532) +* Bump OkHttp 3.8.0, Android 7.x crashes [#9522](https://github.com/mapbox/mapbox-gl-native/pull/9522) +* MyLocationView tint fix [#9410](https://github.com/mapbox/mapbox-gl-native/pull/9410) +* VisibleRegion logic [#9428](https://github.com/mapbox/mapbox-gl-native/pull/9428) +* Validate motion event [#9434](https://github.com/mapbox/mapbox-gl-native/pull/9434) +* Add French localization [#9545](https://github.com/mapbox/mapbox-gl-native/pull/9545) +* Fix public.xml [#9525](https://github.com/mapbox/mapbox-gl-native/pull/9525/files) +* Manage InfoWindow selection in AnnotationManager [#9567](https://github.com/mapbox/mapbox-gl-native/pull/9567) +* Increase touch target size [#9565](https://github.com/mapbox/mapbox-gl-native/pull/9565) +* Work around Adreno 2xx GPU bugs [#9573](https://github.com/mapbox/mapbox-gl-native/pull/9573) + +## 5.1.0 - June 30, 2017 + +* Update to MAS 2.1.3 [#9402](https://github.com/mapbox/mapbox-gl-native/pull/9402) +* Downgrade LOST to v1.1.1 [#9394](https://github.com/mapbox/mapbox-gl-native/pull/9394) +* OnCameraIdle hook into quickzoom gesture [#9339](https://github.com/mapbox/mapbox-gl-native/pull/9339) +* LatLngBounds conversion regression, add test [#9324](https://github.com/mapbox/mapbox-gl-native/pull/9324) + +## 5.1.0-beta.5 - June 21, 2017 + +* Update MAS dependency to 2.1.2 [#9311](https://github.com/mapbox/mapbox-gl-native/pull/9311) +* Update LOST dependency to 3.0.1 [#9302](https://github.com/mapbox/mapbox-gl-native/pull/9302) +* Fix Pulse example Parcelable creator [#9283](https://github.com/mapbox/mapbox-gl-native/pull/9283) +* Custom marker view anchoring [#9282](https://github.com/mapbox/mapbox-gl-native/pull/9282) +* Update Activity test generation [#9276](https://github.com/mapbox/mapbox-gl-native/pull/9276) +* Validate camera position before transforming [#9275](https://github.com/mapbox/mapbox-gl-native/pull/9275) +* Revisit javadoc [#9266](https://github.com/mapbox/mapbox-gl-native/pull/9266) +* Build with NDK 15 [#9263](https://github.com/mapbox/mapbox-gl-native/pull/9263) +* Snapshot with view content [#9263](https://github.com/mapbox/mapbox-gl-native/pull/9263) +* Update source changed javadoc [#9243](https://github.com/mapbox/mapbox-gl-native/pull/9243) +* Run tests on UI-thread [#9198](https://github.com/mapbox/mapbox-gl-native/pull/9198) +* Fix trackball on worker thread [#9305](https://github.com/mapbox/mapbox-gl-native/pull/9305) + +## 5.1.0-beta.4 - June 9, 2017 + +* Option to disable location change animation [#9210](https://github.com/mapbox/mapbox-gl-native/pull/9210) +* Invalidating MyLocationView bearing when not following [#9212](https://github.com/mapbox/mapbox-gl-native/pull/9212) +* Remove upgrade runtime exceptions [#9191](https://github.com/mapbox/mapbox-gl-native/pull/9191) +* Check source usage before removal [#9129](https://github.com/mapbox/mapbox-gl-native/pull/9129) +* Fix tracking mode + camera race condition [#9133](https://github.com/mapbox/mapbox-gl-native/pull/9133) +* Harden orientation changes [#9128](https://github.com/mapbox/mapbox-gl-native/pull/9128) +* Infinite location animation updates [#9194](https://github.com/mapbox/mapbox-gl-native/pull/9194) +* Invoke callback with valid fling gestures [#9192](https://github.com/mapbox/mapbox-gl-native/pull/9192) +* Keep location tracking after screen rotation [#9187](https://github.com/mapbox/mapbox-gl-native/pull/9187) +* Update components with camera values when animating [#9174](https://github.com/mapbox/mapbox-gl-native/pull/9174) +* Validate if gestures should execute [#9173](https://github.com/mapbox/mapbox-gl-native/pull/9173) +* Custom location source and LOST integration [#9142](https://github.com/mapbox/mapbox-gl-native/pull/9142) + +## 5.1.0-beta.3 - May 26, 2017 + +* Add binding support for Light [#9013](https://github.com/mapbox/mapbox-gl-native/pull/9013) +* Update attribution with new Mapbox wordmark [#8774](https://github.com/mapbox/mapbox-gl-native/pull/8774) +* LatLngBounds bearing default value [#9102](https://github.com/mapbox/mapbox-gl-native/pull/9102) +* Stop location updates when toggling MyLocationView [#9099](https://github.com/mapbox/mapbox-gl-native/pull/9099) +* Horizontally rotated in snapshot [#9083](https://github.com/mapbox/mapbox-gl-native/pull/9083) +* Disable letter spacing for Arabic text [#9071](https://github.com/mapbox/mapbox-gl-native/pull/9071) +* Correct bearing conversion when animating the map [#9050](https://github.com/mapbox/mapbox-gl-native/pull/9050) +* Don't leak selected markers when removing [#9047](https://github.com/mapbox/mapbox-gl-native/pull/9047) +* Bump tools and support lib version [#9046](https://github.com/mapbox/mapbox-gl-native/pull/9046) +* MarkerView deselect events with OnMarkerViewClickListener [#9047](https://github.com/mapbox/mapbox-gl-native/pull/9047) +* LOST update to v3.0.0 [#9112](https://github.com/mapbox/mapbox-gl-native/pull/9112) +* Convert dp to pixels for meters per pixel at latitude [#9048](https://github.com/mapbox/mapbox-gl-native/pull/9048) + +## 5.1.0-beta.2 - May 12, 2017 + +5.1.0-beta.2 builds further on 5.1.0-beta.1 and adds: + +* When a layer is added, reload its source's tiles [#8963](https://github.com/mapbox/mapbox-gl-native/pull/8963) +* Update release script to support CircleCI builds [#8950](https://github.com/mapbox/mapbox-gl-native/pull/8950) +* URL getter on Sources [#8959](https://github.com/mapbox/mapbox-gl-native/pull/8959) +* Build SNAPSHOT from release branch CI configuration [#8958](https://github.com/mapbox/mapbox-gl-native/pull/8958) +* Fix UI test filter in Makefile [#8960](https://github.com/mapbox/mapbox-gl-native/pull/8960) +* Bump & fixup dependencies [#8921](https://github.com/mapbox/mapbox-gl-native/pull/8921) +* Ignore already deleted region [#8920](https://github.com/mapbox/mapbox-gl-native/pull/8920) +* Keep offline observer when timeout occurs [#8919](https://github.com/mapbox/mapbox-gl-native/pull/8919) +* Show error message when no browser installed [#8920](https://github.com/mapbox/mapbox-gl-native/pull/8920) +* Reset observers of removed Sources and Layers [#8900](https://github.com/mapbox/mapbox-gl-native/pull/8900) +* Only build custom layer .so for debug builds [#8885](https://github.com/mapbox/mapbox-gl-native/pull/8885) +* Update localizations [#8883](https://github.com/mapbox/mapbox-gl-native/pull/8883) +* Reset observers of removed Sources and Layers [#8862](https://github.com/mapbox/mapbox-gl-native/pull/8862) +* Remove force style cascade [#8866](https://github.com/mapbox/mapbox-gl-native/pull/8866) +* Update proguard config [#8944](https://github.com/mapbox/mapbox-gl-native/pull/8944) +* Update LOST to 2.3.0-SNAPSHOT [#8872](https://github.com/mapbox/mapbox-gl-native/pull/8872) +* Update logo [#8774](https://github.com/mapbox/mapbox-gl-native/pull/8774) +* Camera change listener v2.0 [#8644](https://github.com/mapbox/mapbox-gl-native/pull/8644) +* Allow filesource url transform reset [#8957](https://github.com/mapbox/mapbox-gl-native/pull/8957) + +## 5.1.0-beta.1 - May 2, 2017 + +5.1.0-beta.1 builds further on 5.0.2 and adds: + +* Support for FillExtrusionLayer [#8431](https://github.com/mapbox/mapbox-gl-native/pull/8431) +* Limit Viewport [#8622](https://github.com/mapbox/mapbox-gl-native/pull/8622) +* Transition Properties for Layer attributes [#8509](https://github.com/mapbox/mapbox-gl-native/pull/8509) +* Style wide transition duration and transition offset in milliseconds [#8576](https://github.com/mapbox/mapbox-gl-native/pull/8576) +* Transifex integration, Catalan & Dutch translations [#8556](https://github.com/mapbox/mapbox-gl-native/pull/8556) +* LatLngBounds includes with another bounds [#8517](https://github.com/mapbox/mapbox-gl-native/pull/8517) +* LatLngBounds includes takes in account LatLng on the edges (cfr. core) [#8517](https://github.com/mapbox/mapbox-gl-native/pull/8517) +* LatLngBounds facility getters/setters for LatLnbg on the edges of the bounds [#8517](https://github.com/mapbox/mapbox-gl-native/pull/8517) +* Expose world bounds in LatLngBounds [#8517](https://github.com/mapbox/mapbox-gl-native/pull/8517) +* OfflineRegion are validated if the bounds is found in the world bounds, else onError will be invoked [#8517](https://github.com/mapbox/mapbox-gl-native/pull/8517) +* Polygon holes [#8557](https://github.com/mapbox/mapbox-gl-native/pull/8557) and [#8722](https://github.com/mapbox/mapbox-gl-native/pull/8722) +* Custom location source [#8710](https://github.com/mapbox/mapbox-gl-native/pull/8710) +* Expose source layer identifier [#8709](https://github.com/mapbox/mapbox-gl-native/pull/8709) +* Ensure surface is created after display and context [#8759](https://github.com/mapbox/mapbox-gl-native/pull/8759) +* Harden telemetry event dispatch [#8767](https://github.com/mapbox/mapbox-gl-native/pull/8767) +* Move LatLngBounds calculation for CameraUpdateFactory to core [#8765](https://github.com/mapbox/mapbox-gl-native/pull/8765) +* Spanish, Lithuanian, and Vietnamese localizations [#8852](https://github.com/mapbox/mapbox-gl-native/pull/8852) +* Warning when updating a non-added annotation [#8832](https://github.com/mapbox/mapbox-gl-native/pull/8832) +* Share location source between components [#8825](https://github.com/mapbox/mapbox-gl-native/pull/8825) +* Test application runtime permissions for pre lollipop devices [#8823](https://github.com/mapbox/mapbox-gl-native/pull/8823) +* Do not call OnMarkerClick listener twice [#8804](https://github.com/mapbox/mapbox-gl-native/pull/8804) +* Rework instrumentation tests [#8793](https://github.com/mapbox/mapbox-gl-native/pull/8793) +* Set first render flag to false when destroying the surface [#8739](https://github.com/mapbox/mapbox-gl-native/pull/8739) +* Post resetting tracking settings to avoid race condition reset [#8738](https://github.com/mapbox/mapbox-gl-native/pull/8738) +* Expose Source Layer identifier [#8709](https://github.com/mapbox/mapbox-gl-native/pull/8709) +* Derived source attribution [#8630](https://github.com/mapbox/mapbox-gl-native/pull/8630) +* Consistent use of duration unit [#8578](https://github.com/mapbox/mapbox-gl-native/pull/8578) +* Swedish localization [#8883](https://github.com/mapbox/mapbox-gl-native/pull/8883) +* Streets v10, Outdoors v10, Satellite Streets v10, Traffic Day v2, Traffic Night v2 [#6301](https://github.com/mapbox/mapbox-gl-native/pull/6301) + +## 5.0.2 - April 3, 2017 + +5.0.2 is a patch release that contains the following changes: + +* Binary shader caching [#8604](https://github.com/mapbox/mapbox-gl-native/pull/8604) +* Fix resource transform callback [#8582](https://github.com/mapbox/mapbox-gl-native/pull/8582) +* Restore onTouch behaviour to 4.x version [#8585](https://github.com/mapbox/mapbox-gl-native/pull/8585) +* Restore anchoring after updating MarkerView Icon [#8519](https://github.com/mapbox/mapbox-gl-native/pull/8519) + +## 5.0.1 - March 22nd, 2017 + +5.0.1 is a patch release that addresses a shader precision issue that created a rendering problem on some devices. + +* Use `highp` for color attributes [#8385](https://github.com/mapbox/mapbox-gl-native/issues/8385) + +## 5.0.0 - March 17th, 2017 + +5.0.0 final release contains: + +* Support for data-driven styles [#7752](https://github.com/mapbox/mapbox-gl-native/pull/7752) +* Additional APIs to manage runtime styling layers [#8195](https://github.com/mapbox/mapbox-gl-native/pull/8195) +* Allow query all features in sources (`querySourceFeatures`) [#5792](https://github.com/mapbox/mapbox-gl-native/issues/5792) +* Improve accessibility TalkBack support by including content description in views [#8230](https://github.com/mapbox/mapbox-gl-native/pull/8230) +* Consistent double tap zoom acceleration [#7514](https://github.com/mapbox/mapbox-gl-native/issues/7514) +* Allow zooming/scaling to use `AnimationOptions` [#8181](https://github.com/mapbox/mapbox-gl-native/pull/8181) +* Cleanup inconsistencies `float` vs `double` [#4445](https://github.com/mapbox/mapbox-gl-native/issues/4445) +* Add `mapbox_` prefix to attributes [#6482](https://github.com/mapbox/mapbox-gl-native/issues/6482) +* Update LOST to 2.x [#6573](https://github.com/mapbox/mapbox-gl-native/issues/6537) +* MAS submodules (`geojson`, `telemetry`) are now smaller to reduce the overall method count [#7642](https://github.com/mapbox/mapbox-gl-native/pull/7642) +* Telemetry Service is now include automatically via Manifest merge to simplify set up [#8338](https://github.com/mapbox/mapbox-gl-native/pull/8338) +* Support for Android Nougat [#5910](5910-move-listener-logic-for-nougat) + - Move location listening logic to `onStart`/`onStop` activity lifecylce methods +* Removal of `accessToken` on `MapView` and `MapboxMap` [#5621](https://github.com/mapbox/mapbox-gl-native/issues/5621) +* Introduction of new make targets [#5940](https://github.com/mapbox/mapbox-gl-native/issues/5940) + - Targets for testing, running and using external tools directly from the command line +* Cleanup Gradle files [#6009](https://github.com/mapbox/mapbox-gl-native/issues/6009) + - Introducing single purpose `.gradle` files +* Checkstyle integration [#7442](https://github.com/mapbox/mapbox-gl-native/pull/7442) +* Transform refactor [#6532](https://github.com/mapbox/mapbox-gl-native/issues/6532) + - All camera related code is moved to a dedicated transform class cfr. core architecture +* Encapsulate gestures [#6557](https://github.com/mapbox/mapbox-gl-native/issues/6557) + - All gesture event code is encapsulated in `MapGestureDetector` + - All key event code is encapsulated in `MapKeyListener` +* Transparent background for `MyLocationView` [#7116](https://github.com/mapbox/mapbox-gl-native/issues/7116) +* Correctly scale accuracy circle for `MyLocationView` [#8255](https://github.com/mapbox/mapbox-gl-native/pull/8255) +* Update documentation on generate sanity tests [#7147](https://github.com/mapbox/mapbox-gl-native/issues/7147) +* Update documentation on style templates [#7157](https://github.com/mapbox/mapbox-gl-native/issues/7157) +* Fix `NullPointerException` during save instance state [#7157](https://github.com/mapbox/mapbox-gl-native/issues/7157) +* Move management style to core [#7275](https://github.com/mapbox/mapbox-gl-native/issues/7275) +* Fix `OnCameraChange` not called for the final position of animation bug [#7350](https://github.com/mapbox/mapbox-gl-native/issues/7350) +* `MapboxAccountManager` refactor [#6534](https://github.com/mapbox/mapbox-gl-native/issues/6534) + - Consolidating the location where tokens are set + - Rename to reflect the increased responsibilities introduced in prior releases +* AnnotationManager refactor [#6067](https://github.com/mapbox/mapbox-gl-native/issues/6067) + - Extracting all business logic related to annotations into a separate class cfr. to core and the iOS codebase +* Gesture handling improvements: + - Fix missing call to `onFling` when while pinch zooming [#7666](https://github.com/mapbox/mapbox-gl-native/issues/7666) + - Round tap-zoom gestures to nearest integer [#8027](https://github.com/mapbox/mapbox-gl-native/pull/8027) +* Support for style-wide transition animation duration and delay [#6779](https://github.com/mapbox/mapbox-gl-native/issues/6779) +* Allow callback parameter for style setting [#8262](https://github.com/mapbox/mapbox-gl-native/issues/8262) +* Support for all animated camera changes to configure dismissing tracking modes [#7854](https://github.com/mapbox/mapbox-gl-native/issues/7854) +* Avoid disabling location tracking on all gestures [#7878](https://github.com/mapbox/mapbox-gl-native/issues/7878) +* `LatLng` objects produced by the SDK are wrapped by default [#4522](https://github.com/mapbox/mapbox-gl-native/issues/4522) +* Proguard update for new SDK packages [#8437](https://github.com/mapbox/mapbox-gl-native/pull/8437) +* Javadoc cleanup [#8438](https://github.com/mapbox/mapbox-gl-native/pull/8438) +* MarkerViewClickListener and MapClickListener called simultaneously [#8447](https://github.com/mapbox/mapbox-gl-native/pull/8447) +* Fix LOST v2.0.0 integration [#8445](https://github.com/mapbox/mapbox-gl-native/pull/8445) +* Crash with OnMapChangeListener [#8380](https://github.com/mapbox/mapbox-gl-native/pull/8380) +* Feature query crash [#8374](https://github.com/mapbox/mapbox-gl-native/pull/8374) +* flyTo fix [#8302](https://github.com/mapbox/mapbox-gl-native/pull/8302) +* Don't dispatch MarkerView motion event up to MapView [#8447](https://github.com/mapbox/mapbox-gl-native/pull/8447) + +## 5.0.0-beta.3 - March 10th, 2017 + +5.0.0-beta.3 focuses on addressing stability issues and bug fixes. It also includes: + +* Allow query all features in sources (`querySourceFeatures`) [#5792](https://github.com/mapbox/mapbox-gl-native/issues/5792) +* Improve accessibility TalkBack support by including content description in views [#8230](https://github.com/mapbox/mapbox-gl-native/pull/8230) +* Allow zooming/scaling to use `AnimationOptions` [#8181](https://github.com/mapbox/mapbox-gl-native/pull/8181) +* Telemetry Service is now include automatically via Manifest merge to simplify set up [#8338](https://github.com/mapbox/mapbox-gl-native/pull/8338) +* Correctly scale accuracy circle for `MyLocationView` [#8255](https://github.com/mapbox/mapbox-gl-native/pull/8255) +* Round tap-zoom gestures to nearest integer [#8027](https://github.com/mapbox/mapbox-gl-native/pull/8027) +* Allow callback parameter for style setting [#8262](https://github.com/mapbox/mapbox-gl-native/issues/8262) +* Avoid disabling location tracking on all gestures [#7878](https://github.com/mapbox/mapbox-gl-native/issues/7878) + +## 5.0.0-beta.2 - March 1st, 2017 + +5.0.0-beta.2 focuses on addressing stability issues and bug fixes. It also includes: + +* Data-driven styling for `{text,icon}-{color,opacity,halo-color,halo-blur,halo-width}` [#7939](https://github.com/mapbox/mapbox-gl-native/pull/7939) +* Additional APIs to manage runtime styling layers [#8195](https://github.com/mapbox/mapbox-gl-native/pull/8195) + +## 5.0.0-beta.1 - February 14th, 2017 + +* Support for data-driven styles [#7752](https://github.com/mapbox/mapbox-gl-native/pull/7752) +* Consistent double tap zoom acceleration [#7514](https://github.com/mapbox/mapbox-gl-native/issues/7514) +* Cleanup inconsistencies `float` vs `double` [#4445](https://github.com/mapbox/mapbox-gl-native/issues/4445) +* Add `mapbox_` prefix to attributes [#6482](https://github.com/mapbox/mapbox-gl-native/issues/6482) +* Update LOST to 2.0.0 [#6573](https://github.com/mapbox/mapbox-gl-native/issues/6537) +* MAS submodules (geojson, telemetry) are now smaller to reduce the overall method count [#7642](https://github.com/mapbox/mapbox-gl-native/pull/7642) +* Support for Android Nougat [#5910](5910-move-listener-logic-for-nougat) + - Move location listening logic to onStart/onStop Activity lifecylce methods +* Removal of `accessToken` on `MapView` and `MapboxMap` [#5621](https://github.com/mapbox/mapbox-gl-native/issues/5621) +* Introduction of new make targets [#5940](https://github.com/mapbox/mapbox-gl-native/issues/5940) + - Targets for testing, running and using external tools directly from the command line +* Cleanup Gradle files [#6009](https://github.com/mapbox/mapbox-gl-native/issues/6009) + - Introducing single purpose `.gradle` files +* Checkstyle integration [#7442](https://github.com/mapbox/mapbox-gl-native/pull/7442) +* Transform refactor [#6532](https://github.com/mapbox/mapbox-gl-native/issues/6532) + - All camera related code is moved to a dedicated transform class cfr. core architecture +* Encapsulate Gestures [#6557](https://github.com/mapbox/mapbox-gl-native/issues/6557) + - All gesture event code is encapsulated in `MapGestureDetector` + - All key event code is encapsulated in `MapKeyListener` +* Transparent background for `MyLocationView` [#7116](https://github.com/mapbox/mapbox-gl-native/issues/7116) +* Update documentation on generate sanity tests [#7147](https://github.com/mapbox/mapbox-gl-native/issues/7147) +* Update documentation on style templates [#7157](https://github.com/mapbox/mapbox-gl-native/issues/7157) +* `NullPointerException` during save instance state [#7157](https://github.com/mapbox/mapbox-gl-native/issues/7157) +* Move management style to core [#7275](https://github.com/mapbox/mapbox-gl-native/issues/7275) +* `OnCameraChange` not called for the final position of animation bug [#7350](https://github.com/mapbox/mapbox-gl-native/issues/7350) +* `MapboxAccountManager` refactor [#6534](https://github.com/mapbox/mapbox-gl-native/issues/6534) + - Consolidating the location where tokens are set + - Rename to reflect the increased responsibilities introduced in prior releases +* AnnotationManager refactor [#6067](https://github.com/mapbox/mapbox-gl-native/issues/6067) + - Extracting all business logic related to annotations into a separate class cfr. to core and the iOS codebase +* Gesture handling bugs + - Avoid calls to onFling when while pinch zooming [#7666](https://github.com/mapbox/mapbox-gl-native/issues/7666) +* Support for style-wide transition animation duration and delay [#6779](https://github.com/mapbox/mapbox-gl-native/issues/6779) +* Support for all animated camera changes to configure dismissing tracking modes [#7854](https://github.com/mapbox/mapbox-gl-native/issues/7854) +* `LatLng` objects produced by the SDK are wrapped by default for [compatibility](https://developers.google.com/android/reference/com/google/android/gms/maps/model/LatLng) with the Google Maps API on Android [#4522](https://github.com/mapbox/mapbox-gl-native/issues/4522) + +## 4.2.2 - January 27, 2017 + +Mapbox Android 4.2.2 builds further on v4.2.1 and resolves: +* OnFling gesture improvements + - Improve fling by calculating animation time based on velocity [#7676](https://github.com/mapbox/mapbox-gl-native/pull/7676) + - Avoid triggering a fling after a scale gesture [#7675](https://github.com/mapbox/mapbox-gl-native/issues/7675) +* Deselect marker on infowindow click [#7784](https://github.com/mapbox/mapbox-gl-native/issues/7784) +* Update build tools to Bitrise supported one [#7729](https://github.com/mapbox/mapbox-gl-native/issues/7729) +* Removal of redudant log statements in native_map_view.cpp [#7801](https://github.com/mapbox/mapbox-gl-native/issues/7801) +* Invalidate markersviews when updating their position [#7794](https://github.com/mapbox/mapbox-gl-native/issues/7794) +* Avoid clustering unclusterable GeoJSON [#7633](https://github.com/mapbox/mapbox-gl-native/pull/7633) +* Prevent null pointer exceptions when receiving connecitivy change events [#6858](https://github.com/mapbox/mapbox-gl-native/issues/6858) + +## 4.2.1 - December 22, 2016 + +Mapbox Android 4.2.1 is a bugfix release build on top of 4.2.0 and resolves: + +* Gesture handling bugs + - Reverse fling when tilted [#7383](https://github.com/mapbox/mapbox-gl-native/pull/7383) + - Rotation is hard trigger [#7299](https://github.com/mapbox/mapbox-gl-native/pull/7299) +* Null pointer exception on TrackingSettings [#7505](https://github.com/mapbox/mapbox-gl-native/pull/7505) +* Patches to our CI integration [#7363](https://github.com/mapbox/mapbox-gl-native/pull/7363) & [#7385](https://github.com/mapbox/mapbox-gl-native/pull/7385) +* Screen density is now correctly applied for attributes [#7327](https://github.com/mapbox/mapbox-gl-native/pull/7327) +* Usage of LocalBroadCastReceiver [#7511](https://github.com/mapbox/mapbox-gl-native/pull/7511) + +## 4.2.0 - December 14, 2016 + +Mapbox Android 4.2.0 contains all 4.2.0-beta.5 changes and adds: + +* Adds additional documentation to APIs +* Resolved issue with marker views occasionally not showing up until a gesture was performed on map [#7239](https://github.com/mapbox/mapbox-gl-native/pull/7239) +* Added horizontal accuracy to location events [#7237](https://github.com/mapbox/mapbox-gl-native/pull/7237) +* Resolved issue with changing visibility of a layer [#7242](https://github.com/mapbox/mapbox-gl-native/pull/7242) +* Proguard improvement and fixes + +## 4.2.0-beta.5 - October 25, 2016 + +Mapbox Android 4.2.0-beta.5 adds no new features, only bug fixes. + +## 4.2.0-beta.4 - October 25, 2016 + +Mapbox Android 4.2.0-beta.4 contains all 4.2.0-beta.3 changes and adds: + +* Query rendered features by source [#6516](https://github.com/mapbox/mapbox-gl-native/issues/6516) +* Decreased logging for release builds [#6787](https://github.com/mapbox/mapbox-gl-native/pull/6787) +* setConnected method to manually set a connected flag [#6618](https://github.com/mapbox/mapbox-gl-native/pull/6618) + +## 4.2.0-beta.3 - September 21, 2016 + +Mapbox Android 4.2.0-beta.3 contains all 4.2.0-beta.2 changes and adds: + +* Configurable API endpoint [#6309](https://github.com/mapbox/mapbox-gl-native/pull/6309) +* Adjusted logging level for failed OkHttp requests [6356](https://github.com/mapbox/mapbox-gl-native/issues/6356) +* Offline improvements: + - Fix SQLite crashes by reverting WAL journal mode [#6320](https://github.com/mapbox/mapbox-gl-native/pull/6320) + +## 4.2.0-beta.2 - August 25, 2016 + +Mapbox Android 4.2.0-beta.2 contains all 4.2.0-beta.1 changes and adds: + +* Query rendered features [#5869](https://github.com/mapbox/mapbox-gl-native/issues/5869) +* Clustering [#5852](https://github.com/mapbox/mapbox-gl-native/issues/5852) +* Integrated support for [Mapbox Android Services](https://www.mapbox.com/android-sdk/#mapbox_android_services) +* Offline improvements: + - Enable external storage to host the tiles database [#5589](https://github.com/mapbox/mapbox-gl-native/issues/5589) + - Stop delivering status updates when offline download is canceled [#5538](https://github.com/mapbox/mapbox-gl-native/issues/5538) + - Update SQLite schema with WAL journal mode and normal sync [#5796](https://github.com/mapbox/mapbox-gl-native/pull/5796) + - Check for network connectivity before requesting resources [#6123](https://github.com/mapbox/mapbox-gl-native/pull/6123) + +## 4.2.0-beta.1 - August 6, 2016 + +Mapbox Android 4.2.0 introduces the Runtime Style API and improves performance. + +* Runtime Style API ([#5610](https://github.com/mapbox/mapbox-gl-native/issues/5610)) +* Earcut ([#2444](https://github.com/mapbox/mapbox-gl-native/pull/2444)) +* Map view rendered with `SurfaceView` for improved performance [#5000](https://github.com/mapbox/mapbox-gl-native/issues/5000) + +## 4.1.1 - July 19, 2016 + +Mapbox Android 4.1.1 is a patch release and is recommended to upgrade to it as soon as possible. + +* Default Styles Caching Crash ([#5722](https://github.com/mapbox/mapbox-gl-native/issues/5722)) + +## 4.1.0 - June 29, 2016 + +Mapbox Android 4.1.0 builds off our ambitious 4.0.0 version with 3 major new features being released. + +* View Based Annotations ([#3276](https://github.com/mapbox/mapbox-gl-native/issues/3276)) +* UserLocationView Refactor ([#4396](https://github.com/mapbox/mapbox-gl-native/issues/4396)) +* MapboxAccountManager ([#5004](https://github.com/mapbox/mapbox-gl-native/issues/5004)) + +## 4.1.0-beta.3 - June 20, 2016 + +* New samples: + * [Location picker](https://github.com/mapbox/mapbox-gl-native/pull/5391) + * [Animate and rotate multiple markers](https://github.com/mapbox/mapbox-gl-native/issues/5299) + * [Scaling marker activity](https://github.com/mapbox/mapbox-gl-native/issues/5409) +* Marker improvements: + * [Expose MarkerView alpha](https://github.com/mapbox/mapbox-gl-native/pull/5329) + * [Icon should be optional for MarkerView](https://github.com/mapbox/mapbox-gl-native/pull/5328) + * [Expose an API to enable selection/deselection of markers on a map tap](https://github.com/mapbox/mapbox-gl-native/pull/5312) + * [Bring selected MarkerView to the front](https://github.com/mapbox/mapbox-gl-native/pull/5294) +* [Make gesture focal point configurable](https://github.com/mapbox/mapbox-gl-native/pull/5332) + +## 4.1.0-beta.2 - June 8, 2016 + +* Dynamically Update InfoWindow ([#5237](https://github.com/mapbox/mapbox-gl-native/issues/5237)) +* armeabi ABI Work On armv7 Devices ([#3985](https://github.com/mapbox/mapbox-gl-native/issues/3985)) +* Remove Adapter Requirement For MarkerView ([#5214](https://github.com/mapbox/mapbox-gl-native/issues/5214)) +* Always Current Version Style URL Constants ([#5193](https://github.com/mapbox/mapbox-gl-native/issues/5193)) +* Random NullPointerException On Telemetry ([#5186](https://github.com/mapbox/mapbox-gl-native/issues/5186)) + +## 4.1.0-beta.1 - May 26, 2016 + +Mapbox Android 4.1.0 builds off our ambitious 4.0.0 version with 3 major new features being released. To help us produce the highest quality SDK possible we're releasing an official Beta release first so that everyone has time to explore it and help hardened it before the official 4.1.0 Final Release. + +* View Based Annotations ([#3276](https://github.com/mapbox/mapbox-gl-native/issues/3276)) +* UserLocationView Refactor ([#4396](https://github.com/mapbox/mapbox-gl-native/issues/4396)) +* MapboxAccountManager ([#5004](https://github.com/mapbox/mapbox-gl-native/issues/5004)) + +## 4.0.1 - May 12, 2016 + +Mapbox Android 4.0.1 is a patch release to make this bug fix available sooner. + +* MapboxMap.removeAnnotations() doesn't remove markers ([#4553](https://github.com/mapbox/mapbox-gl-native/issues/4553)) + +## 4.0.0 - March 30, 2016 + +Mapbox Android 4.0.0 contains the following 3 major new features. + +* MapboxMap API Change ([#3145](https://github.com/mapbox/mapbox-gl-native/issues/3145)) +* Offline Maps ([#3891](https://github.com/mapbox/mapbox-gl-native/issues/3891)) +* Telemetry ([#2421](https://github.com/mapbox/mapbox-gl-native/issues/2421)) + +## 4.0.0-rc.1 - March 25, 2016 + +* Default Value Bug Fix for MapboxMapOptions ([#4398](https://github.com/mapbox/mapbox-gl-native/issues/4398)) +* NullPointerException When Scrolling ([#4424](https://github.com/mapbox/mapbox-gl-native/issues/4424)) +* Platform Specific CHANGELOGS ([#4432](https://github.com/mapbox/mapbox-gl-native/issues/4432)) +* Introduce LatLng.wrap() ([#4475](https://github.com/mapbox/mapbox-gl-native/issues/4475)) + +## 4.0.0-beta.2 - March 21, 2016 + +* arm64 ABI Support ([#3128](https://github.com/mapbox/mapbox-gl-native/issues/3128)) +* Unify Offline And Cache Databases ([#4362](https://github.com/mapbox/mapbox-gl-native/issues/4362)) +* Offline Database Vacuuming ([#4342](https://github.com/mapbox/mapbox-gl-native/pull/4342)) +* Telemetry User Agent Fix ([#4328](https://github.com/mapbox/mapbox-gl-native/issues/4328)) +* OnCameraChange Bug Fix ([#4327](https://github.com/mapbox/mapbox-gl-native/issues/4327)) +* OnCameraChangeListener vs getCameraPosition ([#4326](https://github.com/mapbox/mapbox-gl-native/issues/4326)) + +## 4.0.0-beta.1 - March 15, 2016 + +Mapbox Android 4.0.0 is the most ambitious Android release to date with 3 major new features being released. To help us produce the highest quality SDK possible we're releasing an official Beta release first so that everyone has time to explore it and help hardened it before the official 4.0.0 Final Release. + +* MapboxMap API Change ([#3145](https://github.com/mapbox/mapbox-gl-native/issues/3145)) +* Offline Maps ([#3891](https://github.com/mapbox/mapbox-gl-native/issues/3891)) +* Telemetry ([#2421](https://github.com/mapbox/mapbox-gl-native/issues/2421)) + +## 3.2.0 - January 28, 2016 + +* Fixed crash caused by annotation image with non-integer width or height ([#3031](https://github.com/mapbox/mapbox-gl-native/issues/3031)) +* Tracking Mode Reverses Bearing Fix ([#3664](https://github.com/mapbox/mapbox-gl-native/issues/3664)) +* GPS Extra Rotation Fix ([#3661](https://github.com/mapbox/mapbox-gl-native/issues/3661)) +* Added new methods for getting and setting the min and max zoom levels: `getMinZoom`, `setMinZoom`, `getMaxZoom`, `setMaxZoom`. ([#509](https://github.com/mapbox/mapbox-gl-native/issues/509)) + +## 3.1.0 - January 20, 2016 + +* Camera API Callback Improvements ([#3412](https://github.com/mapbox/mapbox-gl-native/issues/3412)) +* Coordinate Deprecated For LatLng ([#3309](https://github.com/mapbox/mapbox-gl-native/issues/3309)) +* Responsive User Dot Location Tracking ([#2049](https://github.com/mapbox/mapbox-gl-native/issues/2049)) + +## 3.0.0 - December 21, 2015 + +* Added Camera API ([#3244](https://github.com/mapbox/mapbox-gl-native/issues/3244)) +* Custom Layer Support ([#3248](https://github.com/mapbox/mapbox-gl-native/issues/3348)) +* Reverse Tilt Gesutre Detection ([#3245](https://github.com/mapbox/mapbox-gl-native/issues/3245)) +* Decoupled Location Provider ([#2954](https://github.com/mapbox/mapbox-gl-native/issues/2954)) + +## 2.3.0 - December 4, 2015 + +* Added Tilt / Pitch Support ([#2805](https://github.com/mapbox/mapbox-gl-native/issues/2805)) +* Keep InfoWindow Open When Panning ([#3121](https://github.com/mapbox/mapbox-gl-native/issues/3121)) +* Concurrent Multiple Open InfoWindows ([#3115](https://github.com/mapbox/mapbox-gl-native/issues/3115)) +* OkHttp Replace curl ([#2856](https://github.com/mapbox/mapbox-gl-native/issues/2856)) +* GPS and Compass Customization Support ([#2858](https://github.com/mapbox/mapbox-gl-native/issues/2858)) + +## 2.2.0 - October 28, 2015 + +- New User Dot location graphics ([#2732](https://github.com/mapbox/mapbox-gl-native/issues/2732)) +- Custom Drawable Markers ([#2744](https://github.com/mapbox/mapbox-gl-native/issues/2744)) +- `MapView.setOnInfoWindowClickListener()` support ([#2448](https://github.com/mapbox/mapbox-gl-native/issues/2448)) +- Completed Annotations API ([#1716](https://github.com/mapbox/mapbox-gl-native/issues/1716)) +- Satellite Streets Style ([#2739](https://github.com/mapbox/mapbox-gl-native/issues/2739)) +- **RESOLVED** Black Screen On Ice Cream Sandwich and Jelly Bean devices ([#2802](https://github.com/mapbox/mapbox-gl-native/issues/2802)) + +## 2.1.0 - October 21, 2015 + +- Initial Android release. diff --git a/DISTRIBUTE.md b/DISTRIBUTE.md new file mode 100644 index 000000000..8f2bec19d --- /dev/null +++ b/DISTRIBUTE.md @@ -0,0 +1,57 @@ +# Distributing Mapbox GL Native for Android + +Depending on your use case, you may want to support all or just a subset of [Android ABIs](http://developer.android.com/ndk/guides/abis.html). This document covers building an `.aar` file from the Mapbox Maps SDK for Android and building `.so` files for specific ABIs. In normal circumstances an application developer will use [APK splits](https://developer.android.com/studio/build/configure-apk-splits.html) to optimize this at application level. + +##### Build types + +With a `BUILDTYPE` var you can specify the build type for the `.so` and `.aar` files: + +```bash +BUILDTYPE=Debug or BUILDTYPE=Release +``` + +##### Creating an Android Archive file that supports all ABIs + +```sh +BUILDTYPE=Release make apackage +``` + +This will build native libraries to support following ABIs: +- armeabi +- armeabi-v7a +- arm64-v8a +- x86 +- x86_64 +- mips + +After succesfully finish building the native libraries, gradle will build the MapboxAndroidSDK module and generate an Android Archive file in `MapboxAndroidSDK/build/outputs/aar `. The packaged native libraries can be found in `MapboxAndroidSDK/src/main/jniLibs/`. + +##### Build native libraries for a specific ABI + +```sh +make android-lib-%% +``` + +In the command above you'll need to replace `%%` with an ABI key listed below: + +| ABI Key | Android ABI | +| ------- | ----------- | +| arm-v5 | armeabi | +| arm-v7 | armeabi-v7a | +| arm-v8 | arm64-v8a | +| x86 | x86 | +| x86-64 | x86_64 | +| mips | mips | + +For example, to build the arm64-v8a ABI the Makefile target would be: + +```sh +make android-lib-arm-v8 +``` + +It's also possible to build multiple ABI (without having to build them all) by running the Makefile multiple times. For example to build arm64-v8a and x86_64 in the same project the commands would be: + +```sh +make android-lib-arm-v8 +make android-lib-arm-x86-64 +``` diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..8423a34a6 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,227 @@ + +## Additional Mapbox GL licenses +Mapbox GL uses portions of the Android Arch-Common. +URL: [https://developer.android.com/topic/libraries/architecture/index.html](https://developer.android.com/topic/libraries/architecture/index.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Arch-Runtime. +URL: [https://developer.android.com/topic/libraries/architecture/index.html](https://developer.android.com/topic/libraries/architecture/index.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Lifecycle LiveData. +URL: [https://developer.android.com/topic/libraries/architecture/index.html](https://developer.android.com/topic/libraries/architecture/index.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Lifecycle LiveData Core. +URL: [https://developer.android.com/topic/libraries/architecture/index.html](https://developer.android.com/topic/libraries/architecture/index.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Lifecycle Runtime. +URL: [https://developer.android.com/topic/libraries/architecture/index.html](https://developer.android.com/topic/libraries/architecture/index.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Lifecycle ViewModel. +URL: [https://developer.android.com/topic/libraries/architecture/index.html](https://developer.android.com/topic/libraries/architecture/index.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Lifecycle-Common. +URL: [https://developer.android.com/topic/libraries/architecture/index.html](https://developer.android.com/topic/libraries/architecture/index.html) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library Annotations. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library Async Layout Inflater. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library collections. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library compat. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library Coordinator Layout. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library core UI. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library core utils. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library Cursor Adapter. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library Custom View. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library Custom View. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library Document File. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library Drawer Layout. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library fragment. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library Interpolators. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library loader. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library Local Broadcast Manager. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library Print. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library Sliding Pane Layout. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Android Support Library View Pager. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Gson. +License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Mapbox Accounts SDK for Android. +URL: [https://github.com/mapbox/mapbox-accounts-android](https://github.com/mapbox/mapbox-accounts-android) +License: [Mapbox Terms of Service](https://www.mapbox.com/tos/) + +=========================================================================== + +Mapbox GL uses portions of the Mapbox Android Core Library. +URL: [https://github.com/mapbox/mapbox-events-android](https://github.com/mapbox/mapbox-events-android) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Mapbox Android Gestures Library. +URL: [https://github.com/mapbox/mapbox-gestures-android](https://github.com/mapbox/mapbox-gestures-android) +License: [BSD](https://opensource.org/licenses/BSD-2-Clause) + +=========================================================================== + +Mapbox GL uses portions of the Mapbox Android Telemetry Library. +URL: [https://github.com/mapbox/mapbox-events-android](https://github.com/mapbox/mapbox-events-android) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Mapbox Java SDK. +URL: [https://github.com/mapbox/mapbox-java](https://github.com/mapbox/mapbox-java) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Mapbox Java SDK. +URL: [https://github.com/mapbox/mapbox-java](https://github.com/mapbox/mapbox-java) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the OkHttp. +License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Okio. +License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the SoLoader. +URL: [https://github.com/facebook/soloader](https://github.com/facebook/soloader) +License: [Apache-2](https://github.com/facebook/soloader/blob/master/LICENSE) + +=========================================================================== + +Mapbox GL uses portions of the VersionedParcelable and friends. +URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) +License: [The Apache Software License](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox GL uses portions of the Gradle License Plugin. +URL: [https://github.com/jaredsburrows/gradle-license-plugin](https://github.com/jaredsburrows/gradle-license-plugin) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + diff --git a/MapboxGLAndroidSDK/.gitignore b/MapboxGLAndroidSDK/.gitignore new file mode 100644 index 000000000..e44384742 --- /dev/null +++ b/MapboxGLAndroidSDK/.gitignore @@ -0,0 +1,2 @@ +dependency-graph-mapbox-libraries.png +src/test/resources/mobile-event* diff --git a/MapboxGLAndroidSDK/build.gradle b/MapboxGLAndroidSDK/build.gradle new file mode 100644 index 000000000..5ee3af61e --- /dev/null +++ b/MapboxGLAndroidSDK/build.gradle @@ -0,0 +1,105 @@ +apply plugin: 'com.android.library' +apply plugin: "com.jaredsburrows.license" +apply plugin: 'kotlin-android' +apply from: "${rootDir}/gradle/native-build.gradle" + +dependencies { + lintChecks project(":MapboxGLAndroidSDKLint") + api dependenciesList.mapboxAndroidTelemetry + api dependenciesList.mapboxJavaGeoJSON + api dependenciesList.mapboxAndroidGestures + api dependenciesList.mapboxAndroidAccounts + implementation dependenciesList.mapboxJavaTurf + implementation dependenciesList.supportAnnotations + implementation dependenciesList.supportFragmentV4 + implementation dependenciesList.okhttp3 + implementation dependenciesList.soLoader + testImplementation dependenciesList.junit + testImplementation dependenciesList.mockito + testImplementation dependenciesList.mockk + testImplementation dependenciesList.robolectric + testImplementation dependenciesList.kotlinLib + testImplementation dependenciesList.commonsIO +} + +android { + compileSdkVersion androidVersions.compileSdkVersion + + defaultConfig { + minSdkVersion androidVersions.minSdkVersion + targetSdkVersion androidVersions.targetSdkVersion + buildConfigField "String", "GIT_REVISION_SHORT", String.format("\"%s\"", getGitRevision()) + buildConfigField "String", "MAPBOX_SDK_IDENTIFIER", String.format("\"%s\"", "mapbox-maps-android") + buildConfigField "String", "MAPBOX_SDK_VERSION", String.format("\"%s\"", project.VERSION_NAME) + buildConfigField "String", "MAPBOX_VERSION_STRING", String.format("\"Mapbox/%s\"", project.VERSION_NAME) + buildConfigField "String", "MAPBOX_EVENTS_USER_AGENT", String.format("\"mapbox-maps-android/%s\"", project.VERSION_NAME) + consumerProguardFiles 'proguard-rules.pro' + } + + // build native libraries + List nativeTargets = ["mapbox-gl"] + if (project.hasProperty("mapbox.with_test")) { + nativeTargets.add("mbgl-test") + } + if (project.hasProperty("mapbox.with_benchmark")) { + nativeTargets.add("mbgl-benchmark") + } + nativeBuild(nativeTargets) + + // avoid naming conflicts, force usage of prefix + resourcePrefix 'mapbox_' + + sourceSets { + // limit amount of exposed library resources + main.res.srcDirs += 'src/main/res-public' + } + + lintOptions { + disable 'MissingTranslation', 'TypographyQuotes', 'ObsoleteLintCustomCheck', 'MissingPermission', 'WrongThreadInterprocedural' + checkAllWarnings true + warningsAsErrors false + } + + testOptions { + unitTests { + returnDefaultValues true + + // Roboelectric 4.0 required config + // http://robolectric.org/migrating/#migrating-to-40 + includeAndroidResources = true + } + } + + buildTypes { + debug { + testCoverageEnabled true + jniDebuggable true + } + } +} + +licenseReport { + generateHtmlReport = false + generateJsonReport = true + copyHtmlReportToAssets = false + copyJsonReportToAssets = false +} + +def static getGitRevision() { + def cmd = "git rev-parse --short HEAD" + def proc = cmd.execute() + def ref = proc.text.trim() + return ref +} + +configurations { + all*.exclude group: 'commons-logging', module: 'commons-logging' + all*.exclude group: 'commons-collections', module: 'commons-collections' +} +apply from: "${rootDir}/gradle/gradle-javadoc.gradle" +apply from: "${rootDir}/gradle/gradle-checkstyle.gradle" +apply from: "${rootDir}/gradle/gradle-dependencies-graph.gradle" +apply from: "${rootDir}/gradle/gradle-update-vendor-modules.gradle" +apply from: "${rootDir}/gradle/android-nitpick.gradle" +apply from: "${rootDir}/gradle/gradle-bintray.gradle" +apply from: "${rootDir}/gradle/jacoco-report.gradle" \ No newline at end of file diff --git a/MapboxGLAndroidSDK/lint.xml b/MapboxGLAndroidSDK/lint.xml new file mode 100644 index 000000000..3a244199a --- /dev/null +++ b/MapboxGLAndroidSDK/lint.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/MapboxGLAndroidSDK/proguard-rules.pro b/MapboxGLAndroidSDK/proguard-rules.pro new file mode 100644 index 000000000..f592efa6b --- /dev/null +++ b/MapboxGLAndroidSDK/proguard-rules.pro @@ -0,0 +1,41 @@ +# By default, the flags in this file are appended to flags specified +# in ../sdk/tools/proguard/proguard-android.txt, +# contents of this file will be appended into proguard-android.txt +-keepattributes Signature, *Annotation*, EnclosingMethod + +# Reflection on classes from native code +-keep class com.google.gson.JsonArray { *; } +-keep class com.google.gson.JsonElement { *; } +-keep class com.google.gson.JsonObject { *; } +-keep class com.google.gson.JsonPrimitive { *; } +-dontnote com.google.gson.** + +# dontnote for keeps the entry point x but not the descriptor class y +-dontnote com.mapbox.mapboxsdk.maps.MapboxMap$OnFpsChangedListener +-dontnote com.mapbox.mapboxsdk.style.layers.PropertyValue +-dontnote com.mapbox.mapboxsdk.maps.MapboxMap +-dontnote com.mapbox.mapboxsdk.maps.MapboxMapOptions +-dontnote com.mapbox.mapboxsdk.log.LoggerDefinition +-dontnote com.mapbox.android.core.location.LocationEnginePriority + +# config for okhttp 3.11.0, https://github.com/square/okhttp/pull/3354 +-dontwarn javax.annotation.** +-dontnote okhttp3.internal.** +-dontwarn org.codehaus.** + +# config for mapbox-sdk-geojson:3.0.1 +-keep class com.mapbox.geojson.** { *; } +-dontwarn com.google.auto.value.** + +# config for additional notes +-dontnote org.robolectric.Robolectric +-dontnote libcore.io.Memory +-dontnote com.google.protobuf.** +-dontnote android.net.** +-dontnote org.apache.http.** + +# config for mapbox-sdk-services +# while we don't include this dependency directly +# a large amount of users combine it with our SDK +# we aren't able to provide a proguard config in that project (jar vs aar) +-dontwarn com.sun.xml.internal.ws.spi.db.* \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/AndroidManifest.xml b/MapboxGLAndroidSDK/src/main/AndroidManifest.xml new file mode 100644 index 000000000..dcbec69bf --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/AccountsManager.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/AccountsManager.java new file mode 100644 index 000000000..5b8f10e01 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/AccountsManager.java @@ -0,0 +1,170 @@ +package com.mapbox.mapboxsdk; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.text.format.DateUtils; + +import com.mapbox.android.accounts.v1.MapboxAccounts; +import com.mapbox.mapboxsdk.constants.MapboxConstants; +import com.mapbox.mapboxsdk.log.Logger; + +import static com.mapbox.mapboxsdk.constants.MapboxConstants.KEY_PREFERENCE_SKU_TOKEN; + +/** + * REMOVAL OR MODIFICATION OF THE FOLLOWING CODE VIOLATES THE MAPBOX TERMS + * OF SERVICE + *

+ * The following code is used to access Mapbox's Mapping APIs. + *

+ * Removal or modification of this code when used with Mapbox's Mapping APIs + * can result in termination of your agreement and/or your account with + * Mapbox. + *

+ * Using this code to access Mapbox Mapping APIs from outside the Mapbox Maps + * SDK also violates the Mapbox Terms of Service. On Android, Mapping APIs + * should be accessed using the methods documented at + * https://www.mapbox.com/android. + *

+ * You can access the Mapbox Terms of Service at https://www.mapbox.com/tos/ + */ +class AccountsManager { + private static final String TAG = "Mbgl-AccountsManager"; + private static final String PREFERENCE_USER_ID = "com.mapbox.mapboxsdk.accounts.userid"; + private static final String PREFERENCE_TIMESTAMP = "com.mapbox.mapboxsdk.accounts.timestamp"; + + private SharedPreferences sharedPreferences; + private String skuToken; + private long timestamp; + private boolean isManaged; + + AccountsManager() { + isManaged = isSkuTokenManaged(); + initialize(); + } + + @VisibleForTesting + AccountsManager(SharedPreferences sharedPreferences, boolean isManaged) { + this.sharedPreferences = sharedPreferences; + this.isManaged = isManaged; + initialize(); + } + + private void initialize() { + retrieveSkuTokenAndTimestamp(); + if (isManaged) { + String userId = validateUserId(); + validateRotation(userId); + } + } + + private boolean isSkuTokenManaged() { + boolean value = MapboxConstants.DEFAULT_MANAGE_SKU_TOKEN; + try { + // Try getting a custom value from the app Manifest + ApplicationInfo appInfo = retrieveApplicationInfo(); + if (appInfo.metaData != null) { + value = appInfo.metaData.getBoolean( + MapboxConstants.KEY_META_DATA_MANAGE_SKU_TOKEN, + MapboxConstants.DEFAULT_MANAGE_SKU_TOKEN + ); + } + } catch (Exception exception) { + Logger.e(TAG, "Failed to read the package metadata: ", exception); + } + + return value; + } + + private ApplicationInfo retrieveApplicationInfo() throws PackageManager.NameNotFoundException { + return Mapbox.getApplicationContext().getPackageManager().getApplicationInfo( + Mapbox.getApplicationContext().getPackageName(), + PackageManager.GET_META_DATA); + } + + private void retrieveSkuTokenAndTimestamp() { + SharedPreferences sharedPreferences = getSharedPreferences(); + skuToken = sharedPreferences.getString(KEY_PREFERENCE_SKU_TOKEN, ""); + timestamp = sharedPreferences.getLong(PREFERENCE_TIMESTAMP, 0L); + } + + private String validateUserId() { + SharedPreferences sharedPreferences = getSharedPreferences(); + String userId = sharedPreferences.getString(PREFERENCE_USER_ID, ""); + if (TextUtils.isEmpty(userId)) { + userId = generateUserId(); + SharedPreferences.Editor editor = getSharedPreferences().edit(); + editor.putString(PREFERENCE_USER_ID, userId); + editor.apply(); + } + + return userId; + } + + private void validateRotation(String userId) { + if (TextUtils.isEmpty(skuToken) || timestamp == 0L) { + skuToken = generateSkuToken(userId); + timestamp = persistRotation(skuToken); + } + } + + String getSkuToken() { + if (isManaged) { + if (isExpired()) { + SharedPreferences sharedPreferences = getSharedPreferences(); + String userId = sharedPreferences.getString(PREFERENCE_USER_ID, ""); + skuToken = generateSkuToken(userId); + timestamp = persistRotation(skuToken); + } + } else { + SharedPreferences sharedPreferences = getSharedPreferences(); + String notManagedSkuToken = sharedPreferences.getString(KEY_PREFERENCE_SKU_TOKEN, ""); + skuToken = notManagedSkuToken; + } + return skuToken; + } + + private boolean isExpired() { + return isExpired(getNow(), timestamp); + } + + static boolean isExpired(long now, long then) { + return ((now - then) > DateUtils.HOUR_IN_MILLIS); + } + + private long persistRotation(String skuToken) { + long now = getNow(); + SharedPreferences.Editor editor = getSharedPreferences().edit(); + editor.putLong(PREFERENCE_TIMESTAMP, now); + editor.putString(KEY_PREFERENCE_SKU_TOKEN, skuToken); + editor.apply(); + return now; + } + + @NonNull + private SharedPreferences getSharedPreferences() { + if (sharedPreferences == null) { + sharedPreferences = Mapbox.getApplicationContext() + .getSharedPreferences(MapboxConstants.MAPBOX_SHARED_PREFERENCES, Context.MODE_PRIVATE); + } + return sharedPreferences; + } + + static long getNow() { + return System.currentTimeMillis(); + } + + @NonNull + private String generateUserId() { + return MapboxAccounts.obtainEndUserId(); + } + + @NonNull + private String generateSkuToken(String userId) { + return MapboxAccounts.obtainMapsSkuUserToken(userId); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/LibraryLoader.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/LibraryLoader.java new file mode 100644 index 000000000..ee9f4b3b6 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/LibraryLoader.java @@ -0,0 +1,55 @@ +package com.mapbox.mapboxsdk; + +import com.mapbox.mapboxsdk.log.Logger; + +/** + * Loads the mapbox-gl shared library + *

+ * By default uses the {@link System#loadLibrary(String)}, + * use {@link #setLibraryLoader(LibraryLoader)} to provide an alternative library loading hook. + *

+ */ +public abstract class LibraryLoader { + + private static final String TAG = "Mbgl-LibraryLoader"; + + private static final LibraryLoader DEFAULT = Mapbox.getModuleProvider() + .createLibraryLoaderProvider() + .getDefaultLibraryLoader(); + + private static volatile LibraryLoader loader = DEFAULT; + + private static boolean loaded; + + /** + * Set the library loader that loads the shared library. + * + * @param libraryLoader the library loader + */ + public static void setLibraryLoader(LibraryLoader libraryLoader) { + loader = libraryLoader; + } + + /** + * Loads "libmapbox-gl.so" native shared library. + *

+ * Catches UnsatisfiedLinkErrors and prints a warning to logcat. + *

+ */ + public static void load() { + try { + if (!loaded) { + loaded = true; + loader.load("mapbox-gl"); + } + } catch (UnsatisfiedLinkError error) { + loaded = false; + String message = "Failed to load native shared library."; + Logger.e(TAG, message, error); + MapStrictMode.strictModeViolation(message, error); + } + } + + public abstract void load(String name); +} + diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/LibraryLoaderProvider.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/LibraryLoaderProvider.java new file mode 100644 index 000000000..c9d98bf76 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/LibraryLoaderProvider.java @@ -0,0 +1,15 @@ +package com.mapbox.mapboxsdk; + +/** + * Injects the default library loader. + */ +public interface LibraryLoaderProvider { + + /** + * Creates and returns a the default Library Loader. + * + * @return the default library loader + */ + LibraryLoader getDefaultLibraryLoader(); + +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapStrictMode.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapStrictMode.java new file mode 100644 index 000000000..c7f995964 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapStrictMode.java @@ -0,0 +1,46 @@ +package com.mapbox.mapboxsdk; + +/** + * Using this class you can enable a strict mode that will throw the {@link MapStrictModeException} + * whenever the map would fail silently otherwise. + */ +public class MapStrictMode { + private static volatile boolean strictModeEnabled; + + /** + * Set the strict mode that will throw the {@link MapStrictModeException} + * whenever the map would fail silently otherwise. + * + * @param strictModeEnabled true to enable the strict mode, false otherwise + */ + public static synchronized void setStrictModeEnabled(boolean strictModeEnabled) { + MapStrictMode.strictModeEnabled = strictModeEnabled; + } + + /** + * Internal use. Called whenever the strict mode violation occurs. + */ + public static void strictModeViolation(String message) { + if (strictModeEnabled) { + throw new MapStrictModeException(message); + } + } + + /** + * Internal use. Called whenever the strict mode violation occurs. + */ + public static void strictModeViolation(String message, Throwable throwable) { + if (strictModeEnabled) { + throw new MapStrictModeException(String.format("%s - %s", message, throwable)); + } + } + + /** + * Internal use. Called whenever the strict mode violation occurs. + */ + public static void strictModeViolation(Throwable throwable) { + if (strictModeEnabled) { + throw new MapStrictModeException(String.format("%s", throwable)); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapStrictModeException.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapStrictModeException.java new file mode 100644 index 000000000..5bfddabf9 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/MapStrictModeException.java @@ -0,0 +1,7 @@ +package com.mapbox.mapboxsdk; + +class MapStrictModeException extends RuntimeException { + MapStrictModeException(String message) { + super(String.format("Map detected an error that would fail silently otherwise: %s", message)); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/Mapbox.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/Mapbox.java new file mode 100644 index 000000000..01c82ddad --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/Mapbox.java @@ -0,0 +1,225 @@ +package com.mapbox.mapboxsdk; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; + +import com.mapbox.mapboxsdk.constants.MapboxConstants; +import com.mapbox.mapboxsdk.exceptions.MapboxConfigurationException; +import com.mapbox.mapboxsdk.log.Logger; +import com.mapbox.mapboxsdk.maps.TelemetryDefinition; +import com.mapbox.mapboxsdk.net.ConnectivityReceiver; +import com.mapbox.mapboxsdk.storage.FileSource; +import com.mapbox.mapboxsdk.utils.ThreadUtils; + +/** + * The entry point to initialize the Mapbox Android SDK. + *

+ * Obtain a reference by calling {@link #getInstance(Context, String)}. Usually this class is configured in + * Application#onCreate() and is responsible for the active access token, application context, and + * connectivity state. + *

+ */ +@UiThread +@SuppressLint("StaticFieldLeak") +public final class Mapbox { + + private static final String TAG = "Mbgl-Mapbox"; + private static ModuleProvider moduleProvider; + private static Mapbox INSTANCE; + + private Context context; + @Nullable + private String accessToken; + @Nullable + private TelemetryDefinition telemetry; + @Nullable + private AccountsManager accounts; + + /** + * Get an instance of Mapbox. + *

+ * This class manages the Mapbox access token, application context, and connectivity state. + *

+ * + * @param context Android context which holds or is an application context + * @param accessToken Mapbox access token + * @return the single instance of Mapbox + */ + @UiThread + @NonNull + public static synchronized Mapbox getInstance(@NonNull Context context, @Nullable String accessToken) { + ThreadUtils.init(context); + ThreadUtils.checkThread(TAG); + if (INSTANCE == null) { + Context appContext = context.getApplicationContext(); + FileSource.initializeFileDirsPaths(appContext); + INSTANCE = new Mapbox(appContext, accessToken); + if (isAccessTokenValid(accessToken)) { + initializeTelemetry(); + INSTANCE.accounts = new AccountsManager(); + } + ConnectivityReceiver.instance(appContext); + } + return INSTANCE; + } + + Mapbox(@NonNull Context context, @Nullable String accessToken) { + this.context = context; + this.accessToken = accessToken; + } + + /** + * Get the current active access token for this application. + * + * @return Mapbox access token + */ + @Nullable + public static String getAccessToken() { + validateMapbox(); + return INSTANCE.accessToken; + } + + /** + * Set the current active accessToken. + */ + public static void setAccessToken(String accessToken) { + validateMapbox(); + INSTANCE.accessToken = accessToken; + + // cleanup telemetry which is dependent on an access token + if (INSTANCE.telemetry != null) { + INSTANCE.telemetry.disableTelemetrySession(); + INSTANCE.telemetry = null; + } + + // initialize components dependent on a token + if (isAccessTokenValid(accessToken)) { + initializeTelemetry(); + INSTANCE.accounts = new AccountsManager(); + } else { + INSTANCE.accounts = null; + } + FileSource.getInstance(getApplicationContext()).setAccessToken(accessToken); + } + + /** + * Returns a SKU token, refreshed if necessary. This method is meant for internal SDK + * usage only. + * + * @return the SKU token + */ + public static String getSkuToken() { + if (INSTANCE.accounts == null) { + throw new MapboxConfigurationException( + "A valid access token parameter is required when using a Mapbox service." + + "\nPlease see https://www.mapbox.com/help/create-api-access-token/ to learn how to create one." + + "\nMore information in this guide https://www.mapbox.com/help/first-steps-android-sdk/#access-tokens." + + "Currently provided token is: " + INSTANCE.accessToken); + } + return INSTANCE.accounts.getSkuToken(); + } + + /** + * Application context + * + * @return the application context + */ + @NonNull + public static Context getApplicationContext() { + validateMapbox(); + return INSTANCE.context; + } + + /** + * Manually sets the connectivity state of the app. This is useful for apps which control their + * own connectivity state and want to bypass any checks to the ConnectivityManager. + * + * @param connected flag to determine the connectivity state, true for connected, false for + * disconnected, and null for ConnectivityManager to determine. + */ + public static synchronized void setConnected(Boolean connected) { + validateMapbox(); + ConnectivityReceiver.instance(INSTANCE.context).setConnected(connected); + } + + /** + * Determines whether we have an internet connection available. Please do not rely on this + * method in your apps. This method is used internally by the SDK. + * + * @return true if there is an internet connection, false otherwise + */ + public static synchronized Boolean isConnected() { + validateMapbox(); + return ConnectivityReceiver.instance(INSTANCE.context).isConnected(); + } + + /** + * Initializes telemetry + */ + private static void initializeTelemetry() { + try { + INSTANCE.telemetry = getModuleProvider().obtainTelemetry(); + } catch (Exception exception) { + String message = "Error occurred while initializing telemetry"; + Logger.e(TAG, message, exception); + MapStrictMode.strictModeViolation(message, exception); + } + } + + /** + * Get an instance of Telemetry if initialised + * + * @return instance of telemetry + */ + @Nullable + public static TelemetryDefinition getTelemetry() { + return INSTANCE.telemetry; + } + + /** + * Get the module provider + * + * @return moduleProvider + */ + @NonNull + public static ModuleProvider getModuleProvider() { + if (moduleProvider == null) { + moduleProvider = new ModuleProviderImpl(); + } + return moduleProvider; + } + + /** + * Runtime validation of Mapbox creation. + */ + private static void validateMapbox() { + if (INSTANCE == null) { + throw new MapboxConfigurationException(); + } + } + + /** + * Runtime validation of Mapbox access token + * + * @param accessToken the access token to validate + * @return true is valid, false otherwise + */ + static boolean isAccessTokenValid(@Nullable String accessToken) { + if (accessToken == null) { + return false; + } + + accessToken = accessToken.trim().toLowerCase(MapboxConstants.MAPBOX_LOCALE); + return accessToken.length() != 0 && (accessToken.startsWith("pk.") || accessToken.startsWith("sk.")); + } + + /** + * Internal use. Check if the {@link Mapbox#INSTANCE} is present. + */ + public static boolean hasInstance() { + return INSTANCE != null; + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/ModuleProvider.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/ModuleProvider.java new file mode 100644 index 000000000..9749290eb --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/ModuleProvider.java @@ -0,0 +1,38 @@ +package com.mapbox.mapboxsdk; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.mapbox.mapboxsdk.http.HttpRequest; +import com.mapbox.mapboxsdk.maps.TelemetryDefinition; + +/** + * Injects concrete instances of configurable abstractions + */ +public interface ModuleProvider { + + /** + * Create a new concrete implementation of HttpRequest. + * + * @return a new instance of an HttpRequest + */ + @NonNull + HttpRequest createHttpRequest(); + + /** + * Get the concrete implementation of TelemetryDefinition + * + * @return a single instance of Telemetry + */ + @Nullable + TelemetryDefinition obtainTelemetry(); + + + /** + * Get the concrete implementation of LibraryLoaderProvider + * + * @return a new instance of LibraryLoaderProvider + */ + @NonNull + LibraryLoaderProvider createLibraryLoaderProvider(); + +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/ModuleProviderImpl.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/ModuleProviderImpl.java new file mode 100644 index 000000000..2de111c97 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/ModuleProviderImpl.java @@ -0,0 +1,30 @@ +package com.mapbox.mapboxsdk; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.mapbox.mapboxsdk.http.HttpRequest; +import com.mapbox.mapboxsdk.maps.TelemetryDefinition; +import com.mapbox.mapboxsdk.module.http.HttpRequestImpl; +import com.mapbox.mapboxsdk.module.loader.LibraryLoaderProviderImpl; +import com.mapbox.mapboxsdk.module.telemetry.TelemetryImpl; + +public class ModuleProviderImpl implements ModuleProvider { + + @Override + @NonNull + public HttpRequest createHttpRequest() { + return new HttpRequestImpl(); + } + + @Override + @Nullable + public TelemetryDefinition obtainTelemetry() { + return new TelemetryImpl(); + } + + @NonNull + @Override + public LibraryLoaderProvider createLibraryLoaderProvider() { + return new LibraryLoaderProviderImpl(); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Annotation.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Annotation.java new file mode 100644 index 000000000..8ea226649 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Annotation.java @@ -0,0 +1,153 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.support.annotation.NonNull; + +import android.support.annotation.Nullable; + +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; + +/** + * Annotation is an overlay on top of a Map. + *

+ * Known subclasses are {@link Polygon}, {@link Polyline} and {@link Marker}. + *

+ *

+ * This class manages attachment to a map and identification, but does not require + * content to be placed at a geographical point. + *

+ * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public abstract class Annotation implements Comparable { + + /** + *

+ * The annotation id + *

+ * Internal C++ id is stored as unsigned int. + */ + private long id = -1; // -1 unless added to a MapView + protected MapboxMap mapboxMap; + protected MapView mapView; + + protected Annotation() { + } + + /** + *

+ * Gets the annotation's unique ID. + *

+ * This ID is unique for a MapView instance and is suitable for associating your own extra + * data with. + * + * @return the assigned id. + */ + public long getId() { + return id; + } + + /** + * Do not use this method, used internally by the SDK. + */ + public void remove() { + if (mapboxMap == null) { + return; + } + mapboxMap.removeAnnotation(this); + } + + /** + * Do not use this method, used internally by the SDK. + * + * @param id the assigned id + */ + public void setId(long id) { + this.id = id; + } + + /** + * Do not use this method, used internally by the SDK. + * + * @param mapboxMap the hosting mapbox map + */ + public void setMapboxMap(MapboxMap mapboxMap) { + this.mapboxMap = mapboxMap; + } + + /** + * Gets the hosting mapbox map. + * + * @return the MapboxMap + */ + protected MapboxMap getMapboxMap() { + return mapboxMap; + } + + /** + * Do not use this method, used internally by the SDK. + * + * @param mapView the hosting map view + */ + public void setMapView(MapView mapView) { + this.mapView = mapView; + } + + /** + * Gets the hosting map view. + * + * @return The MapView + */ + protected MapView getMapView() { + return mapView; + } + + /** + * Compares this Annotation object with another Annotation. + * + * @param annotation Another Annotation to compare with this object. + * @return returns 0 if id's match, 1 if id is lower, -1 if id is higher of another Annotation + */ + @Override + public int compareTo(@NonNull Annotation annotation) { + if (id < annotation.getId()) { + return 1; + } else if (id > annotation.getId()) { + return -1; + } + return 0; + } + + /** + * Checks if this Annotation object is equal to another Annotation. + * + * @param object Another Annotation to check equality with this object. + * @return returns true both id's match else returns false. + */ + @Override + public boolean equals(@Nullable Object object) { + if (this == object) { + return true; + } + if (object == null || !(object instanceof Annotation)) { + return false; + } + Annotation that = (Annotation) object; + return id == that.getId(); + } + + /** + * Gives an integer which can be used as the bucket number for storing elements of the set/map. + * This bucket number is the address of the element inside the set/map. There's no guarantee + * that this hash value will be consistent between different Java implementations, or even + * between different execution runs of the same program. + * + * @return integer value you can use for storing element. + */ + @Override + public int hashCode() { + return (int) (getId() ^ (getId() >>> 32)); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/ArrowDirection.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/ArrowDirection.java new file mode 100644 index 000000000..220544815 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/ArrowDirection.java @@ -0,0 +1,31 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.support.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Deprecated +class ArrowDirection { + @IntDef( {LEFT, RIGHT, TOP, BOTTOM}) + @Retention(RetentionPolicy.SOURCE) + @interface Value { + } + + static final int LEFT = 0; + static final int RIGHT = 1; + static final int TOP = 2; + static final int BOTTOM = 3; + + @Value + private final int value; + + ArrowDirection(@Value int value) { + this.value = value; + } + + @Value + public int getValue() { + return value; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BaseMarkerOptions.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BaseMarkerOptions.java new file mode 100644 index 000000000..384ff8c0a --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BaseMarkerOptions.java @@ -0,0 +1,124 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.os.Parcelable; + +import com.mapbox.mapboxsdk.geometry.LatLng; + +/** + * Abstract builder class for composing custom Marker objects. + *

+ * Extending this class requires implementing Parceable interface. + * + * @param Type of the marker to be composed + * @param Type of the builder to be used for composing a custom Marker + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public abstract class BaseMarkerOptions> implements Parcelable { + + protected LatLng position; + protected String snippet; + protected String title; + protected Icon icon; + + /** + * Set the geographical location of the Marker. + * + * @param position the location to position the {@link Marker}. + * @return the object for which the method was called. + */ + public T position(LatLng position) { + this.position = position; + return getThis(); + } + + /** + * Set the snippet of the Marker. + * + * @param snippet the snippet of the {@link Marker}. + * @return the object for which the method was called. + */ + public T snippet(String snippet) { + this.snippet = snippet; + return getThis(); + } + + /** + * Set the title of the Marker. + * + * @param title the title of the {@link Marker}. + * @return the object for which the method was called. + */ + public T title(String title) { + this.title = title; + return getThis(); + } + + /** + * Set the icon of the Marker. + * + * @param icon the icon of the {@link Marker}. + * @return the object for which the method was called. + */ + public T icon(Icon icon) { + this.icon = icon; + return getThis(); + } + + /** + * Set the icon of the Marker. + * + * @param icon the icon of the {@link Marker}. + * @return the object for which the method was called. + */ + public T setIcon(Icon icon) { + return icon(icon); + } + + /** + * Set the geographical location of the Marker. + * + * @param position the location to position the {@link Marker}. + * @return the object for which the method was called. + */ + public T setPosition(LatLng position) { + return position(position); + } + + /** + * Set the snippet of the Marker. + * + * @param snippet the snippet of the {@link Marker}. + * @return the object for which the method was called. + */ + public T setSnippet(String snippet) { + return snippet(snippet); + } + + /** + * Set the title of the Marker. + * + * @param title the title of the {@link Marker}. + * @return the object for which the method was called. + */ + public T setTitle(String title) { + return title(title); + } + + /** + * Get the instance of the object for which this method was called. + * + * @return the object for which the this method was called. + */ + public abstract T getThis(); + + /** + * Get the Marker. + * + * @return the Marker created from this builder. + */ + public abstract U getMarker(); + +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BasePointCollection.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BasePointCollection.java new file mode 100644 index 000000000..b94721a80 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BasePointCollection.java @@ -0,0 +1,82 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.support.annotation.Keep; + +import android.support.annotation.NonNull; +import com.mapbox.mapboxsdk.geometry.LatLng; + +import java.util.ArrayList; +import java.util.List; + +/** + * Multipoint is an abstract annotation for combining geographical locations. + * + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public abstract class BasePointCollection extends Annotation { + + @Keep + private List points; + @Keep + private float alpha = 1.0f; + + protected BasePointCollection() { + super(); + points = new ArrayList<>(); + } + + /** + * Returns a copy of the points. + * + * @return A {@link List} of points. + */ + @NonNull + public List getPoints() { + return new ArrayList<>(points); + } + + /** + * Sets the points of this polyline. This method will take a copy of the points, so further + * mutations to points will have no effect on this polyline. + * + * @param points A {@link List} of {@link LatLng} points making up the polyline. + */ + public void setPoints(@NonNull List points) { + this.points = new ArrayList<>(points); + update(); + } + + /** + * Add a point to the polyline. + * + * @param point A {@link LatLng} point to be added. + */ + public void addPoint(LatLng point) { + points.add(point); + update(); + } + + /** + * Value between 0 and 1 defining the polyline alpha. + * + * @return float value between 0 and 1. + */ + public float getAlpha() { + return alpha; + } + + /** + * Set this {@link BasePointCollection}s alpha. + * + * @param alpha float value between 0 and 1. + */ + public void setAlpha(float alpha) { + this.alpha = alpha; + update(); + } + + abstract void update(); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Bubble.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Bubble.java new file mode 100644 index 000000000..0768f9c2e --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Bubble.java @@ -0,0 +1,315 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; + +@Deprecated +class Bubble extends Drawable { + + private RectF rect; + private float arrowWidth; + private float arrowHeight; + private float arrowPosition; + private float cornersRadius; + @NonNull + private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private float strokeWidth; + private Paint strokePaint; + private Path strokePath; + @NonNull + private Path path = new Path(); + + Bubble(@NonNull RectF rect, @NonNull ArrowDirection arrowDirection, float arrowWidth, float arrowHeight, + float arrowPosition, float cornersRadius, int bubbleColor, float strokeWidth, int strokeColor) { + this.rect = rect; + this.arrowWidth = arrowWidth; + this.arrowHeight = arrowHeight; + this.arrowPosition = arrowPosition; + this.cornersRadius = cornersRadius; + paint.setColor(bubbleColor); + this.strokeWidth = strokeWidth; + + if (strokeWidth > 0) { + strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + strokePaint.setColor(strokeColor); + strokePath = new Path(); + initPath(arrowDirection, path, strokeWidth); + initPath(arrowDirection, strokePath, 0); + } else { + initPath(arrowDirection, path, 0); + } + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + } + + @Override + public void draw(@NonNull Canvas canvas) { + if (strokeWidth > 0) { + canvas.drawPath(strokePath, strokePaint); + } + canvas.drawPath(path, paint); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + paint.setColorFilter(cf); + } + + @Override + public int getIntrinsicWidth() { + return (int) rect.width(); + } + + @Override + public int getIntrinsicHeight() { + return (int) rect.height(); + } + + private void initPath(@NonNull ArrowDirection arrowDirection, @NonNull Path path, float strokeWidth) { + switch (arrowDirection.getValue()) { + case ArrowDirection.LEFT: + if (cornersRadius <= 0) { + initLeftSquarePath(rect, path, strokeWidth); + break; + } + + if (strokeWidth > 0 && strokeWidth > cornersRadius) { + initLeftSquarePath(rect, path, strokeWidth); + break; + } + + initLeftRoundedPath(rect, path, strokeWidth); + break; + case ArrowDirection.TOP: + if (cornersRadius <= 0) { + initTopSquarePath(rect, path, strokeWidth); + break; + } + + if (strokeWidth > 0 && strokeWidth > cornersRadius) { + initTopSquarePath(rect, path, strokeWidth); + break; + } + + initTopRoundedPath(rect, path, strokeWidth); + break; + case ArrowDirection.RIGHT: + if (cornersRadius <= 0) { + initRightSquarePath(rect, path, strokeWidth); + break; + } + + if (strokeWidth > 0 && strokeWidth > cornersRadius) { + initRightSquarePath(rect, path, strokeWidth); + break; + } + + initRightRoundedPath(rect, path, strokeWidth); + break; + case ArrowDirection.BOTTOM: + if (cornersRadius <= 0) { + initBottomSquarePath(rect, path, strokeWidth); + break; + } + + if (strokeWidth > 0 && strokeWidth > cornersRadius) { + initBottomSquarePath(rect, path, strokeWidth); + break; + } + + initBottomRoundedPath(rect, path, strokeWidth); + break; + } + } + + private void initLeftSquarePath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) { + path.moveTo(arrowWidth + rect.left + strokeWidth, rect.top + strokeWidth); + path.lineTo(rect.width() - strokeWidth, rect.top + strokeWidth); + + path.lineTo(rect.right - strokeWidth, rect.bottom - strokeWidth); + + path.lineTo(rect.left + arrowWidth + strokeWidth, rect.bottom - strokeWidth); + + path.lineTo(rect.left + arrowWidth + strokeWidth, arrowHeight + arrowPosition - (strokeWidth / 2)); + path.lineTo(rect.left + strokeWidth + strokeWidth, arrowPosition + arrowHeight / 2); + path.lineTo(rect.left + arrowWidth + strokeWidth, arrowPosition + (strokeWidth / 2)); + + path.lineTo(rect.left + arrowWidth + strokeWidth, rect.top + strokeWidth); + + path.close(); + } + + private void initLeftRoundedPath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) { + path.moveTo(arrowWidth + rect.left + cornersRadius + strokeWidth, rect.top + strokeWidth); + path.lineTo(rect.width() - cornersRadius - strokeWidth, rect.top + strokeWidth); + path.arcTo(new RectF(rect.right - cornersRadius, rect.top + strokeWidth, rect.right - strokeWidth, + cornersRadius + rect.top), 270, 90); + + path.lineTo(rect.right - strokeWidth, rect.bottom - cornersRadius - strokeWidth); + path.arcTo(new RectF(rect.right - cornersRadius, rect.bottom - cornersRadius, + rect.right - strokeWidth, rect.bottom - strokeWidth), 0, 90); + + path.lineTo(rect.left + arrowWidth + cornersRadius + strokeWidth, rect.bottom - strokeWidth); + + path.arcTo(new RectF(rect.left + arrowWidth + strokeWidth, rect.bottom - cornersRadius, + cornersRadius + rect.left + arrowWidth, rect.bottom - strokeWidth), 90, 90); + + path.lineTo(rect.left + arrowWidth + strokeWidth, arrowHeight + arrowPosition - (strokeWidth / 2)); + + path.lineTo(rect.left + strokeWidth + strokeWidth, arrowPosition + arrowHeight / 2); + + path.lineTo(rect.left + arrowWidth + strokeWidth, arrowPosition + (strokeWidth / 2)); + + path.lineTo(rect.left + arrowWidth + strokeWidth, rect.top + cornersRadius + strokeWidth); + + path.arcTo(new RectF(rect.left + arrowWidth + strokeWidth, rect.top + strokeWidth, cornersRadius + + rect.left + arrowWidth, cornersRadius + rect.top), 180, 90); + + path.close(); + } + + private void initTopSquarePath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) { + path.moveTo(rect.left + arrowPosition + strokeWidth, rect.top + arrowHeight + strokeWidth); + + path.lineTo(rect.left + arrowPosition + (strokeWidth / 2), rect.top + arrowHeight + strokeWidth); + path.lineTo(rect.left + arrowWidth / 2 + arrowPosition, rect.top + strokeWidth + strokeWidth); + path.lineTo(rect.left + arrowWidth + arrowPosition - (strokeWidth / 2), rect.top + arrowHeight + strokeWidth); + path.lineTo(rect.right - strokeWidth, rect.top + arrowHeight + strokeWidth); + + path.lineTo(rect.right - strokeWidth, rect.bottom - strokeWidth); + + path.lineTo(rect.left + strokeWidth, rect.bottom - strokeWidth); + + path.lineTo(rect.left + strokeWidth, rect.top + arrowHeight + strokeWidth); + + path.lineTo(rect.left + arrowPosition + strokeWidth, rect.top + arrowHeight + strokeWidth); + + path.close(); + } + + private void initTopRoundedPath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) { + path.moveTo(rect.left + Math.min(arrowPosition, cornersRadius) + strokeWidth, rect.top + arrowHeight + + strokeWidth); + path.lineTo(rect.left + arrowPosition + (strokeWidth / 2), rect.top + arrowHeight + strokeWidth); + path.lineTo(rect.left + arrowWidth / 2 + arrowPosition, rect.top + strokeWidth + strokeWidth); + path.lineTo(rect.left + arrowWidth + arrowPosition - (strokeWidth / 2), rect.top + arrowHeight + strokeWidth); + path.lineTo(rect.right - cornersRadius - strokeWidth, rect.top + arrowHeight + strokeWidth); + + path.arcTo(new RectF(rect.right - cornersRadius, + rect.top + arrowHeight + strokeWidth, rect.right - strokeWidth, cornersRadius + rect.top + arrowHeight), + 270, 90); + path.lineTo(rect.right - strokeWidth, rect.bottom - cornersRadius - strokeWidth); + + path.arcTo(new RectF(rect.right - cornersRadius, rect.bottom - cornersRadius, + rect.right - strokeWidth, rect.bottom - strokeWidth), 0, 90); + path.lineTo(rect.left + cornersRadius + strokeWidth, rect.bottom - strokeWidth); + + path.arcTo(new RectF(rect.left + strokeWidth, rect.bottom - cornersRadius, + cornersRadius + rect.left, rect.bottom - strokeWidth), 90, 90); + + path.lineTo(rect.left + strokeWidth, rect.top + arrowHeight + cornersRadius + strokeWidth); + + path.arcTo(new RectF(rect.left + strokeWidth, rect.top + arrowHeight + strokeWidth, cornersRadius + + rect.left, cornersRadius + rect.top + arrowHeight), 180, 90); + + path.close(); + } + + private void initRightSquarePath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) { + path.moveTo(rect.left + strokeWidth, rect.top + strokeWidth); + path.lineTo(rect.width() - arrowWidth - strokeWidth, rect.top + strokeWidth); + + path.lineTo(rect.right - arrowWidth - strokeWidth, arrowPosition + (strokeWidth / 2)); + path.lineTo(rect.right - strokeWidth - strokeWidth, arrowPosition + arrowHeight / 2); + path.lineTo(rect.right - arrowWidth - strokeWidth, arrowPosition + arrowHeight - (strokeWidth / 2)); + + path.lineTo(rect.right - arrowWidth - strokeWidth, rect.bottom - strokeWidth); + + path.lineTo(rect.left + strokeWidth, rect.bottom - strokeWidth); + path.lineTo(rect.left + strokeWidth, rect.top + strokeWidth); + + path.close(); + } + + private void initRightRoundedPath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) { + path.moveTo(rect.left + cornersRadius + strokeWidth, rect.top + strokeWidth); + path.lineTo(rect.width() - cornersRadius - arrowWidth - strokeWidth, rect.top + strokeWidth); + path.arcTo(new RectF(rect.right - cornersRadius - arrowWidth, + rect.top + strokeWidth, rect.right - arrowWidth - strokeWidth, cornersRadius + rect.top), 270, 90); + + path.lineTo(rect.right - arrowWidth - strokeWidth, arrowPosition + (strokeWidth / 2)); + path.lineTo(rect.right - strokeWidth - strokeWidth, arrowPosition + arrowHeight / 2); + path.lineTo(rect.right - arrowWidth - strokeWidth, arrowPosition + arrowHeight - (strokeWidth / 2)); + path.lineTo(rect.right - arrowWidth - strokeWidth, rect.bottom - cornersRadius - strokeWidth); + + path.arcTo(new RectF(rect.right - cornersRadius - arrowWidth, rect.bottom - cornersRadius, + rect.right - arrowWidth - strokeWidth, rect.bottom - strokeWidth), 0, 90); + path.lineTo(rect.left + arrowWidth + strokeWidth, rect.bottom - strokeWidth); + + path.arcTo(new RectF(rect.left + strokeWidth, rect.bottom - cornersRadius, + cornersRadius + rect.left, rect.bottom - strokeWidth), 90, 90); + + path.arcTo(new RectF(rect.left + strokeWidth, rect.top + strokeWidth, cornersRadius + + rect.left, cornersRadius + rect.top), 180, 90); + path.close(); + } + + private void initBottomSquarePath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) { + path.moveTo(rect.left + strokeWidth, rect.top + strokeWidth); + path.lineTo(rect.right - strokeWidth, rect.top + strokeWidth); + path.lineTo(rect.right - strokeWidth, rect.bottom - arrowHeight - strokeWidth); + + path.lineTo(rect.left + arrowWidth + arrowPosition - (strokeWidth / 2), rect.bottom - arrowHeight - strokeWidth); + path.lineTo(rect.left + arrowPosition + arrowWidth / 2, rect.bottom - strokeWidth - strokeWidth); + path.lineTo(rect.left + arrowPosition + (strokeWidth / 2), rect.bottom - arrowHeight - strokeWidth); + path.lineTo(rect.left + arrowPosition + strokeWidth, rect.bottom - arrowHeight - strokeWidth); + + path.lineTo(rect.left + strokeWidth, rect.bottom - arrowHeight - strokeWidth); + path.lineTo(rect.left + strokeWidth, rect.top + strokeWidth); + path.close(); + } + + private void initBottomRoundedPath(@NonNull RectF rect, @NonNull Path path, float strokeWidth) { + path.moveTo(rect.left + cornersRadius + strokeWidth, rect.top + strokeWidth); + path.lineTo(rect.width() - cornersRadius - strokeWidth, rect.top + strokeWidth); + path.arcTo(new RectF(rect.right - cornersRadius, + rect.top + strokeWidth, rect.right - strokeWidth, cornersRadius + rect.top), 270, 90); + + path.lineTo(rect.right - strokeWidth, rect.bottom - arrowHeight - cornersRadius - strokeWidth); + path.arcTo(new RectF(rect.right - cornersRadius, rect.bottom - cornersRadius - arrowHeight, + rect.right - strokeWidth, rect.bottom - arrowHeight - strokeWidth), 0, 90); + + path.lineTo(rect.left + arrowWidth + arrowPosition - (strokeWidth / 2), rect.bottom - arrowHeight - strokeWidth); + path.lineTo(rect.left + arrowPosition + arrowWidth / 2, rect.bottom - strokeWidth - strokeWidth); + path.lineTo(rect.left + arrowPosition + (strokeWidth / 2), rect.bottom - arrowHeight - strokeWidth); + path.lineTo(rect.left + Math.min(cornersRadius, arrowPosition) + strokeWidth, rect.bottom - arrowHeight + - strokeWidth); + + path.arcTo(new RectF(rect.left + strokeWidth, rect.bottom - cornersRadius - arrowHeight, + cornersRadius + rect.left, rect.bottom - arrowHeight - strokeWidth), 90, 90); + path.lineTo(rect.left + strokeWidth, rect.top + cornersRadius + strokeWidth); + path.arcTo(new RectF(rect.left + strokeWidth, rect.top + strokeWidth, cornersRadius + + rect.left, cornersRadius + rect.top), 180, 90); + path.close(); + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BubbleLayout.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BubbleLayout.java new file mode 100644 index 000000000..f4a1484f8 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BubbleLayout.java @@ -0,0 +1,350 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.widget.LinearLayout; + +import com.mapbox.mapboxsdk.R; + +/** + * Bubble View for Android with custom stroke width and color, arrow size, position and direction. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public class BubbleLayout extends LinearLayout { + + public static final float DEFAULT_STROKE_WIDTH = -1; + private ArrowDirection arrowDirection; + private float arrowWidth; + private float arrowHeight; + private float arrowPosition; + private float cornersRadius; + private Bubble bubble; + private int bubbleColor; + private float strokeWidth; + private int strokeColor; + + /** + * Creates an instance of bubble layout. + * + * @param context The context used to inflate this bubble layout + */ + public BubbleLayout(@NonNull Context context) { + this(context, null, 0); + } + + /** + * Creates an instance of bubble layout. + * + * @param context The context used to inflate this bubble layout + * @param attrs The attribute set to initialise this bubble layout from + */ + public BubbleLayout(@NonNull Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + /** + * Creates an instance of bubble layout. + * + * @param context The context used to inflate this bubble layout + * @param attrs The attribute set to initialise this bubble layout from + * @param defStyleAttr The default style to apply this bubble layout with + */ + public BubbleLayout(@NonNull Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.mapbox_BubbleLayout); + @ArrowDirection.Value + int location = a.getInt(R.styleable.mapbox_BubbleLayout_mapbox_bl_arrowDirection, + ArrowDirection.LEFT); + arrowDirection = new ArrowDirection(location); + arrowWidth = a.getDimension(R.styleable.mapbox_BubbleLayout_mapbox_bl_arrowWidth, + convertDpToPixel(8, context)); + arrowHeight = a.getDimension(R.styleable.mapbox_BubbleLayout_mapbox_bl_arrowHeight, + convertDpToPixel(8, context)); + arrowPosition = a.getDimension(R.styleable.mapbox_BubbleLayout_mapbox_bl_arrowPosition, + convertDpToPixel(12, context)); + cornersRadius = a.getDimension(R.styleable.mapbox_BubbleLayout_mapbox_bl_cornersRadius, 0); + bubbleColor = a.getColor(R.styleable.mapbox_BubbleLayout_mapbox_bl_bubbleColor, Color.WHITE); + strokeWidth = + a.getDimension(R.styleable.mapbox_BubbleLayout_mapbox_bl_strokeWidth, DEFAULT_STROKE_WIDTH); + strokeColor = a.getColor(R.styleable.mapbox_BubbleLayout_mapbox_bl_strokeColor, Color.GRAY); + + a.recycle(); + initPadding(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + initDrawable(0, getWidth(), 0, getHeight()); + } + + @Override + protected void dispatchDraw(@NonNull Canvas canvas) { + if (bubble != null) { + bubble.draw(canvas); + } + super.dispatchDraw(canvas); + } + + static float convertDpToPixel(float dp, Context context) { + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + return dp * (metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT); + } + + /** + * Get the arrow direction. + * + * @return the arrow direction + */ + public ArrowDirection getArrowDirection() { + return arrowDirection; + } + + /** + * Set the arrow direction. + * + * @param arrowDirection The direction of the arrow + * @return this + */ + @NonNull + public BubbleLayout setArrowDirection(ArrowDirection arrowDirection) { + resetPadding(); + this.arrowDirection = arrowDirection; + initPadding(); + return this; + } + + /** + * Get the arrow width. + * + * @return the width of the arrow + */ + public float getArrowWidth() { + return arrowWidth; + } + + /** + * Set the arrow width. + * + * @param arrowWidth The width of the arrow + * @return this + */ + @NonNull + public BubbleLayout setArrowWidth(float arrowWidth) { + resetPadding(); + this.arrowWidth = arrowWidth; + initPadding(); + return this; + } + + /** + * Get the arrow height + * + * @return the height of the arrow + */ + public float getArrowHeight() { + return arrowHeight; + } + + /** + * Set the arrow height. + * + * @param arrowHeight The height of the arrow + * @return this + */ + @NonNull + public BubbleLayout setArrowHeight(float arrowHeight) { + resetPadding(); + this.arrowHeight = arrowHeight; + initPadding(); + return this; + } + + /** + * Get the arrow position. + * + * @return the arrow position + */ + public float getArrowPosition() { + return arrowPosition; + } + + /** + * Get the arrow position. + * + * @param arrowPosition The arrow position + * @return this + */ + @NonNull + public BubbleLayout setArrowPosition(float arrowPosition) { + resetPadding(); + this.arrowPosition = arrowPosition; + initPadding(); + return this; + } + + /** + * Get the corner radius + * + * @return the corner radius + */ + public float getCornersRadius() { + return cornersRadius; + } + + /** + * Set the corner radius + * + * @param cornersRadius The corner radius + * @return this + */ + @NonNull + public BubbleLayout setCornersRadius(float cornersRadius) { + this.cornersRadius = cornersRadius; + requestLayout(); + return this; + } + + /** + * Get the bubble color. + * + * @return the bubble color + */ + public int getBubbleColor() { + return bubbleColor; + } + + /** + * Set the bubble color. + * + * @param bubbleColor The buble color + * @return this + */ + @NonNull + public BubbleLayout setBubbleColor(int bubbleColor) { + this.bubbleColor = bubbleColor; + requestLayout(); + return this; + } + + /** + * Get stroke width. + * + * @return the stroke width + */ + public float getStrokeWidth() { + return strokeWidth; + } + + /** + * Set the stroke width. + * + * @param strokeWidth The stroke width + * @return this + */ + @NonNull + public BubbleLayout setStrokeWidth(float strokeWidth) { + resetPadding(); + this.strokeWidth = strokeWidth; + initPadding(); + return this; + } + + /** + * Get the stroke color. + * + * @return the stroke color + */ + public int getStrokeColor() { + return strokeColor; + } + + /** + * Set the stroke color. + * + * @param strokeColor The stroke color + * @return this + */ + @NonNull + public BubbleLayout setStrokeColor(int strokeColor) { + this.strokeColor = strokeColor; + requestLayout(); + return this; + } + + private void initPadding() { + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + switch (arrowDirection.getValue()) { + case ArrowDirection.LEFT: + paddingLeft += arrowWidth; + break; + case ArrowDirection.RIGHT: + paddingRight += arrowWidth; + break; + case ArrowDirection.TOP: + paddingTop += arrowHeight; + break; + case ArrowDirection.BOTTOM: + paddingBottom += arrowHeight; + break; + } + if (strokeWidth > 0) { + paddingLeft += strokeWidth; + paddingRight += strokeWidth; + paddingTop += strokeWidth; + paddingBottom += strokeWidth; + } + setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); + } + + private void initDrawable(int left, int right, int top, int bottom) { + if (right < left || bottom < top) { + return; + } + + RectF rectF = new RectF(left, top, right, bottom); + bubble = new Bubble(rectF, arrowDirection, arrowWidth, arrowHeight, arrowPosition, cornersRadius, + bubbleColor, strokeWidth, strokeColor); + } + + private void resetPadding() { + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + switch (arrowDirection.getValue()) { + case ArrowDirection.LEFT: + paddingLeft -= arrowWidth; + break; + case ArrowDirection.RIGHT: + paddingRight -= arrowWidth; + break; + case ArrowDirection.TOP: + paddingTop -= arrowHeight; + break; + case ArrowDirection.BOTTOM: + paddingBottom -= arrowHeight; + break; + } + if (strokeWidth > 0) { + paddingLeft -= strokeWidth; + paddingRight -= strokeWidth; + paddingTop -= strokeWidth; + paddingBottom -= strokeWidth; + } + setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BubblePopupHelper.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BubblePopupHelper.java new file mode 100644 index 000000000..055bfe536 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/BubblePopupHelper.java @@ -0,0 +1,35 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.view.ViewGroup; +import android.widget.PopupWindow; + +import com.mapbox.mapboxsdk.R; + +@Deprecated +class BubblePopupHelper { + + @NonNull + static PopupWindow create(@NonNull Context context, @NonNull BubbleLayout bubbleLayout) { + PopupWindow popupWindow = new PopupWindow(context); + + popupWindow.setContentView(bubbleLayout); + popupWindow.setOutsideTouchable(true); + popupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + popupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + popupWindow.setAnimationStyle(android.R.style.Animation_Dialog); + // change background color to transparent + Drawable drawable; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + drawable = context.getDrawable(R.drawable.mapbox_popup_window_transparent); + } else { + drawable = context.getResources().getDrawable(R.drawable.mapbox_popup_window_transparent); + } + popupWindow.setBackgroundDrawable(drawable); + + return popupWindow; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Icon.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Icon.java new file mode 100644 index 000000000..7ace11350 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Icon.java @@ -0,0 +1,124 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.graphics.Bitmap; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.DisplayMetrics; + +import java.nio.ByteBuffer; + +/** + * Icon is the visual representation of a Marker on a MapView. + * + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public class Icon { + + private Bitmap mBitmap; + private String mId; + + Icon(String id, Bitmap bitmap) { + mId = id; + mBitmap = bitmap; + } + + /** + * String identifier for this icon. + * + * @return String identifier for this icon. + */ + public String getId() { + return mId; + } + + /** + * Get the bitmap being used for this icon. + * + * @return The bitmap being used for the icon. + */ + public Bitmap getBitmap() { + if (mBitmap != null && mBitmap.getConfig() != Bitmap.Config.ARGB_8888) { + mBitmap = mBitmap.copy(Bitmap.Config.ARGB_8888, false); + } + return mBitmap; + } + + /** + * Get the icon bitmap scale. + *

+ * Requires the bitmap to be set before calling this method. + *

+ * + * @return the scale of the bitmap + */ + public float getScale() { + if (mBitmap == null) { + throw new IllegalStateException("Required to set a Icon before calling getScale"); + } + float density = mBitmap.getDensity(); + if (density == Bitmap.DENSITY_NONE) { + density = DisplayMetrics.DENSITY_DEFAULT; + } + return density / DisplayMetrics.DENSITY_DEFAULT; + } + + /** + * Get the icon bitmap bytes. + *

+ * Requires the bitmap to be set before calling this method. + *

+ * + * @return the bytes of the bitmap + */ + @NonNull + public byte[] toBytes() { + if (mBitmap == null) { + throw new IllegalStateException("Required to set a Icon before calling toBytes"); + } + ByteBuffer buffer = ByteBuffer.allocate(mBitmap.getRowBytes() * mBitmap.getHeight()); + mBitmap.copyPixelsToBuffer(buffer); + return buffer.array(); + } + + /** + * Compares this icon object with another icon and determines if they match. + * + * @param object Another iconi to compare with this object. + * @return True if the icon being passed in matches this icon object. Else, false. + */ + @Override + public boolean equals(@Nullable Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + + Icon icon = (Icon) object; + return mBitmap.equals(icon.mBitmap) && mId.equals(icon.mId); + } + + /** + * Gives an integer which can be used as the bucket number for storing elements of the set/map. + * This bucket number is the address of the element inside the set/map. There's no guarantee + * that this hash value will be consistent between different Java implementations, or even + * between different execution runs of the same program. + * + * @return integer value you can use for storing element. + */ + @Override + public int hashCode() { + int result = 0; + if (mBitmap != null) { + result = mBitmap.hashCode(); + } + if (mId != null) { + result = 31 * result + mId.hashCode(); + } + return result; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/IconFactory.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/IconFactory.java new file mode 100644 index 000000000..9f0fa62cd --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/IconFactory.java @@ -0,0 +1,188 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.util.DisplayMetrics; +import android.view.WindowManager; + +import com.mapbox.mapboxsdk.MapStrictMode; +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.exceptions.TooManyIconsException; +import com.mapbox.mapboxsdk.utils.BitmapUtils; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * Factory for creating Icons from bitmap images. + *

+ * icon is used to display bitmaps on top of the map using {@link Marker}. + *

+ * + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public final class IconFactory { + + private static final String ICON_ID_PREFIX = "com.mapbox.icons.icon_"; + + private Context context; + @SuppressLint("StaticFieldLeak") + private static IconFactory instance; + private Icon defaultMarker; + private BitmapFactory.Options options; + + private int nextId = 0; + + /** + * Get a single instance of IconFactory. + * + * @param context the context to derive the application context from + * @return the single instance of IconFactory + */ + public static synchronized IconFactory getInstance(@NonNull Context context) { + if (instance == null) { + instance = new IconFactory(context.getApplicationContext()); + } + return instance; + } + + private IconFactory(@NonNull Context context) { + this.context = context; + DisplayMetrics realMetrics = null; + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + realMetrics = new DisplayMetrics(); + wm.getDefaultDisplay().getRealMetrics(realMetrics); + } + wm.getDefaultDisplay().getMetrics(metrics); + + options = new BitmapFactory.Options(); + options.inScaled = true; + options.inDensity = DisplayMetrics.DENSITY_DEFAULT; + options.inTargetDensity = metrics.densityDpi; + if (realMetrics != null) { + options.inScreenDensity = realMetrics.densityDpi; + } + } + + /** + * Creates an icon from a given Bitmap image. + * + * @param bitmap image used for creating the Icon. + * @return The icon using the given Bitmap image. + */ + public Icon fromBitmap(@NonNull Bitmap bitmap) { + if (nextId < 0) { + throw new TooManyIconsException(); + } + String id = ICON_ID_PREFIX + ++nextId; + return new Icon(id, bitmap); + } + + /** + * Creates an icon using the resource ID of a Bitmap image. + * + * @param resourceId The resource ID of a Bitmap image. + * @return The icon that was loaded from the asset or {@code null} if failed to load. + */ + public Icon fromResource(@DrawableRes int resourceId) { + Drawable drawable = BitmapUtils.getDrawableFromRes(context, resourceId); + if (drawable instanceof BitmapDrawable) { + BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; + return fromBitmap(bitmapDrawable.getBitmap()); + } else { + throw new IllegalArgumentException("Failed to decode image. The resource provided must be a Bitmap."); + } + } + + /** + * Provides an icon using the default marker icon used for {@link Marker}. + * + * @return An icon with the default {@link Marker} icon. + */ + public Icon defaultMarker() { + if (defaultMarker == null) { + defaultMarker = fromResource(R.drawable.mapbox_marker_icon_default); + } + return defaultMarker; + } + + private Icon fromInputStream(@NonNull InputStream is) { + Bitmap bitmap = BitmapFactory.decodeStream(is, null, options); + return fromBitmap(bitmap); + } + + /** + * Creates an Icon using the name of a Bitmap image in the assets directory. + * + * @param assetName The name of a Bitmap image in the assets directory. + * @return The Icon that was loaded from the asset or null if failed to load. + */ + public Icon fromAsset(@NonNull String assetName) { + InputStream is; + try { + is = context.getAssets().open(assetName); + } catch (IOException ioException) { + MapStrictMode.strictModeViolation(ioException); + return null; + } + return fromInputStream(is); + } + + /** + * Creates an Icon using the absolute file path of a Bitmap image. + * + * @param absolutePath The absolute path of the Bitmap image. + * @return The Icon that was loaded from the absolute path or null if failed to load. + */ + public Icon fromPath(@NonNull String absolutePath) { + Bitmap bitmap = BitmapFactory.decodeFile(absolutePath, options); + return fromBitmap(bitmap); + } + + /** + * Create an Icon using the name of a Bitmap image file located in the internal storage. + * In particular, this calls Context#openFileInput(String). + * + * @param fileName The name of the Bitmap image file. + * @return The Icon that was loaded from the asset or null if failed to load. + * @see + * Using the Internal Storage + */ + public Icon fromFile(@NonNull String fileName) { + FileInputStream is; + try { + is = context.openFileInput(fileName); + } catch (FileNotFoundException fileNotFoundException) { + MapStrictMode.strictModeViolation(fileNotFoundException); + return null; + } + return fromInputStream(is); + } + + /** + * Create an Icon using a previously created icon identifier along with a provided Bitmap. + * + * @param iconId The Icon identifier you'd like to recreate. + * @param bitmap a Bitmap used to replace the current one. + * @return The Icon using the new Bitmap. + */ + public static Icon recreate(@NonNull String iconId, @NonNull Bitmap bitmap) { + return new Icon(iconId, bitmap); + } + +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindow.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindow.java new file mode 100644 index 000000000..5bd9aaef1 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/InfoWindow.java @@ -0,0 +1,351 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.content.res.Resources; +import android.graphics.PointF; +import android.os.Build; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.TextView; + +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; + +import java.lang.ref.WeakReference; + +/** + * {@code InfoWindow} is a tooltip shown when a {@link Marker} is tapped. Only + * one info window is displayed at a time. When the user clicks on a marker, the currently open info + * window will be closed and the new info window will be displayed. If the user clicks the same + * marker while its info window is currently open, the info window will be closed. + *

+ * The info window is drawn oriented against the device's screen, centered above its associated + * marker by default. The default info window contains the title in bold and snippet text below the title. + * While either the title and + * snippet are optional, at least one is required to open the info window. + *

+ * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public class InfoWindow { + + private WeakReference boundMarker; + private WeakReference mapboxMap; + protected WeakReference view; + + private float markerHeightOffset; + private float markerWidthOffset; + private float viewWidthOffset; + private float viewHeightOffset; + private PointF coordinates; + private boolean isVisible; + + @LayoutRes + private int layoutRes; + + InfoWindow(MapView mapView, int layoutResId, MapboxMap mapboxMap) { + layoutRes = layoutResId; + View view = LayoutInflater.from(mapView.getContext()).inflate(layoutResId, mapView, false); + initialize(view, mapboxMap); + } + + InfoWindow(@NonNull View view, MapboxMap mapboxMap) { + initialize(view, mapboxMap); + } + + private void initialize(@NonNull View view, MapboxMap mapboxMap) { + this.mapboxMap = new WeakReference<>(mapboxMap); + isVisible = false; + this.view = new WeakReference<>(view); + + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + MapboxMap mapboxMap = InfoWindow.this.mapboxMap.get(); + if (mapboxMap != null) { + MapboxMap.OnInfoWindowClickListener onInfoWindowClickListener = mapboxMap.getOnInfoWindowClickListener(); + boolean handledDefaultClick = false; + if (onInfoWindowClickListener != null) { + handledDefaultClick = onInfoWindowClickListener.onInfoWindowClick(getBoundMarker()); + } + + if (!handledDefaultClick) { + // default behavior: close it when clicking on the tooltip: + closeInfoWindow(); + } + } + } + }); + + view.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + MapboxMap mapboxMap = InfoWindow.this.mapboxMap.get(); + if (mapboxMap != null) { + MapboxMap.OnInfoWindowLongClickListener listener = mapboxMap.getOnInfoWindowLongClickListener(); + if (listener != null) { + listener.onInfoWindowLongClick(getBoundMarker()); + } + } + return true; + } + }); + } + + private void closeInfoWindow() { + MapboxMap mapbox = mapboxMap.get(); + Marker marker = boundMarker.get(); + if (marker != null && mapbox != null) { + mapbox.deselectMarker(marker); + } + close(); + } + + /** + * Open the info window at the specified position. + * + * @param boundMarker The marker on which is hooked the view. + * @param position to place the window on the map. + * @param offsetX The offset of the view to the position, in pixels. This allows to offset + * the view from the object position. + * @param offsetY The offset of the view to the position, in pixels. This allows to offset + * the view from the object position. + * @return this {@link InfoWindow}. + */ + @NonNull + InfoWindow open(@NonNull MapView mapView, Marker boundMarker, @NonNull LatLng position, int offsetX, int offsetY) { + setBoundMarker(boundMarker); + + MapView.LayoutParams lp = new MapView.LayoutParams(MapView.LayoutParams.WRAP_CONTENT, + MapView.LayoutParams.WRAP_CONTENT); + + MapboxMap mapboxMap = this.mapboxMap.get(); + View view = this.view.get(); + if (view != null && mapboxMap != null) { + view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + + markerHeightOffset = offsetY; + markerWidthOffset = -offsetX; + + // Calculate default Android x,y coordinate + coordinates = mapboxMap.getProjection().toScreenLocation(position); + float x = coordinates.x - (view.getMeasuredWidth() / 2) + offsetX; + float y = coordinates.y - view.getMeasuredHeight() + offsetY; + + if (view instanceof BubbleLayout) { + // only apply repositioning/margin for InfoWindowView + Resources resources = mapView.getContext().getResources(); + + // get right/left popup window + float rightSideInfowWindow = x + view.getMeasuredWidth(); + float leftSideInfoWindow = x; + + // get right/left map view + final float mapRight = mapView.getRight(); + final float mapLeft = mapView.getLeft(); + + float marginHorizontal = resources.getDimension(R.dimen.mapbox_infowindow_margin); + float tipViewOffset = resources.getDimension(R.dimen.mapbox_infowindow_tipview_width) / 2; + float tipViewMarginLeft = view.getMeasuredWidth() / 2 - tipViewOffset; + + boolean outOfBoundsLeft = false; + boolean outOfBoundsRight = false; + + // only optimise margins if view is inside current viewport + if (coordinates.x >= 0 && coordinates.x <= mapView.getWidth() + && coordinates.y >= 0 && coordinates.y <= mapView.getHeight()) { + + // if out of bounds right + if (rightSideInfowWindow > mapRight) { + outOfBoundsRight = true; + x -= rightSideInfowWindow - mapRight; + tipViewMarginLeft += rightSideInfowWindow - mapRight + tipViewOffset; + rightSideInfowWindow = x + view.getMeasuredWidth(); + } + + // fit screen left + if (leftSideInfoWindow < mapLeft) { + outOfBoundsLeft = true; + x += mapLeft - leftSideInfoWindow; + tipViewMarginLeft -= mapLeft - leftSideInfoWindow + tipViewOffset; + leftSideInfoWindow = x; + } + + // Add margin right + if (outOfBoundsRight && mapRight - rightSideInfowWindow < marginHorizontal) { + x -= marginHorizontal - (mapRight - rightSideInfowWindow); + tipViewMarginLeft += marginHorizontal - (mapRight - rightSideInfowWindow) - tipViewOffset; + leftSideInfoWindow = x; + } + + // Add margin left + if (outOfBoundsLeft && leftSideInfoWindow - mapLeft < marginHorizontal) { + x += marginHorizontal - (leftSideInfoWindow - mapLeft); + tipViewMarginLeft -= (marginHorizontal - (leftSideInfoWindow - mapLeft)) - tipViewOffset; + } + } + + // Adjust tipView + ((BubbleLayout) view).setArrowPosition(tipViewMarginLeft); + } + + // set anchor popupwindowview + view.setX(x); + view.setY(y); + + // Calculate x-offset and y-offset for update method + viewWidthOffset = x - coordinates.x - offsetX; + viewHeightOffset = -view.getMeasuredHeight() + offsetY; + + + close(); // if it was already opened + mapView.addView(view, lp); + isVisible = true; + } + return this; + } + + /** + * Close this {@link InfoWindow} if it is visible, otherwise calling this will do nothing. + * + * @return This {@link InfoWindow} + */ + @NonNull + InfoWindow close() { + MapboxMap mapboxMap = this.mapboxMap.get(); + if (isVisible && mapboxMap != null) { + isVisible = false; + View view = this.view.get(); + if (view != null && view.getParent() != null) { + ((ViewGroup) view.getParent()).removeView(view); + } + + Marker marker = getBoundMarker(); + MapboxMap.OnInfoWindowCloseListener listener = mapboxMap.getOnInfoWindowCloseListener(); + if (listener != null) { + listener.onInfoWindowClose(marker); + } + + setBoundMarker(null); + } + return this; + } + + /** + * Constructs the view that is displayed when the InfoWindow opens. This retrieves data from + * overlayItem and shows it in the tooltip. + * + * @param overlayItem the tapped overlay item + */ + void adaptDefaultMarker(@NonNull Marker overlayItem, MapboxMap mapboxMap, @NonNull MapView mapView) { + View view = this.view.get(); + if (view == null) { + view = LayoutInflater.from(mapView.getContext()).inflate(layoutRes, mapView, false); + initialize(view, mapboxMap); + } + this.mapboxMap = new WeakReference<>(mapboxMap); + String title = overlayItem.getTitle(); + TextView titleTextView = ((TextView) view.findViewById(R.id.infowindow_title)); + if (!TextUtils.isEmpty(title)) { + titleTextView.setText(title); + titleTextView.setVisibility(View.VISIBLE); + } else { + titleTextView.setVisibility(View.GONE); + } + + String snippet = overlayItem.getSnippet(); + TextView snippetTextView = ((TextView) view.findViewById(R.id.infowindow_description)); + if (!TextUtils.isEmpty(snippet)) { + snippetTextView.setText(snippet); + snippetTextView.setVisibility(View.VISIBLE); + } else { + snippetTextView.setVisibility(View.GONE); + } + } + + @NonNull + InfoWindow setBoundMarker(Marker boundMarker) { + this.boundMarker = new WeakReference<>(boundMarker); + return this; + } + + @Nullable + Marker getBoundMarker() { + if (boundMarker == null) { + return null; + } + return boundMarker.get(); + } + + /** + * Will result in getting this {@link InfoWindow} and updating the view being displayed. + */ + public void update() { + MapboxMap mapboxMap = this.mapboxMap.get(); + Marker marker = boundMarker.get(); + View view = this.view.get(); + if (mapboxMap != null && marker != null && view != null) { + coordinates = mapboxMap.getProjection().toScreenLocation(marker.getPosition()); + + if (view instanceof BubbleLayout) { + view.setX(coordinates.x + viewWidthOffset - markerWidthOffset); + } else { + view.setX(coordinates.x - (view.getMeasuredWidth() / 2) - markerWidthOffset); + } + view.setY(coordinates.y + viewHeightOffset); + } + } + + void onContentUpdate() { + //recalculate y-offset and update position + View view = this.view.get(); + if (view != null) { + ViewTreeObserver viewTreeObserver = view.getViewTreeObserver(); + if (viewTreeObserver.isAlive()) { + viewTreeObserver.addOnGlobalLayoutListener(contentUpdateListener); + } + } + } + + @Nullable + private final ViewTreeObserver.OnGlobalLayoutListener contentUpdateListener = + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + View view = InfoWindow.this.view.get(); + if (view != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + view.getViewTreeObserver().removeOnGlobalLayoutListener(this); + } else { + view.getViewTreeObserver().removeGlobalOnLayoutListener(this); + } + viewHeightOffset = -view.getMeasuredHeight() + markerHeightOffset; + update(); + } + } + }; + + /** + * Retrieve this {@link InfoWindow}'s current view being used. + * + * @return This {@link InfoWindow}'s current View. + */ + @Nullable + public View getView() { + return view != null ? view.get() : null; + } + + boolean isVisible() { + return isVisible; + } + +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Marker.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Marker.java new file mode 100644 index 000000000..c26af9436 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Marker.java @@ -0,0 +1,278 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.support.annotation.Keep; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; + +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; + +/** + * Marker is an annotation that shows an icon image at a geographical location. The default marker + * uses a provided icon. This icon can be customized using {@link IconFactory} to generate an + * {@link Icon} using a provided image. Markers are added to the map by first giving a + * {@link LatLng} and using {@link MapboxMap#addMarker(MarkerOptions)}. The marker icon will be + * centered at this position so it is common to add padding to the icon image before usage. + *

+ * Markers are designed to be interactive. They receive click events by default, and are often used + * with event listeners to bring up info windows. An {@link InfoWindow} is displayed by default when + * either a title or snippet is provided. + *

+ * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public class Marker extends Annotation { + + @Keep + private LatLng position; + private String snippet; + @Nullable + private Icon icon; + //Redundantly stored for JNI access + @Nullable + @Keep + private String iconId; + private String title; + + @Nullable + private InfoWindow infoWindow; + private boolean infoWindowShown; + + private int topOffsetPixels; + private int rightOffsetPixels; + + /** + * Constructor + */ + Marker() { + super(); + } + + /** + * Creates a instance of {@link Marker} using the builder of Marker. + * + * @param baseMarkerOptions The builder used to construct the Marker. + */ + public Marker(BaseMarkerOptions baseMarkerOptions) { + this(baseMarkerOptions.position, baseMarkerOptions.icon, baseMarkerOptions.title, baseMarkerOptions.snippet); + } + + Marker(LatLng position, Icon icon, String title, String snippet) { + this.position = position; + this.title = title; + this.snippet = snippet; + setIcon(icon); + } + + /** + * Returns the position of the marker. + * + * @return A {@link LatLng} object specifying the marker's current position. + */ + public LatLng getPosition() { + return position; + } + + /** + * Gets the snippet of the marker. + * + * @return A string containing the marker's snippet. + */ + public String getSnippet() { + return snippet; + } + + /** + * Gets the snippet of the marker. + * + * @return A string containing the marker's snippet. + */ + public String getTitle() { + return title; + } + + /** + * Do not use this method, used internally by the SDK. + */ + public void hideInfoWindow() { + if (infoWindow != null) { + infoWindow.close(); + } + infoWindowShown = false; + } + + /** + * Do not use this method, used internally by the SDK. + * + * @return true if the infoWindow is shown + */ + public boolean isInfoWindowShown() { + return infoWindowShown; + } + + /** + * Sets the location of the marker. + * + * @param position A {@link LatLng} defining the marker position. + */ + public void setPosition(LatLng position) { + this.position = position; + MapboxMap map = getMapboxMap(); + if (map != null) { + map.updateMarker(this); + } + } + + /** + * Sets the snippet of the marker. + * + * @param snippet A String used in the marker info window. If {@code null}, the snippet is + * cleared. + */ + public void setSnippet(String snippet) { + this.snippet = snippet; + refreshInfoWindowContent(); + } + + /** + * Sets the icon of the marker. + * + * @param icon The {@link Icon} to be used as Marker image + */ + public void setIcon(@Nullable Icon icon) { + this.icon = icon; + this.iconId = icon != null ? icon.getId() : null; + MapboxMap map = getMapboxMap(); + if (map != null) { + map.updateMarker(this); + } + } + + /** + * Gets the {@link Icon} currently used for the marker. If no Icon was set for the marker, the + * default icon will be returned. + * + * @return The {@link Icon} the marker is using. + */ + @Nullable + public Icon getIcon() { + return icon; + } + + /** + * Sets the title of the marker. + * + * @param title A String used in the marker info window. If {@code null}, the title is + * cleared. + */ + public void setTitle(String title) { + this.title = title; + refreshInfoWindowContent(); + } + + /** + * Gets the {@link InfoWindow} the marker is using. If the marker hasn't had an info window + * defined, this will return {@code null}. + * + * @return The info window the marker is using. + */ + @Nullable + public InfoWindow getInfoWindow() { + return infoWindow; + } + + /** + * Update only for default Marker's InfoWindow content for Title and Snippet + */ + private void refreshInfoWindowContent() { + if (isInfoWindowShown() && mapView != null && mapboxMap != null && mapboxMap.getInfoWindowAdapter() == null) { + InfoWindow infoWindow = getInfoWindow(mapView); + if (mapView.getContext() != null) { + infoWindow.adaptDefaultMarker(this, mapboxMap, mapView); + } + MapboxMap map = getMapboxMap(); + if (map != null) { + map.updateMarker(this); + } + infoWindow.onContentUpdate(); + } + } + + /** + * Do not use this method, used internally by the SDK. Use {@link MapboxMap#selectMarker(Marker)} + * if you want to programmatically display the markers info window. + * + * @param mapboxMap The hosting mapbox map. + * @param mapView The hosting map view. + * @return The info window that was shown. + */ + @Nullable + public InfoWindow showInfoWindow(@NonNull MapboxMap mapboxMap, @NonNull MapView mapView) { + setMapboxMap(mapboxMap); + setMapView(mapView); + MapboxMap.InfoWindowAdapter infoWindowAdapter = getMapboxMap().getInfoWindowAdapter(); + if (infoWindowAdapter != null) { + // end developer is using a custom InfoWindowAdapter + View content = infoWindowAdapter.getInfoWindow(this); + if (content != null) { + infoWindow = new InfoWindow(content, mapboxMap); + showInfoWindow(infoWindow, mapView); + return infoWindow; + } + } + + InfoWindow infoWindow = getInfoWindow(mapView); + if (mapView.getContext() != null) { + infoWindow.adaptDefaultMarker(this, mapboxMap, mapView); + } + return showInfoWindow(infoWindow, mapView); + } + + @NonNull + private InfoWindow showInfoWindow(InfoWindow iw, MapView mapView) { + iw.open(mapView, this, getPosition(), rightOffsetPixels, topOffsetPixels); + infoWindowShown = true; + return iw; + } + + @Nullable + private InfoWindow getInfoWindow(@NonNull MapView mapView) { + if (infoWindow == null && mapView.getContext() != null) { + infoWindow = new InfoWindow(mapView, R.layout.mapbox_infowindow_content, getMapboxMap()); + } + return infoWindow; + } + + /** + * Do not use this method, used internally by the SDK. + * + * @param topOffsetPixels the top offset pixels. + */ + public void setTopOffsetPixels(int topOffsetPixels) { + this.topOffsetPixels = topOffsetPixels; + } + + /** + * Do not use this method, used internally by the SDK. + * + * @param rightOffsetPixels the right offset pixels. + */ + public void setRightOffsetPixels(int rightOffsetPixels) { + this.rightOffsetPixels = rightOffsetPixels; + } + + /** + * Returns a String with the marker position. + * + * @return A String with the marker position. + */ + @Override + public String toString() { + return "Marker [position[" + getPosition() + "]]"; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MarkerOptions.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MarkerOptions.java new file mode 100644 index 000000000..62c3d378e --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/MarkerOptions.java @@ -0,0 +1,195 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.mapbox.mapboxsdk.exceptions.InvalidMarkerPositionException; +import com.mapbox.mapboxsdk.geometry.LatLng; + +/** + *

+ * Builder for composing {@link Marker} objects. See {@link Marker} for additional information. + *

+ *

Example

+ *
+ * mapView.addMarker(new MarkerOptions()
+ *   .title("Intersection")
+ *   .snippet("H St NW with 15th St NW")
+ *   .position(new LatLng(38.9002073, -77.03364419)));
+ * 
+ * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public final class MarkerOptions extends BaseMarkerOptions implements Parcelable { + + /** + * Defines options for a Marker. + */ + public MarkerOptions() { + } + + protected MarkerOptions(Parcel in) { + position((LatLng) in.readParcelable(LatLng.class.getClassLoader())); + snippet(in.readString()); + title(in.readString()); + if (in.readByte() != 0) { + // this means we have an icon + String iconId = in.readString(); + Bitmap iconBitmap = in.readParcelable(Bitmap.class.getClassLoader()); + Icon icon = new Icon(iconId, iconBitmap); + icon(icon); + } + } + + @NonNull + @Override + public MarkerOptions getThis() { + return this; + } + + /** + * Describe the kinds of special objects contained in this Parcelable's + * marshalled representation. + * + * @return integer 0. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a Parcel. + * + * @param out The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. May be 0 or + * {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeParcelable(getPosition(), flags); + out.writeString(getSnippet()); + out.writeString(getTitle()); + Icon icon = getIcon(); + out.writeByte((byte) (icon != null ? 1 : 0)); + if (icon != null) { + out.writeString(getIcon().getId()); + out.writeParcelable(getIcon().getBitmap(), flags); + } + } + + /** + * Do not use this method. Used internally by the SDK. + * + * @return Marker The build marker + */ + public Marker getMarker() { + if (position == null) { + throw new InvalidMarkerPositionException(); + } + + return new Marker(position, icon, title, snippet); + } + + /** + * Returns the position set for this {@link MarkerOptions} object. + * + * @return A {@link LatLng} object specifying the marker's current position. + */ + public LatLng getPosition() { + return position; + } + + /** + * Gets the snippet set for this {@link MarkerOptions} object. + * + * @return A string containing the marker's snippet. + */ + public String getSnippet() { + return snippet; + } + + /** + * Gets the title set for this {@link MarkerOptions} object. + * + * @return A string containing the marker's title. + */ + public String getTitle() { + return title; + } + + /** + * Gets the custom icon set for this {@link MarkerOptions} object. + * + * @return A {@link Icon} object that the marker is using. If the icon wasn't set, default icon + * will return. + */ + public Icon getIcon() { + return icon; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public MarkerOptions createFromParcel(@NonNull Parcel in) { + return new MarkerOptions(in); + } + + public MarkerOptions[] newArray(int size) { + return new MarkerOptions[size]; + } + }; + + /** + * Compares this {@link MarkerOptions} object with another {@link MarkerOptions} and + * determines if their properties match. + * + * @param o Another {@link MarkerOptions} to compare with this object. + * @return True if marker properties match this {@link MarkerOptions} object. + * Else, false. + */ + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + MarkerOptions marker = (MarkerOptions) o; + + if (getPosition() != null ? !getPosition().equals(marker.getPosition()) : marker.getPosition() != null) { + return false; + } + if (getSnippet() != null ? !getSnippet().equals(marker.getSnippet()) : marker.getSnippet() != null) { + return false; + } + if (getIcon() != null ? !getIcon().equals(marker.getIcon()) : marker.getIcon() != null) { + return false; + } + return !(getTitle() != null ? !getTitle().equals(marker.getTitle()) : marker.getTitle() != null); + } + + /** + * Gives an integer which can be used as the bucket number for storing elements of the set/map. + * This bucket number is the address of the element inside the set/map. There's no guarantee + * that this hash value will be consistent between different Java implementations, or even + * between different execution runs of the same program. + * + * @return integer value you can use for storing element. + */ + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (getPosition() != null ? getPosition().hashCode() : 0); + result = 31 * result + (getSnippet() != null ? getSnippet().hashCode() : 0); + result = 31 * result + (getIcon() != null ? getIcon().hashCode() : 0); + result = 31 * result + (getTitle() != null ? getTitle().hashCode() : 0); + return result; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polygon.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polygon.java new file mode 100644 index 000000000..5f6cef8d4 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polygon.java @@ -0,0 +1,109 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.graphics.Color; +import android.support.annotation.Keep; + +import android.support.annotation.NonNull; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.maps.MapboxMap; + +import java.util.ArrayList; +import java.util.List; + +/** + * Polygon is a geometry annotation that's a closed loop of coordinates. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public final class Polygon extends BasePointCollection { + + @Keep + private int fillColor = Color.BLACK; // default fillColor is black + @Keep + private int strokeColor = Color.BLACK; // default strokeColor is black + @Keep + private List> holes; + + Polygon() { + super(); + holes = new ArrayList<>(); + } + + /** + * Get the color of the fill region of the polygon. + * + * @return The color of the fill. + */ + public int getFillColor() { + return fillColor; + } + + /** + * Get the color of the stroke of the polygon. + * + * @return The color of the stroke. + */ + public int getStrokeColor() { + return strokeColor; + } + + /** + * Returns a copy of the holes. + * + * @return A {@link List} of {@link List} of {@link LatLng} points making up the holes. + */ + public List> getHoles() { + return new ArrayList<>(holes); + } + + /** + * Sets the color of the fill region of the polygon. + * + * @param color The color in ARGB format. + */ + public void setFillColor(int color) { + fillColor = color; + update(); + } + + /** + * Sets the color of the stroke of the polygon. + * + * @param color The color in ARGB format. + */ + public void setStrokeColor(int color) { + strokeColor = color; + update(); + } + + /** + * Sets the holes of this polygon. This method will take a copy of the holes, so further + * mutations to holes will have no effect on this polygon. + * + * @param holes A {@link List} of {@link List} of {@link LatLng} points making up the holes. + */ + public void setHoles(@NonNull List> holes) { + this.holes = new ArrayList<>(holes); + update(); + } + + /** + * Add a hole to the polygon. + * + * @param hole A {@link List} of {@link List} of {@link LatLng} points making up the hole to be added. + */ + void addHole(List hole) { + holes.add(hole); + update(); + } + + @Override + void update() { + MapboxMap mapboxMap = getMapboxMap(); + if (mapboxMap != null) { + mapboxMap.updatePolygon(this); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolygonOptions.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolygonOptions.java new file mode 100644 index 000000000..452420fcb --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolygonOptions.java @@ -0,0 +1,308 @@ +package com.mapbox.mapboxsdk.annotations; + + +import android.os.Parcel; +import android.os.Parcelable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.mapbox.mapboxsdk.geometry.LatLng; + +import java.util.ArrayList; +import java.util.List; + +/** + * Builder for composing {@link Polygon} objects. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public final class PolygonOptions implements Parcelable { + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public PolygonOptions createFromParcel(@NonNull Parcel in) { + return new PolygonOptions(in); + } + + public PolygonOptions[] newArray(int size) { + return new PolygonOptions[size]; + } + }; + + private PolygonOptions(Parcel in) { + polygon = new Polygon(); + List pointsList = new ArrayList<>(); + in.readList(pointsList, LatLng.class.getClassLoader()); + addAll(pointsList); + List> holes = new ArrayList<>(); + in.readList(holes, LatLng.class.getClassLoader()); + addAllHoles(holes); + alpha(in.readFloat()); + fillColor(in.readInt()); + strokeColor(in.readInt()); + } + + /** + * Describe the kinds of special objects contained in this Parcelable's + * marshalled representation. + * + * @return integer 0. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a Parcel. + * + * @param out The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. May be 0 or + * {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeList(getPoints()); + out.writeList(getHoles()); + out.writeFloat(getAlpha()); + out.writeInt(getFillColor()); + out.writeInt(getStrokeColor()); + } + + private Polygon polygon; + + /** + * Defines options for a polygon. + */ + public PolygonOptions() { + polygon = new Polygon(); + } + + /** + * Adds a vertex to the outline of the polygon being built. + * + * @param point {@link LatLng} point to be added to polygon geometry. + * @return This {@link PolygonOptions} object with the given point added to the outline. + */ + @NonNull + public PolygonOptions add(LatLng point) { + polygon.addPoint(point); + return this; + } + + /** + * Adds vertices to the outline of the polygon being built. + * + * @param points {@link LatLng} points to be added to polygon geometry. + * @return This {@link PolygonOptions} object with the given points added to the outline. + */ + @NonNull + public PolygonOptions add(LatLng... points) { + for (LatLng point : points) { + add(point); + } + return this; + } + + /** + * Adds vertices to the outline of the polygon being built. + * + * @param points {@link Iterable} list made up of {@link LatLng} points defining the polygon + * geometry + * @return This {@link PolygonOptions} object with the given points added to the outline. + */ + @NonNull + public PolygonOptions addAll(Iterable points) { + for (LatLng point : points) { + add(point); + } + return this; + } + + /** + * Adds a hole to the outline of the polygon being built. + * + * @param hole {@link List} list made up of {@link LatLng} points defining the hole + * @return This {@link PolygonOptions} object with the given hole added to the outline. + */ + @NonNull + public PolygonOptions addHole(List hole) { + polygon.addHole(hole); + return this; + } + + /** + * Adds holes to the outline of the polygon being built. + * + * @param holes {@link List} list made up of {@link LatLng} holes to be added to polygon geometry + * @return This {@link PolygonOptions} object with the given holes added to the outline. + */ + @NonNull + public PolygonOptions addHole(List... holes) { + for (List hole : holes) { + addHole(hole); + } + return this; + } + + /** + * Adds holes to the outline of the polygon being built. + * + * @param holes {@link Iterable} list made up of {@link List} list of {@link LatLng} holes defining the hole geometry + * @return This {@link PolygonOptions} object with the given holes added to the outline. + */ + @NonNull + public PolygonOptions addAllHoles(Iterable> holes) { + for (List hole : holes) { + addHole(hole); + } + return this; + } + + /** + * Set the alpha value of the polyline. + * + * @param alpha float value between 0 (not visible) and 1. + * @return This {@link PolygonOptions} object with the given polygon alpha value. + */ + @NonNull + public PolygonOptions alpha(float alpha) { + polygon.setAlpha(alpha); + return this; + } + + /** + * Gets the alpha set for this {@link PolygonOptions} object. + * + * @return float value between 0 and 1 defining the alpha. + */ + public float getAlpha() { + return polygon.getAlpha(); + } + + /** + * Specifies the polygon's fill color, as 32-bit ARGB. The default color is black. + * + * @param color 32-bit ARGB color. + * @return This {@link PolylineOptions} object with a new color set. + */ + @NonNull + public PolygonOptions fillColor(int color) { + polygon.setFillColor(color); + return this; + } + + /** + * Gets the fill color set for this {@link PolygonOptions} object. + * + * @return The fill color of the polygon in ARGB format. + */ + public int getFillColor() { + return polygon.getFillColor(); + } + + /** + * Do not use this method. Used internally by the SDK. + * + * @return Polygon the Polygon to return + */ + public Polygon getPolygon() { + return polygon; + } + + /** + * Specifies the polygon's stroke color, as 32-bit ARGB. The default color is black. + * + * @param color 32-bit ARGB color. + * @return This {@link PolygonOptions} object with a new stroke color set. + */ + @NonNull + public PolygonOptions strokeColor(int color) { + polygon.setStrokeColor(color); + return this; + } + + /** + * Gets the stroke color set for this {@link PolygonOptions} object. + * + * @return The stroke color of the polygon in ARGB format. + */ + public int getStrokeColor() { + return polygon.getStrokeColor(); + } + + /** + * Gets the points set for this {@link PolygonOptions} object. + * + * @return The list made up of {@link LatLng} points defining the polygon. + */ + public List getPoints() { + // the getter gives us a copy, which is the safe thing to do... + return polygon.getPoints(); + } + + /** + * Gets the holes set for this {@link PolygonOptions} object. + * + * @return The list made up of {@link List} of {@link List} of {@link LatLng} points defining the holes. + */ + public List> getHoles() { + return polygon.getHoles(); + } + + + /** + * Compares this {@link PolygonOptions} object with another {@link PolygonOptions} and + * determines if their color, alpha, stroke color, and vertices match. + * + * @param o Another {@link PolygonOptions} to compare with this object. + * @return True if color, alpha, stroke color, vertices and holes match this {@link PolygonOptions} + * {@link PolygonOptions} object. Else, false. + */ + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PolygonOptions polygon = (PolygonOptions) o; + + if (Float.compare(polygon.getAlpha(), getAlpha()) != 0) { + return false; + } + if (getFillColor() != polygon.getFillColor()) { + return false; + } + if (getStrokeColor() != polygon.getStrokeColor()) { + return false; + } + if (getPoints() != null ? !getPoints().equals(polygon.getPoints()) : polygon.getPoints() != null) { + return false; + } + return !(getHoles() != null ? !getHoles().equals(polygon.getHoles()) : polygon.getHoles() != null); + } + + /** + * Gives an integer which can be used as the bucket number for storing elements of the set/map. + * This bucket number is the address of the element inside the set/map. There's no guarantee + * that this hash value will be consistent between different Java implementations, or even + * between different execution runs of the same program. + * + * @return integer value you can use for storing element. + */ + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (getAlpha() != +0.0f ? Float.floatToIntBits(getAlpha()) : 0); + result = 31 * result + getFillColor(); + result = 31 * result + getStrokeColor(); + result = 31 * result + (getPoints() != null ? getPoints().hashCode() : 0); + result = 31 * result + (getHoles() != null ? getHoles().hashCode() : 0); + return result; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polyline.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polyline.java new file mode 100644 index 000000000..76ea15215 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/Polyline.java @@ -0,0 +1,71 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.graphics.Color; +import android.support.annotation.Keep; + +import com.mapbox.mapboxsdk.maps.MapboxMap; + +/** + * Polyline is a geometry feature with an unclosed list of coordinates drawn as a line + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public final class Polyline extends BasePointCollection { + + @Keep + private int color = Color.BLACK; // default color is black + @Keep + private float width = 10; // As specified by Google API Docs (in pixels) + + Polyline() { + super(); + } + + /** + * Gets the color of this polyline. + * + * @return The color in ARGB format. + */ + public int getColor() { + return color; + } + + /** + * Gets the width of this polyline. + * + * @return The width in screen pixels. + */ + public float getWidth() { + return width; + } + + /** + * Sets the color of the polyline. + * + * @param color - the color in ARGB format + */ + public void setColor(int color) { + this.color = color; + update(); + } + + /** + * Sets the width of the polyline. + * + * @param width in pixels + */ + public void setWidth(float width) { + this.width = width; + update(); + } + + @Override + void update() { + MapboxMap mapboxMap = getMapboxMap(); + if (mapboxMap != null) { + mapboxMap.updatePolyline(this); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolylineOptions.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolylineOptions.java new file mode 100644 index 000000000..6a54f948b --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/PolylineOptions.java @@ -0,0 +1,250 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.os.Parcel; +import android.os.Parcelable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.mapbox.mapboxsdk.geometry.LatLng; + +import java.util.ArrayList; +import java.util.List; + +/** + * Builder for composing {@link Polyline} objects. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ +@Deprecated +public final class PolylineOptions implements Parcelable { + + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public PolylineOptions createFromParcel(@NonNull Parcel in) { + return new PolylineOptions(in); + } + + public PolylineOptions[] newArray(int size) { + return new PolylineOptions[size]; + } + }; + + private PolylineOptions(Parcel in) { + polyline = new Polyline(); + ArrayList pointsList = new ArrayList<>(); + in.readList(pointsList, LatLng.class.getClassLoader()); + addAll(pointsList); + alpha(in.readFloat()); + color(in.readInt()); + width(in.readFloat()); + } + + /** + * Describe the kinds of special objects contained in this Parcelable's + * marshalled representation. + * + * @return integer 0. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a Parcel. + * + * @param out The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. May be 0 or + * {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeList(getPoints()); + out.writeFloat(getAlpha()); + out.writeInt(getColor()); + out.writeFloat(getWidth()); + } + + private Polyline polyline; + + /** + * Defines options for a polyline. + */ + public PolylineOptions() { + polyline = new Polyline(); + } + + /** + * Adds a vertex to the end of the polyline being built. + * + * @param point {@link LatLng} point to be added to polyline geometry. + * @return This {@link PolylineOptions} object with the given point on the end. + */ + @NonNull + public PolylineOptions add(LatLng point) { + polyline.addPoint(point); + return this; + } + + /** + * Adds vertices to the end of the polyline being built. + * + * @param points {@link LatLng} points defining the polyline geometry. + * @return This {@link PolylineOptions} object with the given point on the end. + */ + @NonNull + public PolylineOptions add(LatLng... points) { + for (LatLng point : points) { + add(point); + } + return this; + } + + /** + * Adds vertices to the end of the polyline being built. + * + * @param points {@link Iterable} list made up of {@link LatLng} points defining the polyline + * geometry + * @return This {@link PolylineOptions} object with the given points on the end. + */ + @NonNull + public PolylineOptions addAll(Iterable points) { + for (LatLng point : points) { + add(point); + } + return this; + } + + /** + * Set the alpha value of the polyline. + * + * @param alpha float value between 0 (not visible) and 1. + * @return This {@link PolylineOptions} object with the given polyline alpha value. + */ + @NonNull + public PolylineOptions alpha(float alpha) { + polyline.setAlpha(alpha); + return this; + } + + /** + * Gets the alpha set for this {@link PolylineOptions} object. + * + * @return float value between 0 and 1 defining the alpha. + */ + public float getAlpha() { + return polyline.getAlpha(); + } + + /** + * Sets the color of the polyline as a 32-bit ARGB color. The default color is black. + * + * @param color 32-bit ARGB color. + * @return This {@link PolylineOptions} object with a new color set. + */ + @NonNull + public PolylineOptions color(int color) { + polyline.setColor(color); + return this; + } + + /** + * Gets the color set for this {@link PolylineOptions} object. + * + * @return The color of the polyline in ARGB format. + */ + public int getColor() { + return polyline.getColor(); + } + + /** + * Do not use this method. Used internally by the SDK. + * + * @return PolyLine The polyline build by this class. + */ + public Polyline getPolyline() { + return polyline; + } + + /** + * Gets the width set for this {@link PolylineOptions} object. + * + * @return The width of the polyline in screen pixels. + */ + public float getWidth() { + return polyline.getWidth(); + } + + /** + * Sets the width of the polyline in screen pixels. The default is 10. + * + * @param width float value defining width of polyline using unit pixels. + * @return This {@link PolylineOptions} object with a new width set. + */ + @NonNull + public PolylineOptions width(float width) { + polyline.setWidth(width); + return this; + } + + /** + * Gets the points set for this {@link PolylineOptions} object. + * + * @return a {@link List} of {@link LatLng}s specifying the vertices of the polyline. + */ + public List getPoints() { + // the getter gives us a copy, which is the safe thing to do... + return polyline.getPoints(); + } + + /** + * Compares this {@link PolylineOptions} object with another {@link PolylineOptions} and + * determines if their color, alpha, width, and vertices match. + * + * @param o Another {@link PolylineOptions} to compare with this object. + * @return True if color, alpha, width, and vertices match this {@link PolylineOptions} object. + * Else, false. + */ + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + PolylineOptions polyline = (PolylineOptions) o; + + if (Float.compare(polyline.getAlpha(), getAlpha()) != 0) { + return false; + } + if (getColor() != polyline.getColor()) { + return false; + } + if (Float.compare(polyline.getWidth(), getWidth()) != 0) { + return false; + } + return !(getPoints() != null ? !getPoints().equals(polyline.getPoints()) : polyline.getPoints() != null); + } + + /** + * Gives an integer which can be used as the bucket number for storing elements of the set/map. + * This bucket number is the address of the element inside the set/map. There's no guarantee + * that this hash value will be consistent between different Java implementations, or even + * between different execution runs of the same program. + * + * @return integer value you can use for storing element. + */ + @Override + public int hashCode() { + int result = 1; + result = 31 * result + (getAlpha() != +0.0f ? Float.floatToIntBits(getAlpha()) : 0); + result = 31 * result + getColor(); + result = 31 * result + (getWidth() != +0.0f ? Float.floatToIntBits(getWidth()) : 0); + result = 31 * result + (getPoints() != null ? getPoints().hashCode() : 0); + return result; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/package-info.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/package-info.java new file mode 100644 index 000000000..1e2dc542f --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/annotations/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains the Mapbox Maps Android Annotation API classes. + */ +package com.mapbox.mapboxsdk.annotations; diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/Attribution.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/Attribution.java new file mode 100644 index 000000000..035614a90 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/Attribution.java @@ -0,0 +1,70 @@ +package com.mapbox.mapboxsdk.attribution; + +import android.support.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class Attribution { + + private static final String OPENSTREETMAP = "OpenStreetMap"; + private static final String OPENSTREETMAP_ABBR = "OSM"; + static final String TELEMETRY = "Telemetry Settings"; + static final String MAPBOX_URL = "https://www.mapbox.com/about/maps/"; + static final String TELEMETRY_URL = "https://www.mapbox.com/telemetry/"; + static final List IMPROVE_MAP_URLS = new ArrayList<>(); + + static { + // Using a List makes URL backwards compatible + IMPROVE_MAP_URLS.add("https://www.mapbox.com/feedback/"); + IMPROVE_MAP_URLS.add("https://www.mapbox.com/map-feedback/"); + IMPROVE_MAP_URLS.add("https://apps.mapbox.com/feedback/"); + } + + private String title; + private String url; + + Attribution(String title, String url) { + this.title = title; + this.url = url; + } + + public String getTitle() { + return title; + } + + public String getTitleAbbreviated() { + if (title.equals(OPENSTREETMAP)) { + return OPENSTREETMAP_ABBR; + } + return title; + } + + public String getUrl() { + return url; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Attribution that = (Attribution) o; + + if (title != null ? !title.equals(that.title) : that.title != null) { + return false; + } + return url != null ? url.equals(that.url) : that.url == null; + } + + @Override + public int hashCode() { + int result = title != null ? title.hashCode() : 0; + result = 31 * result + (url != null ? url.hashCode() : 0); + return result; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/AttributionLayout.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/AttributionLayout.java new file mode 100644 index 000000000..25d5de7ea --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/AttributionLayout.java @@ -0,0 +1,68 @@ +package com.mapbox.mapboxsdk.attribution; + +import android.graphics.Bitmap; +import android.graphics.PointF; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public class AttributionLayout { + + @Nullable + private Bitmap logo; + @Nullable + private PointF anchorPoint; + private boolean shortText; + + public AttributionLayout(@Nullable Bitmap logo, @Nullable PointF anchorPoint, boolean shortText) { + this.logo = logo; + this.anchorPoint = anchorPoint; + this.shortText = shortText; + } + + @Nullable + public Bitmap getLogo() { + return logo; + } + + @Nullable + public PointF getAnchorPoint() { + return anchorPoint; + } + + public boolean isShortText() { + return shortText; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + AttributionLayout that = (AttributionLayout) o; + + if (logo != null ? !logo.equals(that.logo) : that.logo != null) { + return false; + } + return anchorPoint != null ? anchorPoint.equals(that.anchorPoint) : that.anchorPoint == null; + } + + @Override + public int hashCode() { + int result = logo != null ? logo.hashCode() : 0; + result = 31 * result + (anchorPoint != null ? anchorPoint.hashCode() : 0); + return result; + } + + @NonNull + @Override + public String toString() { + return "AttributionLayout{" + + "logo=" + logo + + ", anchorPoint=" + anchorPoint + + '}'; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/AttributionMeasure.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/AttributionMeasure.java new file mode 100644 index 000000000..b40a7be24 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/AttributionMeasure.java @@ -0,0 +1,249 @@ +package com.mapbox.mapboxsdk.attribution; + +import android.graphics.Bitmap; +import android.graphics.PointF; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.widget.TextView; + +import java.util.Arrays; +import java.util.List; + +public class AttributionMeasure { + + private Bitmap logo; + private Bitmap logoSmall; + private Bitmap snapshot; + private TextView textView; + private TextView textViewShort; + private float margin; + + private boolean shorterText; + + AttributionMeasure(Bitmap snapshot, Bitmap logo, Bitmap logoSmall, TextView tv, TextView tvShort, float margin) { + this.snapshot = snapshot; + this.logo = logo; + this.logoSmall = logoSmall; + this.textView = tv; + this.textViewShort = tvShort; + this.margin = margin; + } + + @Nullable + public AttributionLayout measure() { + Chain chain = new Chain( + new FullLogoLongTextCommand(), + new FullLogoShortTextCommand(), + new SmallLogoLongTextCommand(), + new SmallLogoShortTextCommand(), + new LongTextCommand(), + new ShortTextCommand(), + new NoTextCommand() + ); + + AttributionLayout attributionLayout = chain.start(this); + shorterText = attributionLayout.isShortText(); + return attributionLayout; + } + + + private static class FullLogoLongTextCommand implements Command { + @Nullable + public AttributionLayout execute(@NonNull AttributionMeasure measure) { + float width = measure.getLogoContainerWidth() + measure.getTextViewContainerWidth(); + boolean fitBounds = width <= measure.getMaxSize(); + if (fitBounds) { + PointF anchor = calculateAnchor(measure.snapshot, measure.textView, measure.margin); + return new AttributionLayout(measure.logo, anchor, false); + } + return null; + } + } + + private static class FullLogoShortTextCommand implements Command { + @Nullable + @Override + public AttributionLayout execute(@NonNull AttributionMeasure measure) { + float width = measure.getLogoContainerWidth() + measure.getTextViewShortContainerWidth(); + boolean fitBounds = width <= measure.getMaxSizeShort(); + if (fitBounds) { + PointF anchor = calculateAnchor(measure.snapshot, measure.textViewShort, measure.margin); + return new AttributionLayout(measure.logo, anchor, true); + } + return null; + } + } + + private static class SmallLogoLongTextCommand implements Command { + @Nullable + @Override + public AttributionLayout execute(@NonNull AttributionMeasure measure) { + float width = measure.getLogoSmallContainerWidth() + measure.getTextViewContainerWidth(); + boolean fitBounds = width <= measure.getMaxSize(); + if (fitBounds) { + PointF anchor = calculateAnchor(measure.snapshot, measure.textView, measure.margin); + return new AttributionLayout(measure.logoSmall, anchor, false); + } + return null; + } + } + + private static class SmallLogoShortTextCommand implements Command { + @Nullable + @Override + public AttributionLayout execute(@NonNull AttributionMeasure measure) { + float width = measure.getLogoContainerWidth() + measure.getTextViewShortContainerWidth(); + boolean fitBounds = width <= measure.getMaxSizeShort(); + if (fitBounds) { + PointF anchor = calculateAnchor(measure.snapshot, measure.textViewShort, measure.margin); + return new AttributionLayout(measure.logoSmall, anchor, true); + } + return null; + } + } + + private static class LongTextCommand implements Command { + @Nullable + @Override + public AttributionLayout execute(@NonNull AttributionMeasure measure) { + float width = measure.getTextViewContainerWidth() + measure.margin; + boolean fitBounds = width <= measure.getMaxSize(); + if (fitBounds) { + return new AttributionLayout(null, calculateAnchor(measure.snapshot, measure.textView, measure.margin), false); + } + return null; + } + } + + private static class ShortTextCommand implements Command { + @Nullable + @Override + public AttributionLayout execute(@NonNull AttributionMeasure measure) { + float width = measure.getTextViewShortContainerWidth() + measure.margin; + boolean fitBounds = width <= measure.getMaxSizeShort(); + if (fitBounds) { + PointF anchor = calculateAnchor(measure.snapshot, measure.textViewShort, measure.margin); + return new AttributionLayout(null, anchor, true); + } + return null; + } + } + + private static class NoTextCommand implements Command { + @NonNull + @Override + public AttributionLayout execute(AttributionMeasure measure) { + return new AttributionLayout(null, null, false); + } + } + + private static PointF calculateAnchor(Bitmap snapshot, TextView textView, float margin) { + return new PointF( + snapshot.getWidth() - textView.getMeasuredWidth() - margin, + snapshot.getHeight() - margin - textView.getMeasuredHeight() + ); + } + + public TextView getTextView() { + return shorterText ? textViewShort : textView; + } + + private class Chain { + public List commands; + + Chain(Command... commands) { + this.commands = Arrays.asList(commands); + } + + @Nullable + public AttributionLayout start(AttributionMeasure measure) { + AttributionLayout attributionLayout = null; + for (Command command : commands) { + attributionLayout = command.execute(measure); + if (attributionLayout != null) { + break; + } + } + return attributionLayout; + } + } + + public interface Command { + @Nullable + AttributionLayout execute(AttributionMeasure measure); + } + + private float getTextViewContainerWidth() { + return textView.getMeasuredWidth() + margin; + } + + private float getLogoContainerWidth() { + return logo.getWidth() + (2 * margin); + } + + private float getTextViewShortContainerWidth() { + return textViewShort.getMeasuredWidth() + margin; + } + + private float getLogoSmallContainerWidth() { + return logoSmall.getWidth() + (2 * margin); + } + + private float getMaxSize() { + return snapshot.getWidth() * 8 / 10; + } + + private float getMaxSizeShort() { + return snapshot.getWidth(); + } + + public static class Builder { + private Bitmap snapshot; + private Bitmap logo; + private Bitmap logoSmall; + private TextView textView; + private TextView textViewShort; + private float marginPadding; + + @NonNull + public Builder setSnapshot(Bitmap snapshot) { + this.snapshot = snapshot; + return this; + } + + @NonNull + public Builder setLogo(Bitmap logo) { + this.logo = logo; + return this; + } + + @NonNull + public Builder setLogoSmall(Bitmap logoSmall) { + this.logoSmall = logoSmall; + return this; + } + + @NonNull + public Builder setTextView(TextView textView) { + this.textView = textView; + return this; + } + + @NonNull + public Builder setTextViewShort(TextView textViewShort) { + this.textViewShort = textViewShort; + return this; + } + + @NonNull + public Builder setMarginPadding(float marginPadding) { + this.marginPadding = marginPadding; + return this; + } + + @NonNull + public AttributionMeasure build() { + return new AttributionMeasure(snapshot, logo, logoSmall, textView, textViewShort, marginPadding); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/AttributionParser.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/AttributionParser.java new file mode 100644 index 000000000..ff59dbdf9 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/AttributionParser.java @@ -0,0 +1,314 @@ +package com.mapbox.mapboxsdk.attribution; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.text.Html; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.URLSpan; +import com.mapbox.mapboxsdk.R; + +import java.lang.ref.WeakReference; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Responsible for parsing attribution data coming from Sources and MapSnapshot. + *

+ * Exposes multiple configuration options to manipulate data being parsed. + * Use the Options object to build these configurations. + *

+ */ +public class AttributionParser { + + private static final String IMPROVE_THIS_MAP = "Improve this map"; + private final WeakReference context; + private final Set attributions = new LinkedHashSet<>(); + private final String attributionData; + private final boolean withImproveMap; + private final boolean withCopyrightSign; + private final boolean withTelemetryAttribution; + private final boolean withMapboxAttribution; + + AttributionParser(WeakReference context, String attributionData, boolean withImproveMap, + boolean withCopyrightSign, boolean withTelemetryAttribution, boolean withMapboxAttribution) { + this.context = context; + this.attributionData = attributionData; + this.withImproveMap = withImproveMap; + this.withCopyrightSign = withCopyrightSign; + this.withTelemetryAttribution = withTelemetryAttribution; + this.withMapboxAttribution = withMapboxAttribution; + } + + /** + * Get parsed attributions. + * + * @return the attributions + */ + @NonNull + public Set getAttributions() { + return attributions; + } + + /** + * Get parsed attribution string. + * + * @return the parsed attribution string + */ + @NonNull + public String createAttributionString() { + return createAttributionString(false); + } + + /** + * Get parsed attribution string. + * + * @param shortenedOutput if attribution string should contain shortened output + * @return the parsed attribution string + */ + @NonNull + public String createAttributionString(boolean shortenedOutput) { + StringBuilder stringBuilder = new StringBuilder(withCopyrightSign ? "" : "© "); + int counter = 0; + for (Attribution attribution : attributions) { + counter++; + stringBuilder.append(!shortenedOutput ? attribution.getTitle() : attribution.getTitleAbbreviated()); + if (counter != attributions.size()) { + stringBuilder.append(" / "); + } + } + return stringBuilder.toString(); + } + + /** + * Main attribution for configuration + */ + protected void parse() { + parseAttributions(); + addAdditionalAttributions(); + } + + /** + * Parse attributions + */ + private void parseAttributions() { + SpannableStringBuilder htmlBuilder = (SpannableStringBuilder) fromHtml(attributionData); + URLSpan[] urlSpans = htmlBuilder.getSpans(0, htmlBuilder.length(), URLSpan.class); + for (URLSpan urlSpan : urlSpans) { + parseUrlSpan(htmlBuilder, urlSpan); + } + } + + /** + * Parse an URLSpan containing an attribution. + * + * @param htmlBuilder the html builder + * @param urlSpan the url span to be parsed + */ + private void parseUrlSpan(@NonNull SpannableStringBuilder htmlBuilder, @NonNull URLSpan urlSpan) { + String url = urlSpan.getURL(); + if (isUrlValid(url)) { + String anchor = parseAnchorValue(htmlBuilder, urlSpan); + if (isImproveThisMapAnchor(anchor)) { + anchor = translateImproveThisMapAnchor(anchor); + } + attributions.add(new Attribution(anchor, url)); + } + } + + /** + * Invoked to validate if an url is valid to be included in the final attribution. + * + * @param url the url to be validated + * @return if the url is valid + */ + private boolean isUrlValid(@NonNull String url) { + return isValidForImproveThisMap(url) && isValidForMapbox(url); + } + + /** + * Invoked to validate if an anchor is equal to Improve this map coming from tilesets. + * + * @param anchor the anchor to be validated + * @return if the url is valid + */ + private boolean isImproveThisMapAnchor(String anchor) { + return anchor.equals(IMPROVE_THIS_MAP); + } + + /** + * Invoked to replace the english Improve this map with localized variant. + * + * @param anchor the anchor to be translated + * @return the translated anchor + */ + private String translateImproveThisMapAnchor(String anchor) { + Context context = this.context.get(); + if (context != null) { + anchor = context.getString(R.string.mapbox_telemetryImproveMap); + } + return anchor; + } + + /** + * Invoked to validate if an url is valid for the improve map configuration. + * + * @param url the url to be validated + * @return if the url is valid for improve this map + */ + private boolean isValidForImproveThisMap(@NonNull String url) { + return withImproveMap || !(Attribution.IMPROVE_MAP_URLS.contains(url)); + } + + /** + * Invoked to validate if an url is valid for the Mapbox configuration. + * + * @param url the url to be validated + * @return if the url is valid for Mapbox + */ + private boolean isValidForMapbox(@NonNull String url) { + return withMapboxAttribution || !url.equals(Attribution.MAPBOX_URL); + } + + /** + * Parse the attribution by parsing the anchor value of html href tag. + * + * @param htmlBuilder the html builder + * @param urlSpan the current urlSpan + * @return the parsed anchor value + */ + @NonNull + private String parseAnchorValue(SpannableStringBuilder htmlBuilder, URLSpan urlSpan) { + int start = htmlBuilder.getSpanStart(urlSpan); + int end = htmlBuilder.getSpanEnd(urlSpan); + int length = end - start; + char[] charKey = new char[length]; + htmlBuilder.getChars(start, end, charKey, 0); + return stripCopyright(String.valueOf(charKey)); + } + + /** + * Utility to strip the copyright sign from an attribution + * + * @param anchor the attribution string to strip + * @return the stripped attribution string without the copyright sign + */ + @NonNull + private String stripCopyright(@NonNull String anchor) { + if (!withCopyrightSign && anchor.startsWith("© ")) { + anchor = anchor.substring(2, anchor.length()); + } + return anchor; + } + + /** + * Invoked to manually add attributions + */ + private void addAdditionalAttributions() { + if (withTelemetryAttribution) { + Context context = this.context.get(); + attributions.add( + new Attribution( + context != null ? context.getString(R.string.mapbox_telemetrySettings) : Attribution.TELEMETRY, + Attribution.TELEMETRY_URL + ) + ); + } + } + + /** + * Convert a string to a spanned html representation. + * + * @param html the string to convert + * @return the spanned html representation + */ + private static Spanned fromHtml(String html) { + Spanned result; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); + } else { + result = Html.fromHtml(html); + } + return result; + } + + /** + * Builder to configure using an AttributionParser. + *

+ * AttributionData, set with {@link #withAttributionData(String...)}, is the only required property to build + * the underlying AttributionParser. Other properties include trimming the copyright sign, adding telemetry + * attribution or hiding attribution as improve this map and Mapbox. + *

+ */ + public static class Options { + private final WeakReference context; + private boolean withImproveMap = true; + private boolean withCopyrightSign = true; + private boolean withTelemetryAttribution = false; + private boolean withMapboxAttribution = true; + private String[] attributionDataStringArray; + + public Options(@NonNull Context context) { + this.context = new WeakReference<>(context); + } + + @NonNull + public Options withAttributionData(String... attributionData) { + this.attributionDataStringArray = attributionData; + return this; + } + + @NonNull + public Options withImproveMap(boolean withImproveMap) { + this.withImproveMap = withImproveMap; + return this; + } + + @NonNull + public Options withCopyrightSign(boolean withCopyrightSign) { + this.withCopyrightSign = withCopyrightSign; + return this; + } + + @NonNull + public Options withTelemetryAttribution(boolean withTelemetryAttribution) { + this.withTelemetryAttribution = withTelemetryAttribution; + return this; + } + + @NonNull + public Options withMapboxAttribution(boolean withMapboxAttribution) { + this.withMapboxAttribution = withMapboxAttribution; + return this; + } + + @NonNull + public AttributionParser build() { + if (attributionDataStringArray == null) { + throw new IllegalStateException("Using builder without providing attribution data"); + } + + String fullAttributionString = parseAttribution(attributionDataStringArray); + AttributionParser attributionParser = new AttributionParser( + context, + fullAttributionString, + withImproveMap, + withCopyrightSign, + withTelemetryAttribution, + withMapboxAttribution + ); + attributionParser.parse(); + return attributionParser; + } + + private String parseAttribution(String[] attribution) { + StringBuilder builder = new StringBuilder(); + for (String attr : attribution) { + if (!attr.isEmpty()) { + builder.append(attr); + } + } + return builder.toString(); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/package-info.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/package-info.java new file mode 100644 index 000000000..57d840af5 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/attribution/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains the Mapbox Maps Android Attribution API classes. + */ +package com.mapbox.mapboxsdk.attribution; diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraPosition.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraPosition.java new file mode 100644 index 000000000..e2341029f --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraPosition.java @@ -0,0 +1,397 @@ +package com.mapbox.mapboxsdk.camera; + +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.FloatRange; +import android.support.annotation.Keep; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.Size; + +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.constants.MapboxConstants; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.utils.MathUtils; + +import java.util.Arrays; + +/** + * Resembles the position, angle, zoom and tilt of the user's viewpoint. + */ +public final class CameraPosition implements Parcelable { + + public static final CameraPosition DEFAULT = new CameraPosition(new LatLng(), 0, 0, 0, new double[] {0, 0, 0, 0}); + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public CameraPosition createFromParcel(Parcel in) { + double bearing = in.readDouble(); + LatLng target = in.readParcelable(LatLng.class.getClassLoader()); + double tilt = in.readDouble(); + double zoom = in.readDouble(); + double[] padding = new double[4]; + in.readDoubleArray(padding); + return new CameraPosition(target, zoom, tilt, bearing, padding); + } + + public CameraPosition[] newArray(int size) { + return new CameraPosition[size]; + } + }; + + /** + * Direction that the camera is pointing in, in degrees clockwise from north. + */ + @Keep + public final double bearing; + + /** + * The location that the camera is pointing at. + */ + @Keep + public final LatLng target; + + /** + * The angle, in degrees, of the camera angle from the nadir (directly facing the Earth). + * See {@link Builder#tilt(double)} for details of restrictions on the range of values. + */ + @Keep + public final double tilt; + + /** + * Zoom level near the center of the screen. See {@link Builder#zoom(double)} for the definition of the camera's + * zoom level. + */ + @Keep + public final double zoom; + + /** + * Padding in pixels. Specified in left, top, right, bottom order. + * See {@link Builder#padding(double[])} for the definition of the camera's padding. + */ + @Keep + public final double[] padding; + + /** + * Constructs a CameraPosition. + * + * @param target The target location to align with the center of the screen. + * @param zoom Zoom level at target. See zoom(float) for details of restrictions. + * @param tilt The camera angle, in degrees, from the nadir (directly down). See tilt(float) + * for details of restrictions. + * @param bearing Direction that the camera is pointing in, in degrees clockwise from north. + * This value will be normalized to be within 0 degrees inclusive and 360 degrees + * exclusive. + * @throws NullPointerException if target is null + * @throws IllegalArgumentException if tilt is outside the range of 0 to 90 degrees inclusive. + * @deprecated use {@link CameraPosition#CameraPosition(LatLng, double, double, double, double[])} instead. + */ + @Deprecated + CameraPosition(LatLng target, double zoom, double tilt, double bearing) { + this(target, zoom, tilt, bearing, null); + } + + /** + * Constructs a CameraPosition. + * + * @param target The target location to align with the center of the screen. + * @param zoom Zoom level at target. See zoom(float) for details of restrictions. + * @param tilt The camera angle, in degrees, from the nadir (directly down). See tilt(float) + * for details of restrictions. + * @param bearing Direction that the camera is pointing in, in degrees clockwise from north. + * This value will be normalized to be within 0 degrees inclusive and 360 degrees + * exclusive. + * @param padding Padding in pixels. Specified in left, top, right, bottom order. + * @throws NullPointerException if target is null + * @throws IllegalArgumentException if tilt is outside the range of 0 to 90 degrees inclusive. + */ + @Keep + CameraPosition(LatLng target, double zoom, double tilt, double bearing, double[] padding) { + this.target = target; + this.bearing = bearing; + this.tilt = tilt; + this.zoom = zoom; + this.padding = padding; + } + + /** + * Describe the kinds of special objects contained in this Parcelable's + * marshalled representation. + * + * @return integer 0. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a Parcel. + * + * @param out The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written. May be 0 or + * {@link #PARCELABLE_WRITE_RETURN_VALUE}. + */ + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeDouble(bearing); + out.writeParcelable(target, flags); + out.writeDouble(tilt); + out.writeDouble(zoom); + out.writeDoubleArray(padding); + } + + /** + * Returns a String with the camera target, zoom, bearing and tilt. + * + * @return A String with CameraPosition information. + */ + @Override + public String toString() { + return "Target: " + target + ", Zoom:" + zoom + ", Bearing:" + bearing + ", Tilt:" + tilt + + ", Padding:" + Arrays.toString(padding); + } + + /** + * Compares this {@link CameraPosition} object with another {@link CameraPosition} and + * determines if their target, zoom, tilt, and bearing match. + * + * @param o Another {@link CameraPosition} to compare with this object. + * @return True if target, zoom, tilt, and bearing match this {@link CameraPosition} object. + * Else, false. + */ + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + CameraPosition cameraPosition = (CameraPosition) o; + if (target != null && !target.equals(cameraPosition.target)) { + return false; + } else if (zoom != cameraPosition.zoom) { + return false; + } else if (tilt != cameraPosition.tilt) { + return false; + } else if (bearing != cameraPosition.bearing) { + return false; + } else if (!Arrays.equals(padding, cameraPosition.padding)) { + return false; + } + return true; + } + + /** + * Gives an integer which can be used as the bucket number for storing elements of the set/map. + * This bucket number is the address of the element inside the set/map. There's no guarantee + * that this hash value will be consistent between different Java implementations, or even + * between different execution runs of the same program. + * + * @return integer value you can use for storing element. + */ + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(bearing); + result = (int) (temp ^ (temp >>> 32)); + result = 31 * result + (target != null ? target.hashCode() : 0); + temp = Double.doubleToLongBits(tilt); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(zoom); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + Arrays.hashCode(padding); + return result; + } + + /** + * Builder for composing CameraPosition objects. + */ + public static final class Builder { + + private double bearing = -1; + @Nullable + private LatLng target = null; + private double tilt = -1; + private double zoom = -1; + private double[] padding = null; + + /** + * Create an empty builder. + */ + public Builder() { + super(); + } + + /** + * Create a builder with an existing CameraPosition data. + * + * @param previous Existing CameraPosition values to use + */ + public Builder(@Nullable CameraPosition previous) { + super(); + if (previous != null) { + this.bearing = previous.bearing; + this.target = previous.target; + this.tilt = previous.tilt; + this.zoom = previous.zoom; + this.padding = previous.padding; + } + } + + /** + * Create a builder with an existing CameraPosition data. + * + * @param typedArray TypedArray containing attribute values + */ + public Builder(@Nullable TypedArray typedArray) { + super(); + if (typedArray != null) { + this.bearing = typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_cameraBearing, 0.0f); + double lat = typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_cameraTargetLat, 0.0f); + double lng = typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_cameraTargetLng, 0.0f); + this.target = new LatLng(lat, lng); + this.tilt = typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_cameraTilt, 0.0f); + this.zoom = typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_cameraZoom, 0.0f); + } + } + + /** + * Create a builder from an existing CameraPositionUpdate update. + * + * @param update Update containing camera options + */ + public Builder(@Nullable CameraUpdateFactory.CameraPositionUpdate update) { + super(); + if (update != null) { + bearing = update.getBearing(); + target = update.getTarget(); + tilt = update.getTilt(); + zoom = update.getZoom(); + padding = update.getPadding(); + } + } + + /** + * Create builder from an existing CameraPositionUpdate update. + * + * @param update Update containing camera options + */ + public Builder(@Nullable CameraUpdateFactory.ZoomUpdate update) { + super(); + if (update != null) { + this.zoom = update.getZoom(); + } + } + + /** + * Sets the direction that the camera is pointing in, in degrees clockwise from north. + * + * @param bearing Bearing + * @return this + */ + @NonNull + public Builder bearing(double bearing) { + double direction = bearing; + + while (direction >= 360) { + direction -= 360; + } + while (direction < 0) { + direction += 360; + } + + this.bearing = direction; + return this; + } + + /** + * Sets the location where the camera is pointing at. + * + * @param location target of the camera + * @return this + */ + @NonNull + public Builder target(LatLng location) { + this.target = location; + return this; + } + + /** + * Set the tilt of the camera in degrees + *

+ * value is clamped to {@link MapboxConstants#MINIMUM_TILT} and {@link MapboxConstants#MAXIMUM_TILT}. + *

+ * + * @param tilt Tilt value of the camera + * @return this + */ + @NonNull + public Builder tilt(@FloatRange(from = MapboxConstants.MINIMUM_TILT, + to = MapboxConstants.MAXIMUM_TILT) double tilt) { + this.tilt = MathUtils.clamp(tilt, MapboxConstants.MINIMUM_TILT, MapboxConstants.MAXIMUM_TILT); + return this; + } + + /** + * Set the zoom of the camera + *

+ * Zoom ranges from {@link MapboxConstants#MINIMUM_ZOOM} to {@link MapboxConstants#MAXIMUM_ZOOM} + *

+ * + * @param zoom Zoom value of the camera + * @return this + */ + @NonNull + public Builder zoom(@FloatRange(from = MapboxConstants.MINIMUM_ZOOM, + to = MapboxConstants.MAXIMUM_ZOOM) double zoom) { + this.zoom = zoom; + return this; + } + + /** + * Padding in pixels that shifts the viewport by the specified amount. + * Applied padding is going to persist and impact following camera transformations. + *

+ * Specified in left, top, right, bottom order. + *

+ * + * @param padding Camera padding + * @return this + */ + @NonNull + public Builder padding(@Size(4) double[] padding) { + this.padding = padding; + return this; + } + + /** + * Padding in pixels that shifts the viewport by the specified amount. + * Applied padding is going to persist and impact following camera transformations. + *

+ * Specified in left, top, right, bottom order. + *

+ * + * @return this + */ + @NonNull + public Builder padding(double left, double top, double right, double bottom) { + this.padding = new double[] {left, top, right, bottom}; + return this; + } + + /** + * Builds the CameraPosition. + * + * @return CameraPosition + */ + public CameraPosition build() { + return new CameraPosition(target, zoom, tilt, bearing, padding); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdate.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdate.java new file mode 100644 index 000000000..39b937210 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdate.java @@ -0,0 +1,23 @@ +package com.mapbox.mapboxsdk.camera; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.mapbox.mapboxsdk.maps.MapboxMap; + +/** + * Interface definition for camera updates. + */ +public interface CameraUpdate { + + /** + * Get the camera position from the camera update. + * + * @param mapboxMap Map object to build the position from + * @return the camera position from the implementing camera update + */ + @Nullable + CameraPosition getCameraPosition(@NonNull MapboxMap mapboxMap); + +} + diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactory.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactory.java new file mode 100644 index 000000000..b611456f4 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/CameraUpdateFactory.java @@ -0,0 +1,637 @@ +package com.mapbox.mapboxsdk.camera; + +import android.graphics.Point; +import android.graphics.PointF; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.Projection; +import com.mapbox.mapboxsdk.maps.UiSettings; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; + +/** + * Factory for creating CameraUpdate objects. + */ +public final class CameraUpdateFactory { + + /** + * Returns a CameraUpdate that moves the camera to a specified CameraPosition. + * + * @param cameraPosition Camera Position to change to + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate newCameraPosition(@NonNull CameraPosition cameraPosition) { + return new CameraPositionUpdate(cameraPosition.bearing, cameraPosition.target, cameraPosition.tilt, + cameraPosition.zoom, cameraPosition.padding); + } + + /** + * Returns a CameraUpdate that moves the center of the screen to a latitude and longitude + * specified by a LatLng object. This centers the camera on the LatLng object. + * + * @param latLng Target location to change to + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate newLatLng(@NonNull LatLng latLng) { + return new CameraPositionUpdate(-1, latLng, -1, -1, null); + } + + /** + * Returns a CameraUpdate that transforms the camera such that the specified + * latitude/longitude bounds are centered on screen at the greatest possible zoom level while maintaining + * current camera position bearing and tilt values. + *

+ * You can specify padding, in order to inset the bounding box from the map view's edges. + * The padding will not persist and impact following camera transformations. + *

+ * + * @param bounds Bounds to match Camera position with + * @param padding Padding added to the bounds + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate newLatLngBounds(@NonNull LatLngBounds bounds, int padding) { + return newLatLngBounds(bounds, padding, padding, padding, padding); + } + + /** + * Returns a CameraUpdate that transforms the camera such that the specified + * latitude/longitude bounds are centered on screen at the greatest possible zoom level while using + * provided bearing and tilt values. + *

+ * You can specify padding, in order to inset the bounding box from the map view's edges. + * The padding will not persist and impact following camera transformations. + *

+ * + * @param bounds Bounds to match Camera position with + * @param bearing Bearing to take in account when generating the bounds + * @param tilt Tilt to take in account when generating the bounds + * @param padding Padding added to the bounds + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate newLatLngBounds(@NonNull LatLngBounds bounds, double bearing, double tilt, int padding) { + return newLatLngBounds(bounds, bearing, tilt, padding, padding, padding, padding); + } + + /** + * Returns a CameraUpdate that transforms the camera such that the specified + * latitude/longitude bounds are centered on screen at the greatest possible zoom level while maintaining + * current camera position bearing and tilt values. + *

+ * You can specify padding, in order to inset the bounding box from the map view's edges. + * The padding will not persist and impact following camera transformations. + *

+ * + * @param bounds Bounds to base the Camera position out of + * @param paddingLeft Padding left of the bounds + * @param paddingTop Padding top of the bounds + * @param paddingRight Padding right of the bounds + * @param paddingBottom Padding bottom of the bounds + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate newLatLngBounds(@NonNull LatLngBounds bounds, int paddingLeft, int paddingTop, + int paddingRight, int paddingBottom) { + return new CameraBoundsUpdate(bounds, null, null, paddingLeft, paddingTop, paddingRight, paddingBottom); + } + + /** + * Returns a CameraUpdate that transforms the camera such that the specified + * latitude/longitude bounds are centered on screen at the greatest possible zoom level while using + * provided bearing and tilt values. + *

+ * You can specify padding, in order to inset the bounding box from the map view's edges. + * The padding will not persist and impact following camera transformations. + *

+ * + * @param bounds Bounds to base the Camera position out of + * @param bearing Bearing to take in account when generating the bounds + * @param tilt Tilt to take in account when generating the bounds + * @param paddingLeft Padding left of the bounds + * @param paddingTop Padding top of the bounds + * @param paddingRight Padding right of the bounds + * @param paddingBottom Padding bottom of the bounds + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate newLatLngBounds(@NonNull LatLngBounds bounds, double bearing, double tilt, + int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) { + return new CameraBoundsUpdate(bounds, bearing, tilt, paddingLeft, paddingTop, paddingRight, paddingBottom); + } + + /** + * Returns a CameraUpdate that moves the center of the screen to a latitude and longitude + * specified by a LatLng object taking the specified padding into account. + * + * @param latLng Target location to change to + * @param zoom Zoom level to change to + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate newLatLngZoom(@NonNull LatLng latLng, double zoom) { + return new CameraPositionUpdate(-1, latLng, -1, zoom, null); + } + + /** + * Returns a CameraUpdate that moves the center of the screen to a latitude and longitude + * specified by a LatLng object, and moves to the given zoom level. + * Applied padding is going to persist and impact following camera transformations. + * + * @param latLng Target location to change to + * @param left Left padding + * @param top Top padding + * @param right Right padding + * @param bottom Bottom padding + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate newLatLngPadding(@NonNull LatLng latLng, + double left, double top, double right, double bottom) { + return new CameraPositionUpdate(-1, latLng, -1, -1, new double[] {left, top, right, bottom}); + } + + /** + * Returns a CameraUpdate that shifts the zoom level of the current camera viewpoint. + * + * @param amount Amount of zoom level to change with + * @param focus Focus point of zoom + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate zoomBy(double amount, Point focus) { + return new ZoomUpdate(amount, focus.x, focus.y); + } + + /** + * Returns a CameraUpdate that shifts the zoom level of the current camera viewpoint. + * + * @param amount Amount of zoom level to change with + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate zoomBy(double amount) { + return new ZoomUpdate(ZoomUpdate.ZOOM_BY, amount); + } + + /** + * Returns a CameraUpdate that zooms in on the map by moving the viewpoint's height closer to + * the Earth's surface. The zoom increment is 1.0. + * + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate zoomIn() { + return new ZoomUpdate(ZoomUpdate.ZOOM_IN); + } + + /** + * Returns a CameraUpdate that zooms out on the map by moving the viewpoint's height farther + * away from the Earth's surface. The zoom increment is -1.0. + * + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate zoomOut() { + return new ZoomUpdate(ZoomUpdate.ZOOM_OUT); + } + + /** + * Returns a CameraUpdate that moves the camera viewpoint to a particular zoom level. + * + * @param zoom Zoom level to change to + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate zoomTo(double zoom) { + return new ZoomUpdate(ZoomUpdate.ZOOM_TO, zoom); + } + + /** + * Returns a CameraUpdate that moves the camera viewpoint to a particular bearing. + * + * @param bearing Bearing to change to + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate bearingTo(double bearing) { + return new CameraPositionUpdate(bearing, null, -1, -1, null); + } + + /** + * Returns a CameraUpdate that moves the camera viewpoint to a particular tilt. + * + * @param tilt Tilt to change to + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate tiltTo(double tilt) { + return new CameraPositionUpdate(-1, null, tilt, -1, null); + } + + /** + * Returns a CameraUpdate that when animated changes the camera padding. + * Applied padding is going to persist and impact following camera transformations. + *

+ * Specified in left, top, right, bottom order. + *

+ * + * @param padding Padding to change to + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate paddingTo(double[] padding) { + return new CameraPositionUpdate(-1, null, -1, -1, padding); + } + + /** + * Returns a CameraUpdate that when animated changes the camera padding. + * Applied padding is going to persist and impact following camera transformations. + *

+ * Specified in left, top, right, bottom order. + *

+ * + * @return CameraUpdate Final Camera Position + */ + public static CameraUpdate paddingTo(double left, double top, double right, double bottom) { + return paddingTo(new double[] {left, top, right, bottom}); + } + + // + // CameraUpdate types + // + + static final class CameraPositionUpdate implements CameraUpdate { + + private final double bearing; + private final LatLng target; + private final double tilt; + private final double zoom; + private final double[] padding; + + CameraPositionUpdate(double bearing, LatLng target, double tilt, double zoom, double[] padding) { + this.bearing = bearing; + this.target = target; + this.tilt = tilt; + this.zoom = zoom; + this.padding = padding; + } + + public LatLng getTarget() { + return target; + } + + public double getBearing() { + return bearing; + } + + public double getTilt() { + return tilt; + } + + public double getZoom() { + return zoom; + } + + public double[] getPadding() { + return padding; + } + + @Override + public CameraPosition getCameraPosition(@NonNull MapboxMap mapboxMap) { + if (target == null) { + CameraPosition previousPosition = mapboxMap.getCameraPosition(); + return new CameraPosition.Builder(this) + .target(previousPosition.target) + .build(); + } + return new CameraPosition.Builder(this).build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + CameraPositionUpdate that = (CameraPositionUpdate) o; + + if (Double.compare(that.bearing, bearing) != 0) { + return false; + } + if (Double.compare(that.tilt, tilt) != 0) { + return false; + } + if (Double.compare(that.zoom, zoom) != 0) { + return false; + } + if (target != null ? !target.equals(that.target) : that.target != null) { + return false; + } + return Arrays.equals(padding, that.padding); + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(bearing); + result = (int) (temp ^ (temp >>> 32)); + result = 31 * result + (target != null ? target.hashCode() : 0); + temp = Double.doubleToLongBits(tilt); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(zoom); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + Arrays.hashCode(padding); + return result; + } + + @Override + public String toString() { + return "CameraPositionUpdate{" + + "bearing=" + bearing + + ", target=" + target + + ", tilt=" + tilt + + ", zoom=" + zoom + + ", padding=" + Arrays.toString(padding) + + '}'; + } + } + + static final class CameraBoundsUpdate implements CameraUpdate { + + private final LatLngBounds bounds; + private final int[] padding; + private final Double bearing; + private final Double tilt; + + CameraBoundsUpdate(LatLngBounds bounds, Double bearing, Double tilt, int[] padding) { + this.bounds = bounds; + this.padding = padding; + this.bearing = bearing; + this.tilt = tilt; + } + + CameraBoundsUpdate(LatLngBounds bounds, Double bearing, Double tilt, int paddingLeft, + int paddingTop, int paddingRight, int paddingBottom) { + this(bounds, bearing, tilt, new int[] {paddingLeft, paddingTop, paddingRight, paddingBottom}); + } + + public LatLngBounds getBounds() { + return bounds; + } + + public int[] getPadding() { + return padding; + } + + @Override + public CameraPosition getCameraPosition(@NonNull MapboxMap mapboxMap) { + if (bearing == null && tilt == null) { + // use current camera position tilt and bearing + return mapboxMap.getCameraForLatLngBounds(bounds, padding); + } else { + // use provided tilt and bearing + assert bearing != null; + assert tilt != null; + return mapboxMap.getCameraForLatLngBounds(bounds, padding, bearing, tilt); + } + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + CameraBoundsUpdate that = (CameraBoundsUpdate) o; + + if (!bounds.equals(that.bounds)) { + return false; + } + return Arrays.equals(padding, that.padding); + } + + @Override + public int hashCode() { + int result = bounds.hashCode(); + result = 31 * result + Arrays.hashCode(padding); + return result; + } + + @Override + public String toString() { + return "CameraBoundsUpdate{" + + "bounds=" + bounds + + ", padding=" + Arrays.toString(padding) + + '}'; + } + } + + static final class CameraMoveUpdate implements CameraUpdate { + + private float x; + private float y; + + CameraMoveUpdate(float x, float y) { + this.x = x; + this.y = y; + } + + @Override + public CameraPosition getCameraPosition(@NonNull MapboxMap mapboxMap) { + UiSettings uiSettings = mapboxMap.getUiSettings(); + Projection projection = mapboxMap.getProjection(); + // Calculate the new center point + float viewPortWidth = uiSettings.getWidth(); + float viewPortHeight = uiSettings.getHeight(); + int[] padding = mapboxMap.getPadding(); + + // we inverse the map padding, is reapplied when using moveTo/easeTo or animateTo + PointF targetPoint = new PointF( + (viewPortWidth - padding[0] + padding[1]) / 2 + x, + (viewPortHeight + padding[1] - padding[3]) / 2 + y + ); + + LatLng latLng = projection.fromScreenLocation(targetPoint); + CameraPosition previousPosition = mapboxMap.getCameraPosition(); + CameraPosition position = + new CameraPosition.Builder() + .target(latLng) + .zoom(previousPosition.zoom) + .tilt(previousPosition.tilt) + .bearing(previousPosition.bearing) + .build(); + return position; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + CameraMoveUpdate that = (CameraMoveUpdate) o; + + if (Float.compare(that.x, x) != 0) { + return false; + } + return Float.compare(that.y, y) == 0; + } + + @Override + public int hashCode() { + int result = (x != +0.0f ? Float.floatToIntBits(x) : 0); + result = 31 * result + (y != +0.0f ? Float.floatToIntBits(y) : 0); + return result; + } + + @Override + public String toString() { + return "CameraMoveUpdate{" + + "x=" + x + + ", y=" + y + + '}'; + } + } + + static final class ZoomUpdate implements CameraUpdate { + + @IntDef( {ZOOM_IN, ZOOM_OUT, ZOOM_BY, ZOOM_TO, ZOOM_TO_POINT}) + @Retention(RetentionPolicy.SOURCE) + @interface Type { + } + + static final int ZOOM_IN = 0; + static final int ZOOM_OUT = 1; + static final int ZOOM_BY = 2; + static final int ZOOM_TO = 3; + static final int ZOOM_TO_POINT = 4; + + @Type + private final int type; + private final double zoom; + private float x; + private float y; + + ZoomUpdate(@Type int type) { + this.type = type; + this.zoom = 0; + } + + ZoomUpdate(@Type int type, double zoom) { + this.type = type; + this.zoom = zoom; + } + + ZoomUpdate(double zoom, float x, float y) { + this.type = ZOOM_TO_POINT; + this.zoom = zoom; + this.x = x; + this.y = y; + } + + public double getZoom() { + return zoom; + } + + @Type + public int getType() { + return type; + } + + public float getX() { + return x; + } + + public float getY() { + return y; + } + + double transformZoom(double currentZoom) { + switch (getType()) { + case CameraUpdateFactory.ZoomUpdate.ZOOM_IN: + currentZoom++; + break; + case CameraUpdateFactory.ZoomUpdate.ZOOM_OUT: + currentZoom--; + if (currentZoom < 0) { + currentZoom = 0; + } + break; + case CameraUpdateFactory.ZoomUpdate.ZOOM_TO: + currentZoom = getZoom(); + break; + case CameraUpdateFactory.ZoomUpdate.ZOOM_BY: + currentZoom = currentZoom + getZoom(); + break; + case CameraUpdateFactory.ZoomUpdate.ZOOM_TO_POINT: + currentZoom = currentZoom + getZoom(); + break; + } + return currentZoom; + } + + @Override + public CameraPosition getCameraPosition(@NonNull MapboxMap mapboxMap) { + CameraPosition cameraPosition = mapboxMap.getCameraPosition(); + if (getType() != CameraUpdateFactory.ZoomUpdate.ZOOM_TO_POINT) { + return new CameraPosition.Builder(cameraPosition) + .zoom(transformZoom(cameraPosition.zoom)) + .build(); + } else { + return new CameraPosition.Builder(cameraPosition) + .zoom(transformZoom(cameraPosition.zoom)) + .target(mapboxMap.getProjection().fromScreenLocation(new PointF(getX(), getY()))) + .build(); + } + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ZoomUpdate that = (ZoomUpdate) o; + + if (type != that.type) { + return false; + } + if (Double.compare(that.zoom, zoom) != 0) { + return false; + } + if (Float.compare(that.x, x) != 0) { + return false; + } + return Float.compare(that.y, y) == 0; + } + + @Override + public int hashCode() { + int result; + long temp; + result = type; + temp = Double.doubleToLongBits(zoom); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + (x != +0.0f ? Float.floatToIntBits(x) : 0); + result = 31 * result + (y != +0.0f ? Float.floatToIntBits(y) : 0); + return result; + } + + @Override + public String toString() { + return "ZoomUpdate{" + + "type=" + type + + ", zoom=" + zoom + + ", x=" + x + + ", y=" + y + + '}'; + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/package-info.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/package-info.java new file mode 100644 index 000000000..04ef699c2 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/camera/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains the Mapbox Maps Android Camera API classes. + */ +package com.mapbox.mapboxsdk.camera; diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeometryConstants.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeometryConstants.java new file mode 100644 index 000000000..6f6e77792 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeometryConstants.java @@ -0,0 +1,67 @@ +package com.mapbox.mapboxsdk.constants; + +/** + * Contains constants used throughout the sdk classes. + */ +public class GeometryConstants { + + /** + * The equatorial radius + * value in meters + */ + public static final int RADIUS_EARTH_METERS = 6378137; + + /** + * This constant represents the lowest longitude value available to represent a wrapped geolocation. + */ + public static final double MIN_WRAP_LONGITUDE = -180; + + /** + * This constant represents the highest longitude value available to represent a wrapped geolocation. + */ + public static final double MAX_WRAP_LONGITUDE = 180; + + /** + * This constant represents the lowest longitude value available to represent a geolocation. + */ + public static final double MIN_LONGITUDE = -Double.MAX_VALUE; + + /** + * This constant represents the highest longitude value available to represent a geolocation. + */ + public static final double MAX_LONGITUDE = Double.MAX_VALUE; + + /** + * This constant represents the lowest latitude value available to represent a geolocation. + */ + public static final double MIN_LATITUDE = -90; + + /** + * This constant represents the latitude span when representing a geolocation. + */ + public static final double LATITUDE_SPAN = 180; + + /** + * This constant represents the longitude span when representing a geolocation. + */ + public static final double LONGITUDE_SPAN = 360; + + /** + * This constant represents the highest latitude value available to represent a geolocation. + */ + public static final double MAX_LATITUDE = 90; + + /** + * Maximum latitude value in Mercator projection. + */ + public static final double MAX_MERCATOR_LATITUDE = 85.05112877980659; + + /** + * Minimum latitude value in Mercator projection. + */ + public static final double MIN_MERCATOR_LATITUDE = -85.05112877980659; + + private GeometryConstants() { + // Private constructor to prevent initializing of this class. + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java new file mode 100644 index 000000000..6156b3af7 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java @@ -0,0 +1,232 @@ +package com.mapbox.mapboxsdk.constants; + +import java.util.Locale; + +/** + * MapboxConstants exposes Mapbox related constants + */ +public class MapboxConstants { + + /** + * Default Locale for data processing (ex: String.toLowerCase(MAPBOX_LOCALE, "foo")) + */ + public static final Locale MAPBOX_LOCALE = Locale.US; + + /** + * The name of the desired preferences file for Android's SharedPreferences. + */ + public static final String MAPBOX_SHARED_PREFERENCES = "MapboxSharedPreferences"; + + /** + * Key used to switch storage to external in AndroidManifest.xml + */ + public static final String KEY_META_DATA_SET_STORAGE_EXTERNAL = "com.mapbox.SetStorageExternal"; + + /** + * Default value for KEY_META_DATA_SET_STORAGE_EXTERNAL (default is internal storage) + */ + public static final boolean DEFAULT_SET_STORAGE_EXTERNAL = false; + + /** + * Key used to switch Tile Download Measuring on/off in AndroidManifest.xml + */ + public static final String KEY_META_DATA_MEASURE_TILE_DOWNLOAD_ON = "com.mapbox.MeasureTileDownloadOn"; + + /** + * Default value for KEY_META_DATA_MEASURE_TILE_DOWNLOAD_ON (default is off) + */ + public static final boolean DEFAULT_MEASURE_TILE_DOWNLOAD_ON = false; + + public static final String KEY_PREFERENCE_SKU_TOKEN = "com.mapbox.mapboxsdk.accounts.skutoken"; + + public static final String KEY_META_DATA_MANAGE_SKU_TOKEN = "com.mapbox.ManageSkuToken"; + + public static final boolean DEFAULT_MANAGE_SKU_TOKEN = true; + + /** + * Default value for font fallback for local ideograph fonts + */ + public static final String DEFAULT_FONT = "sans-serif"; + + /** + * Unmeasured state + */ + public static final float UNMEASURED = -1f; + + /** + * Default animation time + */ + public static final int ANIMATION_DURATION = 300; + + /** + * Default short animation time + */ + public static final int ANIMATION_DURATION_SHORT = 150; + + /** + * Animation time of a fling gesture + */ + public static final long ANIMATION_DURATION_FLING_BASE = ANIMATION_DURATION_SHORT; + + /** + * Velocity threshold for a fling gesture + */ + public static final long VELOCITY_THRESHOLD_IGNORE_FLING = 1000; + + /** + * Value by which the default rotation threshold will be increased when scaling + * + * @deprecated unused, see {@link com.mapbox.mapboxsdk.maps.UiSettings#setDisableRotateWhenScaling(boolean)} + */ + @Deprecated + public static final float ROTATION_THRESHOLD_INCREASE_WHEN_SCALING = 25f; + + /** + * Maximum absolute zoom change for multi-pointer scale velocity animation + */ + public static final double MAX_ABSOLUTE_SCALE_VELOCITY_CHANGE = 2.5; + + /** + * Maximum possible zoom change during the quick zoom gesture executed across the whole screen + */ + public static final double QUICK_ZOOM_MAX_ZOOM_CHANGE = 4.0; + + /** + * Scale velocity animation duration multiplier. + */ + public static final double SCALE_VELOCITY_ANIMATION_DURATION_MULTIPLIER = 150; + + /** + * Minimum angular velocity for rotation animation + * + * @deprecated unused, see {@link #ROTATE_VELOCITY_RATIO_THRESHOLD} + */ + @Deprecated + public static final float MINIMUM_ANGULAR_VELOCITY = 1.5f; + + /** + * Last scale span delta to XY velocity ratio required to execute scale velocity animation. + */ + public static final double SCALE_VELOCITY_RATIO_THRESHOLD = 4 * 1e-3; + + /** + * Last rotation delta to XY velocity ratio required to execute rotation velocity animation. + */ + public static final double ROTATE_VELOCITY_RATIO_THRESHOLD = 2.2 * 1e-4; + + /** + * Time within which user needs to lift fingers for velocity animation to start. + */ + public static final long SCHEDULED_ANIMATION_TIMEOUT = 150L; + + /** + * Maximum angular velocity for rotation animation + */ + public static final float MAXIMUM_ANGULAR_VELOCITY = 30f; + + /** + * Factor to calculate tilt change based on pixel change during shove gesture. + */ + public static final float SHOVE_PIXEL_CHANGE_FACTOR = 0.1f; + + /** + * The currently supported minimum zoom level. + */ + public static final float MINIMUM_ZOOM = 0.0f; + + /** + * The currently supported maximum zoom level. + */ + public static final float MAXIMUM_ZOOM = 25.5f; + + /** + * The currently supported maximum tilt value. + */ + public static final double MAXIMUM_TILT = 60; + + /** + * The currently supported minimum tilt value. + */ + public static final double MINIMUM_TILT = 0; + + /** + * The currently supported maximum direction + */ + public static final double MAXIMUM_DIRECTION = 360; + + /** + * The currently supported minimum direction + */ + public static final double MINIMUM_DIRECTION = 0; + + /** + * The currently used minimum scale factor to clamp to when a quick zoom gesture occurs + * + * @deprecated unused + */ + @Deprecated + public static final float MINIMUM_SCALE_FACTOR_CLAMP = 0.00f; + + /** + * The currently used maximum scale factor to clamp to when a quick zoom gesture occurs + * + * @deprecated unused + */ + @Deprecated + public static final float MAXIMUM_SCALE_FACTOR_CLAMP = 0.15f; + + /** + * Zoom value multiplier for scale gestures. + */ + public static final float ZOOM_RATE = 0.65f; + + /** + * Fragment Argument Key for MapboxMapOptions + */ + public static final String FRAG_ARG_MAPBOXMAPOPTIONS = "MapboxMapOptions"; + + /** + * Layer Id of annotations layer + */ + public static final String LAYER_ID_ANNOTATIONS = "com.mapbox.annotations.points"; + + // Save instance state keys + public static final String STATE_HAS_SAVED_STATE = "mapbox_savedState"; + public static final String STATE_CAMERA_POSITION = "mapbox_cameraPosition"; + public static final String STATE_ZOOM_ENABLED = "mapbox_zoomEnabled"; + public static final String STATE_SCROLL_ENABLED = "mapbox_scrollEnabled"; + public static final String STATE_ROTATE_ENABLED = "mapbox_rotateEnabled"; + public static final String STATE_TILT_ENABLED = "mapbox_tiltEnabled"; + public static final String STATE_DOUBLE_TAP_ENABLED = "mapbox_doubleTapEnabled"; + public static final String STATE_QUICK_ZOOM_ENABLED = "mapbox_quickZoom"; + public static final String STATE_ZOOM_RATE = "mapbox_zoomRate"; + public static final String STATE_DEBUG_ACTIVE = "mapbox_debugActive"; + public static final String STATE_COMPASS_ENABLED = "mapbox_compassEnabled"; + public static final String STATE_COMPASS_GRAVITY = "mapbox_compassGravity"; + public static final String STATE_COMPASS_MARGIN_LEFT = "mapbox_compassMarginLeft"; + public static final String STATE_COMPASS_MARGIN_TOP = "mapbox_compassMarginTop"; + public static final String STATE_COMPASS_MARGIN_RIGHT = "mapbox_compassMarginRight"; + public static final String STATE_COMPASS_MARGIN_BOTTOM = "mapbox_compassMarginBottom"; + public static final String STATE_COMPASS_FADE_WHEN_FACING_NORTH = "mapbox_compassFade"; + public static final String STATE_COMPASS_IMAGE_BITMAP = "mapbox_compassImage"; + public static final String STATE_LOGO_GRAVITY = "mapbox_logoGravity"; + public static final String STATE_LOGO_MARGIN_LEFT = "mapbox_logoMarginLeft"; + public static final String STATE_LOGO_MARGIN_TOP = "mapbox_logoMarginTop"; + public static final String STATE_LOGO_MARGIN_RIGHT = "mapbox_logoMarginRight"; + public static final String STATE_LOGO_MARGIN_BOTTOM = "mapbox_logoMarginBottom"; + public static final String STATE_LOGO_ENABLED = "mapbox_logoEnabled"; + public static final String STATE_ATTRIBUTION_GRAVITY = "mapbox_attrGravity"; + public static final String STATE_ATTRIBUTION_MARGIN_LEFT = "mapbox_attrMarginLeft"; + public static final String STATE_ATTRIBUTION_MARGIN_TOP = "mapbox_attrMarginTop"; + public static final String STATE_ATTRIBUTION_MARGIN_RIGHT = "mapbox_attrMarginRight"; + public static final String STATE_ATTRIBUTION_MARGIN_BOTTOM = "mapbox_atrrMarginBottom"; + public static final String STATE_ATTRIBUTION_ENABLED = "mapbox_atrrEnabled"; + public static final String STATE_DESELECT_MARKER_ON_TAP = "mapbox_deselectMarkerOnTap"; + public static final String STATE_USER_FOCAL_POINT = "mapbox_userFocalPoint"; + public static final String STATE_SCALE_ANIMATION_ENABLED = "mapbox_scaleAnimationEnabled"; + public static final String STATE_ROTATE_ANIMATION_ENABLED = "mapbox_rotateAnimationEnabled"; + public static final String STATE_FLING_ANIMATION_ENABLED = "mapbox_flingAnimationEnabled"; + public static final String STATE_INCREASE_ROTATE_THRESHOLD = "mapbox_increaseRotateThreshold"; + public static final String STATE_DISABLE_ROTATE_WHEN_SCALING = "mapbox_disableRotateWhenScaling"; + public static final String STATE_INCREASE_SCALE_THRESHOLD = "mapbox_increaseScaleThreshold"; +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/TelemetryConstants.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/TelemetryConstants.java new file mode 100644 index 000000000..3dbae9de4 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/TelemetryConstants.java @@ -0,0 +1,14 @@ +package com.mapbox.mapboxsdk.constants; + +@Deprecated +public class TelemetryConstants { + + public static final String TWO_FINGER_TAP = "TwoFingerTap"; + public static final String DOUBLE_TAP = "DoubleTap"; + public static final String SINGLE_TAP = "SingleTap"; + public static final String PAN = "Pan"; + public static final String PINCH = "Pinch"; + public static final String ROTATION = "Rotation"; + public static final String PITCH = "Pitch"; + +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/package-info.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/package-info.java new file mode 100644 index 000000000..165394c03 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains the Mapbox Maps Android Constant API classes. + */ +package com.mapbox.mapboxsdk.constants; diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/CalledFromWorkerThreadException.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/CalledFromWorkerThreadException.java new file mode 100644 index 000000000..32e852d31 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/CalledFromWorkerThreadException.java @@ -0,0 +1,8 @@ +package com.mapbox.mapboxsdk.exceptions; + +public class CalledFromWorkerThreadException extends RuntimeException { + + public CalledFromWorkerThreadException(String message) { + super(message); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/ConversionException.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/ConversionException.java new file mode 100644 index 000000000..0d16c53b8 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/ConversionException.java @@ -0,0 +1,11 @@ +package com.mapbox.mapboxsdk.exceptions; + +/** + * A ConversionException is thrown when a conversion failed to execute. + */ +public class ConversionException extends RuntimeException { + + public ConversionException(String detailMessage) { + super(detailMessage); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/IconBitmapChangedException.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/IconBitmapChangedException.java new file mode 100644 index 000000000..1f6b0efc4 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/IconBitmapChangedException.java @@ -0,0 +1,25 @@ +package com.mapbox.mapboxsdk.exceptions; + +import com.mapbox.mapboxsdk.annotations.Icon; +import com.mapbox.mapboxsdk.annotations.Marker; +import com.mapbox.mapboxsdk.maps.MapView; + +/** + *

+ * An IconBitmapChangedException is thrown by MapView when a Marker is added + * that has an Icon with a Bitmap that has been modified since the creation of the Icon. + *

+ * You cannot modify a {@code Icon} after it has been added to the map in a {@code Marker} + * + * @see MapView + * @see Icon + * @see Marker + */ +public class IconBitmapChangedException extends RuntimeException { + + public IconBitmapChangedException() { + super("The added Marker has an Icon with a bitmap that has been modified. An Icon cannot be modified" + + "after it has been added to the map in a Marker."); + } + +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidLatLngBoundsException.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidLatLngBoundsException.java new file mode 100644 index 000000000..c1d038581 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidLatLngBoundsException.java @@ -0,0 +1,12 @@ +package com.mapbox.mapboxsdk.exceptions; + +/** + * An InvalidLatLngBoundsException is thrown by LatLngBounds + * when there aren't enough LatLng to create a bounds. + */ +public class InvalidLatLngBoundsException extends RuntimeException { + + public InvalidLatLngBoundsException(int latLngsListSize) { + super("Cannot create a LatLngBounds from " + latLngsListSize + " items"); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidMarkerPositionException.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidMarkerPositionException.java new file mode 100644 index 000000000..44ee83265 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/InvalidMarkerPositionException.java @@ -0,0 +1,16 @@ +package com.mapbox.mapboxsdk.exceptions; + +/** + * A InvalidMarkerPositionException is thrown when a Marker object is created with an invalid LatLng position. + */ +public class InvalidMarkerPositionException extends RuntimeException { + + /** + * Creates a invalid marker position exception thrown when a Marker object is created with an invalid LatLng position. + */ + public InvalidMarkerPositionException() { + super("Adding an invalid Marker to a Map. " + + "Missing the required position field. " + + "Provide a non null LatLng as position to the Marker."); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/MapboxConfigurationException.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/MapboxConfigurationException.java new file mode 100644 index 000000000..431de85f0 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/MapboxConfigurationException.java @@ -0,0 +1,33 @@ +package com.mapbox.mapboxsdk.exceptions; + +import android.content.Context; +import android.support.annotation.NonNull; + +/** + * A MapboxConfigurationException is thrown by MapboxMap when the SDK hasn't been properly initialised. + *

+ * This occurs either when {@link com.mapbox.mapboxsdk.Mapbox} is not correctly initialised or the provided access token + * through {@link com.mapbox.mapboxsdk.Mapbox#getInstance(Context, String)} isn't valid. + *

+ * + * @see com.mapbox.mapboxsdk.Mapbox#getInstance(Context, String) + */ +public class MapboxConfigurationException extends RuntimeException { + + /** + * Creates a Mapbox configuration exception thrown by MapboxMap when the SDK hasn't been properly initialised. + */ + public MapboxConfigurationException() { + super("\nUsing MapView requires calling Mapbox.getInstance(Context context, String accessToken) before " + + "inflating or creating the view. The access token parameter is required when using a Mapbox service." + + "\nPlease see https://www.mapbox.com/help/create-api-access-token/ to learn how to create one." + + "\nMore information in this guide https://www.mapbox.com/help/first-steps-android-sdk/#access-tokens."); + } + + /** + * Creates a Mapbox configuration exception thrown by MapboxMap when the SDK hasn't been properly initialised. + */ + public MapboxConfigurationException(@NonNull String message) { + super(message); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/TooManyIconsException.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/TooManyIconsException.java new file mode 100644 index 000000000..bffc10dc0 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/TooManyIconsException.java @@ -0,0 +1,20 @@ +package com.mapbox.mapboxsdk.exceptions; + +import com.mapbox.mapboxsdk.annotations.IconFactory; + +/** + *

+ * A TooManyIconsException is thrown by IconFactory when it + * cannot create a Icon because there are already too many icons created. + *

+ * You should try to reuse Icon objects whenever possible. + * + * @see IconFactory + */ +public class TooManyIconsException extends RuntimeException { + + public TooManyIconsException() { + super("Cannot create an Icon because there are already too many. Try reusing Icon objects whenever possible."); + } + +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/package-info.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/package-info.java new file mode 100644 index 000000000..09ecd55c1 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/exceptions/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains the Mapbox Maps Android Exception API classes. + */ +package com.mapbox.mapboxsdk.exceptions; diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLng.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLng.java new file mode 100644 index 000000000..ce12489b4 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLng.java @@ -0,0 +1,334 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.location.Location; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.FloatRange; +import android.support.annotation.Keep; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.mapbox.geojson.Point; +import com.mapbox.mapboxsdk.constants.GeometryConstants; +import com.mapbox.turf.TurfMeasurement; + +import static com.mapbox.turf.TurfConstants.UNIT_METRES; + + +/** + * A geographical location which contains a single latitude, longitude pair, with + * optional altitude. + *

+ * Latitude and longitude are expressed as decimal degrees + * in the WGS84 datum. By default, altitude is 0.0, or sea level. + *

+ *

+ * Mapbox GL displays maps in the Mercator Projection and projects geographical + * data automatically, so all data enters in the WGS84 datum. + *

+ */ +public class LatLng implements Parcelable { + + /** + * Inner class responsible for recreating Parcels into objects. + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public LatLng createFromParcel(@NonNull Parcel in) { + return new LatLng(in); + } + + public LatLng[] newArray(int size) { + return new LatLng[size]; + } + }; + + @Keep + private double latitude; + @Keep + private double longitude; + private double altitude = 0.0; + + /** + * Construct a new latitude, longitude point at (0, 0) + */ + public LatLng() { + this.latitude = 0.0; + this.longitude = 0.0; + } + + /** + * Construct a new latitude, longitude point given double arguments + * + * @param latitude Latitude in degrees + * @param longitude Longitude in degrees + */ + @Keep + public LatLng(double latitude, double longitude) { + setLatitude(latitude); + setLongitude(longitude); + } + + /** + * Construct a new latitude, longitude, altitude point given double arguments + * + * @param latitude Latitude in degrees + * @param longitude Longitude in degress + * @param altitude Altitude in meters + */ + public LatLng(double latitude, double longitude, double altitude) { + setLatitude(latitude); + setLongitude(longitude); + setAltitude(altitude); + } + + /** + * Construct a new latitude, longitude, altitude point given location argument + * + * @param location Android Location + */ + public LatLng(Location location) { + this(location.getLatitude(), location.getLongitude(), location.getAltitude()); + } + + /** + * Construct a new latitude, longitude, altitude point given another latitude, longitude, altitude point. + * + * @param latLng LatLng to be cloned. + */ + public LatLng(LatLng latLng) { + this.latitude = latLng.latitude; + this.longitude = latLng.longitude; + this.altitude = latLng.altitude; + } + + /** + * Constructs a new latitude, longitude, altitude tuple given a parcel. + * + * @param in the parcel containing the latitude, longitude, altitude values + */ + protected LatLng(Parcel in) { + setLatitude(in.readDouble()); + setLongitude(in.readDouble()); + setAltitude(in.readDouble()); + } + + /** + * Set the latitude, in degrees. + *

+ * This value is in the range of [-90, 90], see {@link GeometryConstants#MIN_LATITUDE} and + * {@link GeometryConstants#MAX_LATITUDE} + *

+ * + * @param latitude the latitude value in degrees + * @see GeometryConstants#MIN_LATITUDE + * @see GeometryConstants#MAX_LATITUDE + */ + public void setLatitude( + @FloatRange(from = GeometryConstants.MIN_LATITUDE, to = GeometryConstants.MAX_LATITUDE) double latitude) { + if (Double.isNaN(latitude)) { + throw new IllegalArgumentException("latitude must not be NaN"); + } + if (Math.abs(latitude) > GeometryConstants.MAX_LATITUDE) { + throw new IllegalArgumentException("latitude must be between -90 and 90"); + } + this.latitude = latitude; + } + + /** + * Get the latitude, in degrees. + *

+ * This value is in the range of [-90, 90], see {@link GeometryConstants#MIN_LATITUDE} and + * {@link GeometryConstants#MAX_LATITUDE} + *

+ * + * @return the latitude value in degrees + * @see GeometryConstants#MIN_LATITUDE + * @see GeometryConstants#MAX_LATITUDE + */ + public double getLatitude() { + return latitude; + } + + /** + * Set the longitude, in degrees. + *

+ * This value is in the range of [-180, 180], see {@link GeometryConstants#MIN_LONGITUDE} and + * {@link GeometryConstants#MAX_LONGITUDE} + *

+ * + * @param longitude the longitude value in degrees + * @see GeometryConstants#MIN_LONGITUDE + * @see GeometryConstants#MAX_LONGITUDE + */ + public void setLongitude(@FloatRange(from = GeometryConstants.MIN_LONGITUDE, to = GeometryConstants.MAX_LONGITUDE) + double longitude) { + if (Double.isNaN(longitude)) { + throw new IllegalArgumentException("longitude must not be NaN"); + } + if (Double.isInfinite(longitude)) { + throw new IllegalArgumentException("longitude must not be infinite"); + } + this.longitude = longitude; + } + + /** + * Get the longitude, in degrees. + *

+ * This value is in the range of [-180, 180], see {@link GeometryConstants#MIN_LONGITUDE} and + * {@link GeometryConstants#MAX_LONGITUDE} + *

+ * + * @return the longitude value in degrees + * @see GeometryConstants#MIN_LONGITUDE + * @see GeometryConstants#MAX_LONGITUDE + */ + public double getLongitude() { + return longitude; + } + + /** + * Set the altitude, in meters. + * + * @param altitude the altitude in meters + */ + public void setAltitude(double altitude) { + this.altitude = altitude; + } + + /** + * Get the altitude, in meters. + * + * @return the altitude value in meters + */ + public double getAltitude() { + return altitude; + } + + /** + * Return a new LatLng object with a wrapped Longitude. This allows original data object + * to remain unchanged. + * + * @return new LatLng object with wrapped Longitude + */ + @NonNull + public LatLng wrap() { + return new LatLng(latitude, wrap(longitude, + GeometryConstants.MIN_WRAP_LONGITUDE, GeometryConstants.MAX_WRAP_LONGITUDE) + ); + } + + + /** + * Constrains value to the given range (including min & max) via modular arithmetic. + *

+ * Same formula as used in Core GL (wrap.hpp) + * std::fmod((std::fmod((value - min), d) + d), d) + min; + *

+ *

+ * Multiples of max value will be wrapped to max. + *

+ * + * @param value Value to wrap + * @param min Minimum value + * @param max Maximum value + * @return Wrapped value + */ + static double wrap(double value, double min, double max) { + double delta = max - min; + + double firstMod = (value - min) % delta; + double secondMod = (firstMod + delta) % delta; + + if (value >= max && secondMod == 0) { + return max; + } + return secondMod + min; + } + + /** + * Indicates whether some other object is "equal to" this one. + * + * @param object The object to compare + * @return True if equal, false if not + */ + @Override + public boolean equals(@Nullable Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + + LatLng latLng = (LatLng) object; + + return Double.compare(latLng.altitude, altitude) == 0 && Double.compare(latLng.latitude, latitude) == 0 + && Double.compare(latLng.longitude, longitude) == 0; + } + + /** + * Returns a hash code value for the object. + * + * @return the hash code value + */ + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(latitude); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(longitude); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(altitude); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + /** + * Returns a string representation of the object. + * + * @return the string representation + */ + @NonNull + @Override + public String toString() { + return "LatLng [latitude=" + latitude + ", longitude=" + longitude + ", altitude=" + altitude + "]"; + } + + /** + * Describe the kinds of special objects contained in this Parcelable instance's marshaled representation. + * + * @return a bitmask indicating the set of special object types marshaled by this Parcelable object instance. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a Parcel. + * + * @param out The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written + */ + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeDouble(latitude); + out.writeDouble(longitude); + out.writeDouble(altitude); + } + + /** + * Calculate distance between two points + * + * @param other Other LatLng to compare to + * @return distance in meters + */ + public double distanceTo(@NonNull LatLng other) { + return TurfMeasurement.distance( + Point.fromLngLat(longitude, latitude), + Point.fromLngLat(other.getLongitude(), other.getLatitude()), + UNIT_METRES + ); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java new file mode 100644 index 000000000..263780f47 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java @@ -0,0 +1,608 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.FloatRange; +import android.support.annotation.Keep; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.mapbox.mapboxsdk.constants.GeometryConstants; +import com.mapbox.mapboxsdk.exceptions.InvalidLatLngBoundsException; + +import java.util.ArrayList; +import java.util.List; + +import static com.mapbox.mapboxsdk.constants.GeometryConstants.MAX_LATITUDE; +import static com.mapbox.mapboxsdk.constants.GeometryConstants.MAX_LONGITUDE; +import static com.mapbox.mapboxsdk.constants.GeometryConstants.MAX_WRAP_LONGITUDE; +import static com.mapbox.mapboxsdk.constants.GeometryConstants.MIN_LATITUDE; +import static com.mapbox.mapboxsdk.constants.GeometryConstants.MIN_LONGITUDE; +import static com.mapbox.mapboxsdk.constants.GeometryConstants.MIN_WRAP_LONGITUDE; + +/** + * A geographical area representing a latitude/longitude aligned rectangle. + *

+ * This class does not wrap values to the world bounds. + *

+ */ +public class LatLngBounds implements Parcelable { + + @Keep + private final double latitudeNorth; + @Keep + private final double latitudeSouth; + @Keep + private final double longitudeEast; + @Keep + private final double longitudeWest; + + /** + * Construct a new LatLngBounds based on its corners, given in NESW + * order. + *

+ * @since 7.0.0 LatLngBounds cannot be wrapped any more, i.e longitudeWest has to be + * less or equal to longitudeEast. + * + * For example, to represent bounds spanning 20 degrees crossing antimeridian with + * the NE point as (10, -170) and the SW point as (-10, 170), + * use (10, -190) and (-10, -170), or (10, -170) and (-10, -150). + * + * @param northLatitude Northern Latitude + * @param eastLongitude Eastern Longitude + * @param southLatitude Southern Latitude + * @param westLongitude Western Longitude + */ + @Keep + LatLngBounds(final double northLatitude, final double eastLongitude, + final double southLatitude, final double westLongitude) { + this.latitudeNorth = northLatitude; + this.longitudeEast = eastLongitude; + this.latitudeSouth = southLatitude; + this.longitudeWest = westLongitude; + } + + /** + * Returns the world bounds. + * + * @return the bounds representing the world + */ + public static LatLngBounds world() { + return LatLngBounds.from( + MAX_LATITUDE, MAX_WRAP_LONGITUDE, + MIN_LATITUDE, MIN_WRAP_LONGITUDE); + } + + /** + * Calculates the centerpoint of this LatLngBounds by simple interpolation and returns + * it as a point. This is a non-geodesic calculation which is not the geographic center. + * + * @return LatLng center of this LatLngBounds + */ + @NonNull + public LatLng getCenter() { + double latCenter = (this.latitudeNorth + this.latitudeSouth) / 2.0; + double longCenter = (this.longitudeEast + this.longitudeWest) / 2.0; + + return new LatLng(latCenter, longCenter); + } + + /** + * Get the north latitude value of this bounds. + * + * @return double latitude value for north + */ + public double getLatNorth() { + return this.latitudeNorth; + } + + /** + * Get the south latitude value of this bounds. + * + * @return double latitude value for south + */ + public double getLatSouth() { + return this.latitudeSouth; + } + + /** + * Get the east longitude value of this bounds. + * + * @return double longitude value for east + */ + public double getLonEast() { + return this.longitudeEast; + } + + /** + * Get the west longitude value of this bounds. + * + * @return double longitude value for west + */ + public double getLonWest() { + return this.longitudeWest; + } + + /** + * Get the latitude-longitude pair of the south west corner of this bounds. + * + * @return LatLng of the south west corner + */ + @NonNull + public LatLng getSouthWest() { + return new LatLng(latitudeSouth, longitudeWest); + } + + /** + * Get the latitude-longitude paur if the north east corner of this bounds. + * + * @return LatLng of the north east corner + */ + @NonNull + public LatLng getNorthEast() { + return new LatLng(latitudeNorth, longitudeEast); + } + + /** + * Get the latitude-longitude pair of the south east corner of this bounds. + * + * @return LatLng of the south east corner + */ + @NonNull + public LatLng getSouthEast() { + return new LatLng(latitudeSouth, longitudeEast); + } + + /** + * Get the latitude-longitude pair of the north west corner of this bounds. + * + * @return LatLng of the north west corner + */ + @NonNull + public LatLng getNorthWest() { + return new LatLng(latitudeNorth, longitudeWest); + } + + /** + * Get the area spanned by this LatLngBounds + * + * @return LatLngSpan area + */ + @NonNull + public LatLngSpan getSpan() { + return new LatLngSpan(getLatitudeSpan(), getLongitudeSpan()); + } + + /** + * Get the absolute distance, in degrees, between the north and + * south boundaries of this LatLngBounds + * + * @return Span distance + */ + public double getLatitudeSpan() { + return Math.abs(this.latitudeNorth - this.latitudeSouth); + } + + /** + * Get the absolute distance, in degrees, between the west and + * east boundaries of this LatLngBounds + * + * @return Span distance + */ + public double getLongitudeSpan() { + return Math.abs(this.longitudeEast - this.longitudeWest); + } + + /** + * Validate if LatLngBounds is empty, determined if absolute distance is + * + * @return boolean indicating if span is empty + */ + public boolean isEmptySpan() { + return getLongitudeSpan() == 0.0 || getLatitudeSpan() == 0.0; + } + + /** + * Returns a string representaton of the object. + * + * @return the string representation + */ + @NonNull + @Override + public String toString() { + return "N:" + this.latitudeNorth + "; E:" + this.longitudeEast + "; S:" + this.latitudeSouth + + "; W:" + this.longitudeWest; + } + + /** + * Constructs a LatLngBounds that contains all of a list of LatLng + * objects. Empty lists will yield invalid LatLngBounds. + * + * @param latLngs List of LatLng objects + * @return LatLngBounds + */ + static LatLngBounds fromLatLngs(final List latLngs) { + double minLat = MAX_LATITUDE; + double minLon = MAX_LONGITUDE; + double maxLat = MIN_LATITUDE; + double maxLon = MIN_LONGITUDE; + + for (final LatLng gp : latLngs) { + final double latitude = gp.getLatitude(); + final double longitude = gp.getLongitude(); + minLat = Math.min(minLat, latitude); + minLon = Math.min(minLon, longitude); + maxLat = Math.max(maxLat, latitude); + maxLon = Math.max(maxLon, longitude); + } + + return new LatLngBounds(maxLat, maxLon, minLat, minLon); + } + + /** + * Return an array of LatLng objects resembling this bounds. + * + * @return an array of 2 LatLng objects. + */ + @NonNull + public LatLng[] toLatLngs() { + return new LatLng[] {getNorthEast(), getSouthWest()}; + } + + /** + * Constructs a LatLngBounds from doubles representing a LatLng pair. + *

+ * This values of latNorth and latSouth should be in the range of [-90, 90], + * see {@link GeometryConstants#MIN_LATITUDE} and {@link GeometryConstants#MAX_LATITUDE}, + * otherwise IllegalArgumentException will be thrown. + * latNorth should be greater or equal latSouth, otherwise IllegalArgumentException will be thrown. + *

+ * This method doesn't recalculate most east or most west boundaries. + * Note @since 7.0.0 lonEast and lonWest will NOT be wrapped to be in the range of [-180, 180], + * see {@link GeometryConstants#MIN_LONGITUDE} and {@link GeometryConstants#MAX_LONGITUDE} + * lonEast should be greater or equal lonWest, otherwise IllegalArgumentException will be thrown. + *

+ */ + public static LatLngBounds from( + @FloatRange(from = MIN_LATITUDE, to = MAX_LATITUDE) double latNorth, + double lonEast, + @FloatRange(from = MIN_LATITUDE, to = MAX_LATITUDE) double latSouth, + double lonWest) { + + checkParams(latNorth, lonEast, latSouth, lonWest); + + return new LatLngBounds(latNorth, lonEast, latSouth, lonWest); + } + + private static void checkParams( + @FloatRange(from = MIN_LATITUDE, to = MAX_LATITUDE) double latNorth, + double lonEast, + @FloatRange(from = MIN_LATITUDE, to = MAX_LATITUDE) double latSouth, + double lonWest) { + + if (Double.isNaN(latNorth) || Double.isNaN(latSouth)) { + throw new IllegalArgumentException("latitude must not be NaN"); + } + + if (Double.isNaN(lonEast) || Double.isNaN(lonWest)) { + throw new IllegalArgumentException("longitude must not be NaN"); + } + + if (Double.isInfinite(lonEast) || Double.isInfinite(lonWest)) { + throw new IllegalArgumentException("longitude must not be infinite"); + } + + if (latNorth > MAX_LATITUDE || latNorth < MIN_LATITUDE + || latSouth > MAX_LATITUDE || latSouth < MIN_LATITUDE) { + throw new IllegalArgumentException("latitude must be between -90 and 90"); + } + + if (latNorth < latSouth) { + throw new IllegalArgumentException("latNorth cannot be less than latSouth"); + } + + if (lonEast < lonWest) { + throw new IllegalArgumentException("lonEast cannot be less than 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 - GeometryConstants.MAX_WRAP_LONGITUDE; + } + + /** + * Constructs a LatLngBounds from a Tile identifier. + *

+ * Returned bounds will have latitude in the range of Mercator projection. + * + * @param z Tile zoom level. + * @param x Tile X coordinate. + * @param y Tile Y coordinate. + * @see GeometryConstants#MIN_MERCATOR_LATITUDE + * @see GeometryConstants#MAX_MERCATOR_LATITUDE + */ + 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. + * + * @param latLng the latitude lognitude pair to include in the bounds. + * @return the newly constructed bounds + */ + @NonNull + public LatLngBounds include(@NonNull LatLng latLng) { + return new LatLngBounds.Builder() + .include(getNorthEast()) + .include(getSouthWest()) + .include(latLng) + .build(); + } + + /** + * Determines whether this LatLngBounds matches another one via LatLng. + * + * @param o another object + * @return a boolean indicating whether the LatLngBounds are equal + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o instanceof LatLngBounds) { + LatLngBounds other = (LatLngBounds) o; + return latitudeNorth == other.getLatNorth() + && latitudeSouth == other.getLatSouth() + && longitudeEast == other.getLonEast() + && longitudeWest == other.getLonWest(); + } + return false; + } + + + private boolean containsLatitude(final double latitude) { + return (latitude <= this.latitudeNorth) + && (latitude >= this.latitudeSouth); + } + + private boolean containsLongitude(final double longitude) { + return (longitude <= this.longitudeEast) + && (longitude >= this.longitudeWest); + } + + /** + * Determines whether this LatLngBounds contains a point. + * + * @param latLng the point which may be contained + * @return true, if the point is contained within the bounds + */ + public boolean contains(@NonNull final LatLng latLng) { + return containsLatitude(latLng.getLatitude()) + && containsLongitude(latLng.getLongitude()); + } + + /** + * Determines whether this LatLngBounds contains another bounds. + * + * @param other the bounds which may be contained + * @return true, if the bounds is contained within the bounds + */ + public boolean contains(@NonNull final LatLngBounds other) { + return contains(other.getNorthEast()) + && contains(other.getSouthWest()); + } + + /** + * Returns a new LatLngBounds that stretches to contain both this and another LatLngBounds. + * + * @param bounds LatLngBounds to add + * @return LatLngBounds + */ + @NonNull + public LatLngBounds union(@NonNull LatLngBounds bounds) { + return unionNoParamCheck(bounds.getLatNorth(), bounds.getLonEast(), + bounds.getLatSouth(), bounds.getLonWest()); + } + + /** + * Returns a new LatLngBounds that stretches to contain both this and another LatLngBounds. + * + *

+ * This values of northLat and southLat should be in the range of [-90, 90], + * see {@link GeometryConstants#MIN_LATITUDE} and {@link GeometryConstants#MAX_LATITUDE}, + * otherwise IllegalArgumentException will be thrown. + * northLat should be greater or equal southLat, otherwise IllegalArgumentException will be thrown. + * + *

+ * eastLon should be greater or equal westLon, otherwise IllegalArgumentException will be thrown. + * + * @param northLat Northern Latitude corner point + * @param eastLon Eastern Longitude corner point + * @param southLat Southern Latitude corner point + * @param westLon Western Longitude corner point + * @return LatLngBounds + */ + @NonNull + public LatLngBounds union(final double northLat, final double eastLon, + final double southLat, final double westLon) { + checkParams(northLat, eastLon, southLat, westLon); + return unionNoParamCheck(northLat, eastLon, southLat, westLon); + } + + private LatLngBounds unionNoParamCheck(final double northLat, final double eastLon, + final double southLat, final double westLon) { + + return new LatLngBounds((this.latitudeNorth < northLat) ? northLat : this.latitudeNorth, + (this.longitudeEast < eastLon) ? eastLon : this.longitudeEast, + (this.latitudeSouth > southLat) ? southLat : this.latitudeSouth, + (this.longitudeWest > westLon) ? westLon : this.longitudeWest); + } + + /** + * Returns a new LatLngBounds that is the intersection of this with another LatLngBounds, + * + * @param box LatLngBounds to intersect with + * @return LatLngBounds + */ + @Nullable + public LatLngBounds intersect(@NonNull LatLngBounds box) { + return intersectNoParamCheck(box.getLatNorth(), box.getLonEast(), box.getLatSouth(), box.getLonWest()); + } + + + /** + * Returns a new LatLngBounds that is the intersection of this with another box. + * + *

+ * This values of northLat and southLat should be in the range of [-90, 90], + * see {@link GeometryConstants#MIN_LATITUDE} and {@link GeometryConstants#MAX_LATITUDE}, + * otherwise IllegalArgumentException will be thrown. + * northLat should be greater or equal southLat, otherwise IllegalArgumentException will be thrown. + * + *

+ * eastLon should be greater or equal westLon, otherwise IllegalArgumentException will be thrown. + * + * @param northLat Northern Latitude corner point + * @param eastLon Eastern Longitude corner point + * @param southLat Southern Latitude corner point + * @param westLon Western Longitude corner point + * @return LatLngBounds + */ + @NonNull + public LatLngBounds intersect(final double northLat, final double eastLon, + final double southLat, final double westLon) { + checkParams(northLat, eastLon, southLat, westLon); + return intersectNoParamCheck(northLat, eastLon, southLat, westLon); + } + + private LatLngBounds intersectNoParamCheck(final double northLat, final double eastLon, + final double southLat, final double westLon) { + + double minLonWest = Math.max(this.longitudeWest, westLon); + double maxLonEast = Math.min(this.longitudeEast, eastLon); + if (maxLonEast >= minLonWest) { + double minLatSouth = Math.max(this.latitudeSouth, southLat); + double maxLatNorth = Math.min(this.latitudeNorth, northLat); + if (maxLatNorth >= minLatSouth) { + return new LatLngBounds(maxLatNorth, maxLonEast, minLatSouth, minLonWest); + } + } + return null; + } + + + + /** + * Inner class responsible for recreating Parcels into objects. + */ + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public LatLngBounds createFromParcel(@NonNull final Parcel in) { + return readFromParcel(in); + } + + @Override + public LatLngBounds[] newArray(final int size) { + return new LatLngBounds[size]; + } + }; + + /** + * Returns a hash code value for the object. + * + * @return the hash code + */ + @Override + public int hashCode() { + return (int) ((latitudeNorth + 90) + + ((latitudeSouth + 90) * 1000) + + ((longitudeEast + 180) * 1000000) + + ((longitudeWest + 180) * 1000000000)); + } + + /** + * Describe the kinds of special objects contained in this Parcelable instance's marshaled representation. + * + * @return a bitmask indicating the set of special object types marshaled by this Parcelable object instance. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a Parcel. + * + * @param out The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written + */ + @Override + public void writeToParcel(@NonNull final Parcel out, final int flags) { + out.writeDouble(this.latitudeNorth); + out.writeDouble(this.longitudeEast); + out.writeDouble(this.latitudeSouth); + out.writeDouble(this.longitudeWest); + } + + private static LatLngBounds readFromParcel(final Parcel in) { + final double northLat = in.readDouble(); + final double eastLon = in.readDouble(); + final double southLat = in.readDouble(); + final double westLon = in.readDouble(); + return new LatLngBounds(northLat, eastLon, southLat, westLon); + } + + /** + * Builder for composing LatLngBounds objects. + */ + public static final class Builder { + + private final List latLngList = new ArrayList<>(); + + /** + * Builds a new LatLngBounds. + *

+ * Throws an {@link InvalidLatLngBoundsException} when no LatLngBounds can be created. + *

+ * + * @return the build LatLngBounds + */ + public LatLngBounds build() { + if (latLngList.size() < 2) { + throw new InvalidLatLngBoundsException(latLngList.size()); + } + return LatLngBounds.fromLatLngs(latLngList); + } + + /** + * Adds a LatLng object to the LatLngBounds.Builder. + * + * @param latLngs the List of LatLng objects to be added + * @return this + */ + @NonNull + public Builder includes(@NonNull List latLngs) { + latLngList.addAll(latLngs); + return this; + } + + /** + * Adds a LatLng object to the LatLngBounds.Builder. + * + * @param latLng the LatLng to be added + * @return this + */ + @NonNull + public Builder include(@NonNull LatLng latLng) { + latLngList.add(latLng); + return this; + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngQuad.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngQuad.java new file mode 100644 index 000000000..d17d0d6fe --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngQuad.java @@ -0,0 +1,94 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Keep; +import android.support.annotation.NonNull; + +/** + * A geographical area representing a non-aligned quadrilateral + *

+ * This class does not wrap values to the world bounds + *

+ */ +public class LatLngQuad implements Parcelable { + + @Keep + private final LatLng topLeft; + @Keep + private final LatLng topRight; + @Keep + private final LatLng bottomRight; + @Keep + private final LatLng bottomLeft; + + /** + * Construct a new LatLngQuad based on its corners, + * in order top left, top right, bottom left, bottom right + */ + @Keep + public LatLngQuad(final LatLng topLeft, final LatLng topRight, final LatLng bottomRight, final LatLng bottomLeft) { + this.topLeft = topLeft; + this.topRight = topRight; + this.bottomRight = bottomRight; + this.bottomLeft = bottomLeft; + } + + public LatLng getTopLeft() { + return this.topLeft; + } + + public LatLng getTopRight() { + return this.topRight; + } + + public LatLng getBottomRight() { + return this.bottomRight; + } + + public LatLng getBottomLeft() { + return this.bottomLeft; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public LatLngQuad createFromParcel(@NonNull final Parcel in) { + return readFromParcel(in); + } + + @Override + public LatLngQuad[] newArray(final int size) { + return new LatLngQuad[size]; + } + }; + + @Override + public int hashCode() { + int code = topLeft.hashCode(); + code = (code ^ code >>> 31) + topRight.hashCode(); + code = (code ^ code >>> 31) + bottomRight.hashCode(); + code = (code ^ code >>> 31) + bottomLeft.hashCode(); + return code; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull final Parcel out, final int arg1) { + topLeft.writeToParcel(out, arg1); + topRight.writeToParcel(out, arg1); + bottomRight.writeToParcel(out, arg1); + bottomLeft.writeToParcel(out, arg1); + } + + private static LatLngQuad readFromParcel(@NonNull final Parcel in) { + final LatLng topLeft = new LatLng(in); + final LatLng topRight = new LatLng(in); + final LatLng bottomRight = new LatLng(in); + final LatLng bottomLeft = new LatLng(in); + return new LatLngQuad(topLeft, topRight, bottomRight, bottomLeft); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngSpan.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngSpan.java new file mode 100644 index 000000000..941e6652f --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngSpan.java @@ -0,0 +1,139 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; + +/** + * A geographical span defined by its latitude and longitude span. + */ +public class LatLngSpan implements Parcelable { + + private double mLatitudeSpan; + private double mLongitudeSpan; + + private LatLngSpan(@NonNull Parcel in) { + mLatitudeSpan = in.readDouble(); + mLongitudeSpan = in.readDouble(); + } + + /** + * Creates a LatLgnSpan. + * + * @param latitudeSpan The span used for latitude. + * @param longitudeSpan The span used for longitude. + */ + public LatLngSpan(double latitudeSpan, double longitudeSpan) { + mLatitudeSpan = latitudeSpan; + mLongitudeSpan = longitudeSpan; + } + + /** + * Returns the latitude span. + * + * @return The latitude span. + */ + public double getLatitudeSpan() { + return mLatitudeSpan; + } + + /** + * Sets the latitude span. + * + * @param latitudeSpan The latitude span to set. + */ + public void setLatitudeSpan(double latitudeSpan) { + mLatitudeSpan = latitudeSpan; + } + + /** + * Returns to longitude span. + * + * @return The longitude span. + */ + public double getLongitudeSpan() { + return mLongitudeSpan; + } + + /** + * Sets the longitude span. + * + * @param longitudeSpan The longitude span to set. + */ + public void setLongitudeSpan(double longitudeSpan) { + mLongitudeSpan = longitudeSpan; + } + + /** + * Indicates whether some other object is "equal to" this one. + * + * @param object The object to compare + * @return True if equal, false if not + */ + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object instanceof LatLngSpan) { + LatLngSpan other = (LatLngSpan) object; + return mLongitudeSpan == other.getLongitudeSpan() + && mLatitudeSpan == other.getLatitudeSpan(); + } + return false; + } + + /** + * Inner class responsible for recreating Parcels into objects. + */ + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public LatLngSpan createFromParcel(@NonNull Parcel in) { + return new LatLngSpan(in); + } + + @Override + public LatLngSpan[] newArray(int size) { + return new LatLngSpan[size]; + } + }; + + /** + * Describe the kinds of special objects contained in this Parcelable instance's marshaled representation. + * + * @return a bitmask indicating the set of special object types marshaled by this Parcelable object instance. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a Parcel. + * + * @param out Parcel in which the object should be written + * @param flags Additional flags about how the object should be written + */ + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeDouble(mLatitudeSpan); + out.writeDouble(mLongitudeSpan); + } + + /** + * Returns a hash code value for the object. + * + * @return hash code value of this + */ + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(mLatitudeSpan); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(mLongitudeSpan); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ProjectedMeters.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ProjectedMeters.java new file mode 100644 index 000000000..745a7c6be --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/ProjectedMeters.java @@ -0,0 +1,155 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Keep; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * ProjectedMeters is a projection of longitude, latitude points in Mercator meters. + *

+ * these have been projected into Mapbox GL's Mercator projection. Instead of decimal + * degrees, it uses Mercator meters (which are notably not equivalent to SI meters) + * except at the equator. + *

+ */ +public class ProjectedMeters implements Parcelable { + + /** + * Inner class responsible for recreating Parcels into objects. + */ + public static final Creator CREATOR = new Creator() { + public ProjectedMeters createFromParcel(@NonNull Parcel in) { + return new ProjectedMeters(in); + } + + public ProjectedMeters[] newArray(int size) { + return new ProjectedMeters[size]; + } + }; + + private double northing; + private double easting; + + /** + * Creates a ProjectedMeters based on projected meters in north and east direction. + * + * @param northing the northing in meters + * @param easting the easting in meters + */ + @Keep + public ProjectedMeters(double northing, double easting) { + this.northing = northing; + this.easting = easting; + } + + /** + * Creates a ProjecteMeters based on another set of projected meters. + * + * @param projectedMeters The projected meters to be based on. + */ + public ProjectedMeters(ProjectedMeters projectedMeters) { + this.northing = projectedMeters.northing; + this.easting = projectedMeters.easting; + } + + /** + * Creates a ProjectedMeters from a Parcel. + * + * @param in The parcel to create from + * @return a bitmask indicating the set of special object types marshaled by this Parcelable object instance. + */ + private ProjectedMeters(Parcel in) { + northing = in.readDouble(); + easting = in.readDouble(); + } + + /** + * Get projected meters in north direction. + * + * @return Projected meters in north. + */ + public double getNorthing() { + return northing; + } + + /** + * Get projected meters in east direction. + * + * @return Projected meters in east. + */ + public double getEasting() { + return easting; + } + + /** + * Indicates whether some other object is "equal to" this one. + * + * @param other The object to compare this to + * @return true if equal, false if not + */ + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + ProjectedMeters projectedMeters = (ProjectedMeters) other; + + return Double.compare(projectedMeters.easting, easting) == 0 + && Double.compare(projectedMeters.northing, northing) == 0; + } + + /** + * Returns a hash code value for the object. + * + * @return the hash code of this + */ + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(easting); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(northing); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + /** + * Returns a string representation of the object. + * + * @return the string representation of this + */ + @NonNull + @Override + public String toString() { + return "ProjectedMeters [northing=" + northing + ", easting=" + easting + "]"; + } + + /** + * Describe the kinds of special objects contained in this Parcelable instance's marshaled representation. + * + * @return a bitmask indicating the set of special object types marshaled by this Parcelable object instance. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a Parcel. + * + * @param out The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written + */ + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeDouble(northing); + out.writeDouble(easting); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/VisibleRegion.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/VisibleRegion.java new file mode 100644 index 000000000..a8b4421ee --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/VisibleRegion.java @@ -0,0 +1,161 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; + +/** + * Contains the four points defining the four-sided polygon that is visible in a map's camera. + * This polygon can be a trapezoid instead of a rectangle, because a camera can have tilt. + * If the camera is directly over the center of the camera, the shape is rectangular, + * but if the camera is tilted, the shape will appear to be a trapezoid whose + * smallest side is closest to the point of view. + */ +public class VisibleRegion implements Parcelable { + + /** + * LatLng object that defines the far left corner of the camera. + */ + public final LatLng farLeft; + + /** + * LatLng object that defines the far right corner of the camera. + */ + public final LatLng farRight; + + /** + * LatLng object that defines the bottom left corner of the camera. + */ + public final LatLng nearLeft; + + /** + * LatLng object that defines the bottom right corner of the camera. + */ + public final LatLng nearRight; + + /** + * The smallest bounding box that includes the visible region defined in this class. + */ + public final LatLngBounds latLngBounds; + + /** + * Creates a VisibleRegion from a Parcel. + * + * @param in The Parcel to create this from + */ + private VisibleRegion(Parcel in) { + this.farLeft = in.readParcelable(LatLng.class.getClassLoader()); + this.farRight = in.readParcelable(LatLng.class.getClassLoader()); + this.nearLeft = in.readParcelable(LatLng.class.getClassLoader()); + this.nearRight = in.readParcelable(LatLng.class.getClassLoader()); + this.latLngBounds = in.readParcelable(LatLngBounds.class.getClassLoader()); + } + + /** + * Creates a VisibleRegion given the four corners of the camera. + * + * @param farLeft A LatLng object containing the latitude and longitude of the near left corner of the region. + * @param farRight A LatLng object containing the latitude and longitude of the near left corner of the region. + * @param nearLeft A LatLng object containing the latitude and longitude of the near left corner of the region. + * @param nearRight A LatLng object containing the latitude and longitude of the near left corner of the region. + * @param latLngBounds The smallest bounding box that includes the visible region defined in this class. + */ + public VisibleRegion(LatLng farLeft, LatLng farRight, LatLng nearLeft, LatLng nearRight, LatLngBounds latLngBounds) { + this.farLeft = farLeft; + this.farRight = farRight; + this.nearLeft = nearLeft; + this.nearRight = nearRight; + this.latLngBounds = latLngBounds; + } + + /** + * Compares this VisibleRegion to another object. + * If the other object is actually a pointer to this object, + * or if all four corners and the bounds of the two objects are the same, + * this method returns true. Otherwise, this method returns false. + * + * @param o The Object to compare with. + * @return true if both objects are the same object. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof VisibleRegion)) { + return false; + } + if (o == this) { + return true; + } + + VisibleRegion visibleRegion = (VisibleRegion) o; + return farLeft.equals(visibleRegion.farLeft) + && farRight.equals(visibleRegion.farRight) + && nearLeft.equals(visibleRegion.nearLeft) + && nearRight.equals(visibleRegion.nearRight) + && latLngBounds.equals(visibleRegion.latLngBounds); + } + + /** + * The string representation of the object. + * + * @return the string representation of this + */ + @NonNull + @Override + public String toString() { + return "[farLeft [" + farLeft + "], farRight [" + farRight + "], nearLeft [" + nearLeft + "], nearRight [" + + nearRight + "], latLngBounds [" + latLngBounds + "]]"; + } + + /** + * Returns a hash code value for the object. + * + * @return the hash code + */ + @Override + public int hashCode() { + return ((farLeft.hashCode() + 90) + + ((farRight.hashCode() + 90) * 1000) + + ((nearLeft.hashCode() + 180) * 1000000) + + ((nearRight.hashCode() + 180) * 1000000000)); + } + + /** + * Describe the kinds of special objects contained in this Parcelable instance's marshaled representation. + * + * @return a bitmask indicating the set of special object types marshaled by this Parcelable object instance. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a Parcel. + * + * @param out The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written + */ + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeParcelable(farLeft, flags); + out.writeParcelable(farRight, flags); + out.writeParcelable(nearLeft, flags); + out.writeParcelable(nearRight, flags); + out.writeParcelable(latLngBounds, flags); + } + + /** + * Inner class responsible for recreating Parcels into objects. + */ + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public VisibleRegion createFromParcel(@NonNull Parcel in) { + return new VisibleRegion(in); + } + + public VisibleRegion[] newArray(int size) { + return new VisibleRegion[size]; + } + }; + +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/package-info.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/package-info.java new file mode 100644 index 000000000..754af7659 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains the Mapbox Maps Android Geometry API classes. + */ +package com.mapbox.mapboxsdk.geometry; diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpIdentifier.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpIdentifier.java new file mode 100644 index 000000000..5f4f85ea7 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpIdentifier.java @@ -0,0 +1,39 @@ +package com.mapbox.mapboxsdk.http; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.support.annotation.NonNull; + +import com.mapbox.mapboxsdk.MapStrictMode; +import com.mapbox.mapboxsdk.Mapbox; + +public class HttpIdentifier { + + private HttpIdentifier() { + } + + /** + * Returns the application identifier, consisting out the package name, version name and version code. + * + * @return the application identifier + */ + public static String getIdentifier() { + return getIdentifier(Mapbox.getApplicationContext()); + } + + /** + * Returns the application identifier, consisting out the package name, version name and version code. + * + * @param context the context used to retrieve the package manager from + * @return the application identifier + */ + private static String getIdentifier(@NonNull Context context) { + try { + PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + return String.format("%s/%s (%s)", context.getPackageName(), packageInfo.versionName, packageInfo.versionCode); + } catch (Exception exception) { + MapStrictMode.strictModeViolation(exception); + return ""; + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpLogger.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpLogger.java new file mode 100644 index 000000000..2f44694f0 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpLogger.java @@ -0,0 +1,35 @@ +package com.mapbox.mapboxsdk.http; + +import android.util.Log; +import com.mapbox.mapboxsdk.log.Logger; + +import static com.mapbox.mapboxsdk.http.HttpRequest.CONNECTION_ERROR; +import static com.mapbox.mapboxsdk.http.HttpRequest.TEMPORARY_ERROR; + +public class HttpLogger { + + private static final String TAG = "Mbgl-HttpRequest"; + + public static boolean logRequestUrl; + public static boolean logEnabled = true; + + private HttpLogger(){ + } + + public static void logFailure(int type, String errorMessage, String requestUrl) { + log(type == TEMPORARY_ERROR ? Log.DEBUG : type == CONNECTION_ERROR ? Log.INFO : Log.WARN, + String.format( + "Request failed due to a %s error: %s %s", + type == TEMPORARY_ERROR ? "temporary" : type == CONNECTION_ERROR ? "connection" : "permanent", + errorMessage, + logRequestUrl ? requestUrl : "" + ) + ); + } + + public static void log(int type, String errorMessage) { + if (logEnabled) { + Logger.log(type, TAG, errorMessage); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpRequest.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpRequest.java new file mode 100644 index 000000000..1fd4f43cd --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpRequest.java @@ -0,0 +1,32 @@ +package com.mapbox.mapboxsdk.http; + +/** + * Interface definition for performing http requests. + *

+ * This allows to provide alternative implementations for the http interaction of this library. + *

+ */ +public interface HttpRequest { + + int CONNECTION_ERROR = 0; + int TEMPORARY_ERROR = 1; + int PERMANENT_ERROR = 2; + + /** + * Executes the request. + * + * @param httpRequest callback to be invoked when we receive a response + * @param nativePtr the pointer associated to the request + * @param resourceUrl the resource url to download + * @param etag http header, identifier for a specific version of a resource + * @param modified http header, used to determine if a resource hasn't been modified since + * @param offlineUsage flag to indicate a resource will be used for offline, appends offline=true as a query parameter + */ + void executeRequest(HttpResponder httpRequest, long nativePtr, String resourceUrl, + String etag, String modified, boolean offlineUsage); + + /** + * Cancels the request. + */ + void cancelRequest(); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpRequestUrl.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpRequestUrl.java new file mode 100644 index 000000000..1faf8f5e5 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpRequestUrl.java @@ -0,0 +1,53 @@ +package com.mapbox.mapboxsdk.http; + +import android.support.annotation.NonNull; + +import com.mapbox.mapboxsdk.Mapbox; + +public class HttpRequestUrl { + + private HttpRequestUrl() { + } + + /** + * Adapts a resource request url based on the host, query size, and offline requirement. + * Mapbox resources downloaded for offline use are subject to separate Vector Tile and + * Raster Tile API pricing and are not included in the Maps SDK’s “unlimited” requests. + * See our pricing page for more information. + * + * @param host the host used as endpoint + * @param resourceUrl the resource to download + * @param querySize the query size of the resource request + * @param offline the type of resource, either offline or online + * @return the adapted resource url + */ + public static String buildResourceUrl(@NonNull String host, String resourceUrl, int querySize, boolean offline) { + if (isValidMapboxEndpoint(host)) { + if (querySize == 0) { + resourceUrl = resourceUrl + "?"; + } else { + resourceUrl = resourceUrl + "&"; + } + // Only add SKU token to requests not tagged as "offline" usage. + if (offline) { + resourceUrl = resourceUrl + "offline=true"; + } else { + resourceUrl = resourceUrl + "sku=" + Mapbox.getSkuToken(); + } + } + return resourceUrl; + } + + /** + * Validates if the host used as endpoint is a valid Mapbox endpoint. + * + * @param host the host used as endpoint + * @return true if a valid Mapbox endpoint + */ + private static boolean isValidMapboxEndpoint(String host) { + return host.equals("mapbox.com") + || host.endsWith(".mapbox.com") + || host.equals("mapbox.cn") + || host.endsWith(".mapbox.cn"); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpResponder.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpResponder.java new file mode 100644 index 000000000..a050e07c5 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HttpResponder.java @@ -0,0 +1,32 @@ +package com.mapbox.mapboxsdk.http; + +/** + * Interface definition for a callback to be invoked when either a response was returned for a requested resource or + * when an error occurred when requesting the resource. + */ +public interface HttpResponder { + + /** + * Invoked when a resource has finished. + * + * @param responseCode http response code + * @param eTag http header, identifier for a specific version of a resource + * @param lastModified http header, used to determine if a resource hasn't been modified since + * @param cacheControl http header, used to determine cache strategy of a resource + * @param expires http header, used to determine when a resource is stale + * @param retryAfter http header, used to indicate when the service is expected to be unavailable to the client + * @param xRateLimitReset http header, used to determine the remaining window before the rate limit resets + * @param body http response body, in an array of bytes representation + */ + void onResponse(int responseCode, String eTag, String lastModified, String cacheControl, String expires, + String retryAfter, String xRateLimitReset, byte[] body); + + /** + * Invoked when a resource failed to be retrieved. + * + * @param type the error type, either one of {@link HttpRequest#CONNECTION_ERROR}, + * {@link HttpRequest#TEMPORARY_ERROR} or {@link HttpRequest#PERMANENT_ERROR} + * @param errorMessage the error message associated with the failure + */ + void handleFailure(int type, String errorMessage); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/LocalRequestTask.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/LocalRequestTask.java new file mode 100644 index 000000000..2d9fe43ad --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/LocalRequestTask.java @@ -0,0 +1,75 @@ +package com.mapbox.mapboxsdk.http; + +import android.content.res.AssetManager; +import android.os.AsyncTask; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.mapbox.mapboxsdk.MapStrictMode; +import com.mapbox.mapboxsdk.Mapbox; +import com.mapbox.mapboxsdk.log.Logger; + +import java.io.IOException; +import java.io.InputStream; + +class LocalRequestTask extends AsyncTask { + + private static final String TAG = "Mbgl-LocalRequestTask"; + + private OnLocalRequestResponse requestResponse; + + LocalRequestTask(OnLocalRequestResponse requestResponse) { + this.requestResponse = requestResponse; + } + + @Nullable + @Override + protected byte[] doInBackground(String... strings) { + return loadFile(Mapbox.getApplicationContext().getAssets(), + "integration/" + strings[0] + .substring(8) + .replaceAll("%20", " ") + .replaceAll("%2c", ",")); + } + + @Override + protected void onPostExecute(@Nullable byte[] bytes) { + super.onPostExecute(bytes); + if (bytes != null && requestResponse != null) { + requestResponse.onResponse(bytes); + } + } + + @Nullable + private static byte[] loadFile(AssetManager assets, @NonNull String path) { + byte[] buffer = null; + InputStream input = null; + try { + input = assets.open(path); + int size = input.available(); + buffer = new byte[size]; + input.read(buffer); + } catch (IOException exception) { + logFileError(exception); + } finally { + if (input != null) { + try { + input.close(); + } catch (IOException exception) { + logFileError(exception); + } + } + } + return buffer; + } + + private static void logFileError(Exception exception) { + String message = "Load file failed"; + Logger.e(TAG, message, exception); + MapStrictMode.strictModeViolation(message, exception); + } + + public interface OnLocalRequestResponse { + void onResponse(byte[] bytes); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/NativeHttpRequest.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/NativeHttpRequest.java new file mode 100644 index 000000000..e59b07dc1 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/NativeHttpRequest.java @@ -0,0 +1,90 @@ +package com.mapbox.mapboxsdk.http; + +import android.support.annotation.Keep; +import android.support.annotation.Nullable; +import com.mapbox.mapboxsdk.Mapbox; + +import java.util.concurrent.locks.ReentrantLock; + +@Keep +public class NativeHttpRequest implements HttpResponder { + + private final HttpRequest httpRequest = Mapbox.getModuleProvider().createHttpRequest(); + + // Reentrancy is not needed, but "Lock" is an abstract class. + private final ReentrantLock lock = new ReentrantLock(); + + @Keep + private long nativePtr; + + @Keep + private NativeHttpRequest(long nativePtr, String resourceUrl, String etag, String modified, boolean offlineUsage) { + this.nativePtr = nativePtr; + + if (resourceUrl.startsWith("local://")) { + // used by render test to serve files from assets + executeLocalRequest(resourceUrl); + return; + } + httpRequest.executeRequest(this, nativePtr, resourceUrl, etag, modified, offlineUsage); + } + + public void cancel() { + httpRequest.cancelRequest(); + + // TODO: We need a lock here because we can try + // to cancel at the same time the request is getting + // answered on the OkHTTP thread. We could get rid of + // this lock by using Runnable when we move Android + // implementation of mbgl::RunLoop to Looper. + lock.lock(); + nativePtr = 0; + lock.unlock(); + } + + public void onResponse(int responseCode, String etag, String lastModified, String cacheControl, String expires, + String retryAfter, String xRateLimitReset, byte[] body) { + lock.lock(); + if (nativePtr != 0) { + nativeOnResponse(responseCode, + etag, + lastModified, + cacheControl, + expires, + retryAfter, + xRateLimitReset, + body); + } + lock.unlock(); + } + + private void executeLocalRequest(String resourceUrl) { + new LocalRequestTask(new LocalRequestTask.OnLocalRequestResponse() { + @Override + public void onResponse(@Nullable byte[] bytes) { + if (bytes != null) { + lock.lock(); + if (nativePtr != 0) { + NativeHttpRequest.this.nativeOnResponse(200, null, null, null, null, null, null, bytes); + } + lock.unlock(); + } + } + }).execute(resourceUrl); + } + + public void handleFailure(int type, String errorMessage) { + lock.lock(); + if (nativePtr != 0) { + nativeOnFailure(type, errorMessage); + } + lock.unlock(); + } + + @Keep + private native void nativeOnFailure(int type, String message); + + @Keep + private native void nativeOnResponse(int code, String etag, String modified, String cacheControl, String expires, + String retryAfter, String xRateLimitReset, byte[] body); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/package-info.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/package-info.java new file mode 100644 index 000000000..09eeda2b7 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/package-info.java @@ -0,0 +1,4 @@ +/** + * Do not use this package. Internal use only. + */ +package com.mapbox.mapboxsdk.http; diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/AnimatorListenerHolder.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/AnimatorListenerHolder.java new file mode 100644 index 000000000..5dcf30919 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/AnimatorListenerHolder.java @@ -0,0 +1,45 @@ +package com.mapbox.mapboxsdk.location; + +class AnimatorListenerHolder { + @MapboxAnimator.Type + private final int animatorType; + private final MapboxAnimator.AnimationsValueChangeListener listener; + + AnimatorListenerHolder(@MapboxAnimator.Type int animatorType, MapboxAnimator.AnimationsValueChangeListener listener) { + this.animatorType = animatorType; + this.listener = listener; + } + + @MapboxAnimator.Type + public int getAnimatorType() { + return animatorType; + } + + public MapboxAnimator.AnimationsValueChangeListener getListener() { + return listener; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + AnimatorListenerHolder that = (AnimatorListenerHolder) o; + + if (animatorType != that.animatorType) { + return false; + } + return listener != null ? listener.equals(that.listener) : that.listener == null; + } + + @Override + public int hashCode() { + int result = animatorType; + result = 31 * result + (listener != null ? listener.hashCode() : 0); + return result; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassEngine.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassEngine.java new file mode 100644 index 000000000..bb7fd4972 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassEngine.java @@ -0,0 +1,55 @@ +package com.mapbox.mapboxsdk.location; + +import android.support.annotation.NonNull; + +import com.mapbox.mapboxsdk.location.modes.CameraMode; +import com.mapbox.mapboxsdk.location.modes.RenderMode; + +/** + * Interface defining the source of compass heading data that is + * consumed by the {@link LocationComponent} when in compass related + * {@link RenderMode} or + * {@link CameraMode}s. + */ +public interface CompassEngine { + + /** + * Adds a {@link CompassListener} that can be used to + * receive heading and state changes. + * + * @param compassListener to be added + */ + void addCompassListener(@NonNull CompassListener compassListener); + + /** + * Removes a {@link CompassListener} that can be used to + * receive heading and state changes. + * + * @param compassListener to be removed + */ + void removeCompassListener(@NonNull CompassListener compassListener); + + /** + * Returns the last heading value produced and pushed via + * a compass listener. + * + * @return last heading value + */ + float getLastHeading(); + + /** + * Provides the last know accuracy status from the sensor manager. + *

+ * An integer value which is identical to the {@code SensorManager} class constants: + *

    + *
  • {@link android.hardware.SensorManager#SENSOR_STATUS_NO_CONTACT}
  • + *
  • {@link android.hardware.SensorManager#SENSOR_STATUS_UNRELIABLE}
  • + *
  • {@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_LOW}
  • + *
  • {@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_MEDIUM}
  • + *
  • {@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_HIGH}
  • + *
+ * + * @return last accuracy status + */ + int getLastAccuracySensorStatus(); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassListener.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassListener.java new file mode 100644 index 000000000..c44218a1d --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/CompassListener.java @@ -0,0 +1,33 @@ +package com.mapbox.mapboxsdk.location; + +/** + * Callbacks related to the compass + */ +public interface CompassListener { + + /** + * Callback's invoked when a new compass update occurs. You can listen into the compass updates + * using {@link CompassEngine#addCompassListener(CompassListener)} and implementing these + * callbacks. Note that this interface is also used internally to to update the UI chevron/arrow. + * + * @param userHeading the new compass heading + */ + void onCompassChanged(float userHeading); + + /** + * This gets invoked when the compass accuracy status changes from one value to another. It + * provides an integer value which is identical to the {@code SensorManager} class constants: + *
    + *
  • {@link android.hardware.SensorManager#SENSOR_STATUS_NO_CONTACT}
  • + *
  • {@link android.hardware.SensorManager#SENSOR_STATUS_UNRELIABLE}
  • + *
  • {@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_LOW}
  • + *
  • {@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_MEDIUM}
  • + *
  • {@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_HIGH}
  • + *
+ * + * @param compassStatus the new accuracy of this sensor, one of + * {@code SensorManager.SENSOR_STATUS_*} + */ + void onCompassAccuracyChange(int compassStatus); +} + diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LatLngEvaluator.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LatLngEvaluator.java new file mode 100644 index 000000000..c631b1b75 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LatLngEvaluator.java @@ -0,0 +1,21 @@ +package com.mapbox.mapboxsdk.location; + +import android.animation.TypeEvaluator; + +import android.support.annotation.NonNull; +import com.mapbox.mapboxsdk.geometry.LatLng; + +class LatLngEvaluator implements TypeEvaluator { + + private final LatLng latLng = new LatLng(); + + @NonNull + @Override + public LatLng evaluate(float fraction, @NonNull LatLng startValue, @NonNull LatLng endValue) { + latLng.setLatitude(startValue.getLatitude() + + ((endValue.getLatitude() - startValue.getLatitude()) * fraction)); + latLng.setLongitude(startValue.getLongitude() + + ((endValue.getLongitude() - startValue.getLongitude()) * fraction)); + return latLng; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerBitmapProvider.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerBitmapProvider.java new file mode 100644 index 000000000..14a1b49dc --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerBitmapProvider.java @@ -0,0 +1,32 @@ +package com.mapbox.mapboxsdk.location; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorInt; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; + +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.utils.BitmapUtils; + +import static com.mapbox.mapboxsdk.location.Utils.generateShadow; + +class LayerBitmapProvider { + + private final Context context; + + LayerBitmapProvider(Context context) { + this.context = context; + } + + Bitmap generateBitmap(@DrawableRes int drawableRes, @ColorInt Integer tintColor) { + Drawable drawable = BitmapUtils.getDrawableFromRes(context, drawableRes, tintColor); + return BitmapUtils.getBitmapFromDrawable(drawable); + } + + Bitmap generateShadowBitmap(@NonNull LocationComponentOptions options) { + Drawable shadowDrawable = BitmapUtils.getDrawableFromRes(context, R.drawable.mapbox_user_icon_shadow); + return generateShadow(shadowDrawable, options.elevation()); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerFeatureProvider.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerFeatureProvider.java new file mode 100644 index 000000000..faa83ea0e --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerFeatureProvider.java @@ -0,0 +1,26 @@ +package com.mapbox.mapboxsdk.location; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.mapbox.geojson.Feature; +import com.mapbox.geojson.Point; + +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_COMPASS_BEARING; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_GPS_BEARING; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_LOCATION_STALE; + +class LayerFeatureProvider { + + @NonNull + Feature generateLocationFeature(@Nullable Feature locationFeature, @NonNull LocationComponentOptions options) { + if (locationFeature != null) { + return locationFeature; + } + locationFeature = Feature.fromGeometry(Point.fromLngLat(0.0, 0.0)); + locationFeature.addNumberProperty(PROPERTY_GPS_BEARING, 0f); + locationFeature.addNumberProperty(PROPERTY_COMPASS_BEARING, 0f); + locationFeature.addBooleanProperty(PROPERTY_LOCATION_STALE, options.enableStaleState()); + return locationFeature; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java new file mode 100644 index 000000000..cac513c2f --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java @@ -0,0 +1,111 @@ +package com.mapbox.mapboxsdk.location; + +import android.support.annotation.NonNull; +import com.mapbox.geojson.Feature; +import com.mapbox.mapboxsdk.style.layers.CircleLayer; +import com.mapbox.mapboxsdk.style.layers.Layer; +import com.mapbox.mapboxsdk.style.layers.Property; +import com.mapbox.mapboxsdk.style.layers.SymbolLayer; +import com.mapbox.mapboxsdk.style.sources.GeoJsonOptions; +import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; + +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.ACCURACY_LAYER; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_LAYER; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BEARING_LAYER; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_LAYER; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.LOCATION_SOURCE; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_ALPHA; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_COLOR; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_RADIUS; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BACKGROUND_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BACKGROUND_STALE_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BEARING_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_COMPASS_BEARING; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON_OFFSET; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_STALE_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_GPS_BEARING; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_LOCATION_STALE; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_SHADOW_ICON_OFFSET; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_LAYER; +import static com.mapbox.mapboxsdk.style.expressions.Expression.get; +import static com.mapbox.mapboxsdk.style.expressions.Expression.literal; +import static com.mapbox.mapboxsdk.style.expressions.Expression.match; +import static com.mapbox.mapboxsdk.style.expressions.Expression.stop; +import static com.mapbox.mapboxsdk.style.expressions.Expression.switchCase; +import static com.mapbox.mapboxsdk.style.layers.Property.ICON_ROTATION_ALIGNMENT_MAP; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleColor; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleOpacity; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circlePitchAlignment; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleRadius; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleStrokeColor; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconOffset; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconRotate; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconRotationAlignment; + +class LayerSourceProvider { + + private static final String EMPTY_STRING = ""; + + @NonNull + GeoJsonSource generateSource(Feature locationFeature) { + return new GeoJsonSource( + LOCATION_SOURCE, + locationFeature, + new GeoJsonOptions().withMaxZoom(16) + ); + } + + @NonNull + Layer generateLayer(@NonNull String layerId) { + SymbolLayer layer = new SymbolLayer(layerId, LOCATION_SOURCE); + layer.setProperties( + iconAllowOverlap(true), + iconIgnorePlacement(true), + iconRotationAlignment(ICON_ROTATION_ALIGNMENT_MAP), + iconRotate( + match(literal(layerId), literal(0f), + stop(FOREGROUND_LAYER, get(PROPERTY_GPS_BEARING)), + stop(BACKGROUND_LAYER, get(PROPERTY_GPS_BEARING)), + stop(SHADOW_LAYER, get(PROPERTY_GPS_BEARING)), + stop(BEARING_LAYER, get(PROPERTY_COMPASS_BEARING)) + ) + ), + iconImage( + match(literal(layerId), literal(EMPTY_STRING), + stop(FOREGROUND_LAYER, switchCase( + get(PROPERTY_LOCATION_STALE), get(PROPERTY_FOREGROUND_STALE_ICON), + get(PROPERTY_FOREGROUND_ICON))), + stop(BACKGROUND_LAYER, switchCase( + get(PROPERTY_LOCATION_STALE), get(PROPERTY_BACKGROUND_STALE_ICON), + get(PROPERTY_BACKGROUND_ICON))), + stop(SHADOW_LAYER, literal(SHADOW_ICON)), + stop(BEARING_LAYER, get(PROPERTY_BEARING_ICON)) + ) + ), + iconOffset( + match(literal(layerId), literal(new Float[] {0f, 0f}), + stop(literal(FOREGROUND_LAYER), get(PROPERTY_FOREGROUND_ICON_OFFSET)), + stop(literal(SHADOW_LAYER), get(PROPERTY_SHADOW_ICON_OFFSET)) + ) + ) + ); + return layer; + } + + @NonNull + Layer generateAccuracyLayer() { + return new CircleLayer(ACCURACY_LAYER, LOCATION_SOURCE) + .withProperties( + circleRadius(get(PROPERTY_ACCURACY_RADIUS)), + circleColor(get(PROPERTY_ACCURACY_COLOR)), + circleOpacity(get(PROPERTY_ACCURACY_ALPHA)), + circleStrokeColor(get(PROPERTY_ACCURACY_COLOR)), + circlePitchAlignment(Property.CIRCLE_PITCH_ALIGNMENT_MAP) + ); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java new file mode 100644 index 000000000..5aad038a2 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java @@ -0,0 +1,422 @@ +package com.mapbox.mapboxsdk.location; + +import android.animation.Animator; +import android.location.Location; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.SparseArray; +import android.view.animation.LinearInterpolator; + +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.log.Logger; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.Projection; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.ACCURACY_RADIUS_ANIMATION_DURATION; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.COMPASS_UPDATE_RATE_MS; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.MAX_ANIMATION_DURATION_MS; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.TRANSITION_ANIMATION_DURATION_MS; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_CAMERA_COMPASS_BEARING; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_CAMERA_GPS_BEARING; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_CAMERA_LATLNG; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_ACCURACY; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_COMPASS_BEARING; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_GPS_BEARING; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_LATLNG; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_TILT; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_ZOOM; +import static com.mapbox.mapboxsdk.location.Utils.immediateAnimation; + +final class LocationAnimatorCoordinator { + + private static final String TAG = "Mbgl-LocationAnimatorCoordinator"; + + @VisibleForTesting + final SparseArray animatorArray = new SparseArray<>(); + + private final Projection projection; + private Location previousLocation; + private float previousAccuracyRadius = -1; + private float previousCompassBearing = -1; + private long locationUpdateTimestamp = -1; + private float durationMultiplier; + private final MapboxAnimatorProvider animatorProvider; + private final MapboxAnimatorSetProvider animatorSetProvider; + private boolean compassAnimationEnabled; + private boolean accuracyAnimationEnabled; + + @VisibleForTesting + int maxAnimationFps = Integer.MAX_VALUE; + + @VisibleForTesting + final SparseArray listeners = new SparseArray<>(); + + LocationAnimatorCoordinator(@NonNull Projection projection, @NonNull MapboxAnimatorSetProvider animatorSetProvider, + @NonNull MapboxAnimatorProvider animatorProvider) { + this.projection = projection; + this.animatorProvider = animatorProvider; + this.animatorSetProvider = animatorSetProvider; + } + + void updateAnimatorListenerHolders(@NonNull Set listenerHolders) { + listeners.clear(); + for (AnimatorListenerHolder holder : listenerHolders) { + listeners.append(holder.getAnimatorType(), holder.getListener()); + } + } + + void feedNewLocation(@NonNull Location newLocation, @NonNull CameraPosition currentCameraPosition, + boolean isGpsNorth) { + if (previousLocation == null) { + previousLocation = newLocation; + locationUpdateTimestamp = SystemClock.elapsedRealtime() - TRANSITION_ANIMATION_DURATION_MS; + } + + LatLng previousLayerLatLng = getPreviousLayerLatLng(); + float previousLayerBearing = getPreviousLayerGpsBearing(); + LatLng previousCameraLatLng = currentCameraPosition.target; + float previousCameraBearing = (float) currentCameraPosition.bearing; + + LatLng targetLatLng = new LatLng(newLocation); + float targetLayerBearing = newLocation.getBearing(); + float targetCameraBearing = newLocation.getBearing(); + targetCameraBearing = checkGpsNorth(isGpsNorth, targetCameraBearing); + + updateLayerAnimators(previousLayerLatLng, targetLatLng, previousLayerBearing, targetLayerBearing); + updateCameraAnimators(previousCameraLatLng, previousCameraBearing, targetLatLng, targetCameraBearing); + + boolean snap = immediateAnimation(projection, previousCameraLatLng, targetLatLng) + || immediateAnimation(projection, previousLayerLatLng, targetLatLng); + + long animationDuration = 0; + if (!snap) { + long previousUpdateTimeStamp = locationUpdateTimestamp; + locationUpdateTimestamp = SystemClock.elapsedRealtime(); + + if (previousUpdateTimeStamp == 0) { + animationDuration = 0; + } else { + animationDuration = (long) ((locationUpdateTimestamp - previousUpdateTimeStamp) * durationMultiplier) + /* make animation slightly longer with durationMultiplier, defaults to 1.1f */; + } + + animationDuration = Math.min(animationDuration, MAX_ANIMATION_DURATION_MS); + } + + playAnimators(animationDuration, + ANIMATOR_LAYER_LATLNG, + ANIMATOR_LAYER_GPS_BEARING, + ANIMATOR_CAMERA_LATLNG, + ANIMATOR_CAMERA_GPS_BEARING); + + previousLocation = newLocation; + } + + void feedNewCompassBearing(float targetCompassBearing, @NonNull CameraPosition currentCameraPosition) { + if (previousCompassBearing < 0) { + previousCompassBearing = targetCompassBearing; + } + + float previousLayerBearing = getPreviousLayerCompassBearing(); + float previousCameraBearing = (float) currentCameraPosition.bearing; + + updateCompassAnimators(targetCompassBearing, previousLayerBearing, previousCameraBearing); + playAnimators( + compassAnimationEnabled ? COMPASS_UPDATE_RATE_MS : 0, + ANIMATOR_LAYER_COMPASS_BEARING, + ANIMATOR_CAMERA_COMPASS_BEARING); + + previousCompassBearing = targetCompassBearing; + } + + void feedNewAccuracyRadius(float targetAccuracyRadius, boolean noAnimation) { + if (previousAccuracyRadius < 0) { + previousAccuracyRadius = targetAccuracyRadius; + } + + float previousAccuracyRadius = getPreviousAccuracyRadius(); + updateAccuracyAnimators(targetAccuracyRadius, previousAccuracyRadius); + playAnimators( + noAnimation || !accuracyAnimationEnabled ? 0 : ACCURACY_RADIUS_ANIMATION_DURATION, + ANIMATOR_LAYER_ACCURACY); + + this.previousAccuracyRadius = targetAccuracyRadius; + } + + void feedNewZoomLevel(double targetZoomLevel, @NonNull CameraPosition currentCameraPosition, long animationDuration, + @Nullable MapboxMap.CancelableCallback callback) { + updateZoomAnimator((float) targetZoomLevel, (float) currentCameraPosition.zoom, callback); + playAnimators(animationDuration, ANIMATOR_ZOOM); + } + + void feedNewTilt(double targetTilt, @NonNull CameraPosition currentCameraPosition, long animationDuration, + @Nullable MapboxMap.CancelableCallback callback) { + updateTiltAnimator((float) targetTilt, (float) currentCameraPosition.tilt, callback); + playAnimators(animationDuration, ANIMATOR_TILT); + } + + private LatLng getPreviousLayerLatLng() { + LatLng previousLatLng; + MapboxAnimator latLngAnimator = animatorArray.get(ANIMATOR_LAYER_LATLNG); + if (latLngAnimator != null) { + previousLatLng = (LatLng) latLngAnimator.getAnimatedValue(); + } else { + previousLatLng = new LatLng(previousLocation); + } + return previousLatLng; + } + + private float getPreviousLayerGpsBearing() { + MapboxFloatAnimator animator = (MapboxFloatAnimator) animatorArray.get(ANIMATOR_LAYER_GPS_BEARING); + float previousBearing; + if (animator != null) { + previousBearing = (float) animator.getAnimatedValue(); + } else { + previousBearing = previousLocation.getBearing(); + } + return previousBearing; + } + + private float getPreviousLayerCompassBearing() { + MapboxFloatAnimator animator = (MapboxFloatAnimator) animatorArray.get(ANIMATOR_LAYER_COMPASS_BEARING); + + float previousBearing; + if (animator != null) { + previousBearing = (float) animator.getAnimatedValue(); + } else { + previousBearing = previousCompassBearing; + } + return previousBearing; + } + + private float getPreviousAccuracyRadius() { + MapboxAnimator animator = animatorArray.get(ANIMATOR_LAYER_ACCURACY); + float previousRadius; + if (animator != null) { + previousRadius = (float) animator.getAnimatedValue(); + } else { + previousRadius = previousAccuracyRadius; + } + return previousRadius; + } + + private void updateLayerAnimators(LatLng previousLatLng, LatLng targetLatLng, + float previousBearing, float targetBearing) { + createNewLatLngAnimator(ANIMATOR_LAYER_LATLNG, previousLatLng, targetLatLng); + + // Because Location bearing values are normalized to [0, 360] + // we need to do the same for the previous bearing value to determine the shortest path + previousBearing = Utils.normalize(previousBearing); + float normalizedLayerBearing = Utils.shortestRotation(targetBearing, previousBearing); + createNewFloatAnimator(ANIMATOR_LAYER_GPS_BEARING, previousBearing, normalizedLayerBearing); + } + + private void updateCameraAnimators(LatLng previousCameraLatLng, float previousCameraBearing, + LatLng targetLatLng, float targetBearing) { + createNewLatLngAnimator(ANIMATOR_CAMERA_LATLNG, previousCameraLatLng, targetLatLng); + + float normalizedCameraBearing = Utils.shortestRotation(targetBearing, previousCameraBearing); + createNewFloatAnimator(ANIMATOR_CAMERA_GPS_BEARING, previousCameraBearing, normalizedCameraBearing); + } + + private void updateCompassAnimators(float targetCompassBearing, float previousLayerBearing, + float previousCameraBearing) { + float normalizedLayerBearing = Utils.shortestRotation(targetCompassBearing, previousLayerBearing); + createNewFloatAnimator(ANIMATOR_LAYER_COMPASS_BEARING, previousLayerBearing, normalizedLayerBearing); + + float normalizedCameraBearing = Utils.shortestRotation(targetCompassBearing, previousCameraBearing); + createNewFloatAnimator(ANIMATOR_CAMERA_COMPASS_BEARING, previousCameraBearing, normalizedCameraBearing); + } + + private void updateAccuracyAnimators(float targetAccuracyRadius, float previousAccuracyRadius) { + createNewFloatAnimator(ANIMATOR_LAYER_ACCURACY, previousAccuracyRadius, targetAccuracyRadius); + } + + private void updateZoomAnimator(float targetZoomLevel, float previousZoomLevel, + @Nullable MapboxMap.CancelableCallback cancelableCallback) { + createNewCameraAdapterAnimator(ANIMATOR_ZOOM, previousZoomLevel, targetZoomLevel, cancelableCallback); + } + + private void updateTiltAnimator(float targetTilt, float previousTiltLevel, + @Nullable MapboxMap.CancelableCallback cancelableCallback) { + createNewCameraAdapterAnimator(ANIMATOR_TILT, previousTiltLevel, targetTilt, cancelableCallback); + } + + private void createNewLatLngAnimator(@MapboxAnimator.Type int animatorType, LatLng previous, LatLng target) { + cancelAnimator(animatorType); + MapboxAnimator.AnimationsValueChangeListener listener = listeners.get(animatorType); + if (listener != null) { + animatorArray.put(animatorType, animatorProvider.latLngAnimator(previous, target, listener, maxAnimationFps)); + } + } + + private void createNewFloatAnimator(@MapboxAnimator.Type int animatorType, float previous, float target) { + cancelAnimator(animatorType); + MapboxAnimator.AnimationsValueChangeListener listener = listeners.get(animatorType); + if (listener != null) { + animatorArray.put(animatorType, animatorProvider.floatAnimator(previous, target, listener, maxAnimationFps)); + } + } + + private void createNewCameraAdapterAnimator(@MapboxAnimator.Type int animatorType, float previous, float target, + @Nullable MapboxMap.CancelableCallback cancelableCallback) { + cancelAnimator(animatorType); + MapboxAnimator.AnimationsValueChangeListener listener = listeners.get(animatorType); + if (listener != null) { + animatorArray.put(animatorType, animatorProvider.cameraAnimator(previous, target, listener, cancelableCallback)); + } + } + + private float checkGpsNorth(boolean isGpsNorth, float targetCameraBearing) { + if (isGpsNorth) { + targetCameraBearing = 0; + } + return targetCameraBearing; + } + + private void playAnimators(long duration, @MapboxAnimator.Type int... animatorTypes) { + List animators = new ArrayList<>(); + for (@MapboxAnimator.Type int animatorType : animatorTypes) { + Animator animator = animatorArray.get(animatorType); + if (animator != null) { + animators.add(animator); + } + } + animatorSetProvider.startAnimation(animators, new LinearInterpolator(), duration); + } + + void resetAllCameraAnimations(@NonNull CameraPosition currentCameraPosition, boolean isGpsNorth) { + resetCameraCompassAnimation(currentCameraPosition); + boolean snap = resetCameraLocationAnimations(currentCameraPosition, isGpsNorth); + playAnimators( + snap ? 0 : TRANSITION_ANIMATION_DURATION_MS, + ANIMATOR_CAMERA_LATLNG, + ANIMATOR_CAMERA_GPS_BEARING); + } + + private boolean resetCameraLocationAnimations(@NonNull CameraPosition currentCameraPosition, boolean isGpsNorth) { + resetCameraGpsBearingAnimation(currentCameraPosition, isGpsNorth); + return resetCameraLatLngAnimation(currentCameraPosition); + } + + private boolean resetCameraLatLngAnimation(@NonNull CameraPosition currentCameraPosition) { + MapboxLatLngAnimator animator = (MapboxLatLngAnimator) animatorArray.get(ANIMATOR_CAMERA_LATLNG); + if (animator == null) { + return false; + } + + LatLng currentTarget = animator.getTarget(); + LatLng previousCameraTarget = currentCameraPosition.target; + createNewLatLngAnimator(ANIMATOR_CAMERA_LATLNG, previousCameraTarget, currentTarget); + + return immediateAnimation(projection, previousCameraTarget, currentTarget); + } + + private void resetCameraGpsBearingAnimation(@NonNull CameraPosition currentCameraPosition, boolean isGpsNorth) { + MapboxFloatAnimator animator = (MapboxFloatAnimator) animatorArray.get(ANIMATOR_CAMERA_GPS_BEARING); + if (animator == null) { + return; + } + + float currentTargetBearing = animator.getTarget(); + currentTargetBearing = checkGpsNorth(isGpsNorth, currentTargetBearing); + float previousCameraBearing = (float) currentCameraPosition.bearing; + float normalizedCameraBearing = Utils.shortestRotation(currentTargetBearing, previousCameraBearing); + createNewFloatAnimator(ANIMATOR_CAMERA_GPS_BEARING, previousCameraBearing, normalizedCameraBearing); + } + + private void resetCameraCompassAnimation(@NonNull CameraPosition currentCameraPosition) { + MapboxFloatAnimator animator = + (MapboxFloatAnimator) animatorArray.get(ANIMATOR_CAMERA_COMPASS_BEARING); + if (animator == null) { + return; + } + + float currentTargetBearing = animator.getTarget(); + float previousCameraBearing = (float) currentCameraPosition.bearing; + float normalizedCameraBearing = Utils.shortestRotation(currentTargetBearing, previousCameraBearing); + createNewFloatAnimator(ANIMATOR_CAMERA_COMPASS_BEARING, previousCameraBearing, normalizedCameraBearing); + } + + void resetAllLayerAnimations() { + MapboxLatLngAnimator latLngAnimator = (MapboxLatLngAnimator) animatorArray.get(ANIMATOR_LAYER_LATLNG); + MapboxFloatAnimator gpsBearingAnimator = (MapboxFloatAnimator) animatorArray.get(ANIMATOR_LAYER_GPS_BEARING); + MapboxFloatAnimator compassBearingAnimator = + (MapboxFloatAnimator) animatorArray.get(ANIMATOR_LAYER_COMPASS_BEARING); + + if (latLngAnimator != null && gpsBearingAnimator != null) { + LatLng currentLatLng = (LatLng) latLngAnimator.getAnimatedValue(); + LatLng currentLatLngTarget = latLngAnimator.getTarget(); + createNewLatLngAnimator(ANIMATOR_LAYER_LATLNG, currentLatLng, currentLatLngTarget); + + float currentGpsBearing = (float) gpsBearingAnimator.getAnimatedValue(); + float currentGpsBearingTarget = gpsBearingAnimator.getTarget(); + createNewFloatAnimator(ANIMATOR_LAYER_GPS_BEARING, currentGpsBearing, currentGpsBearingTarget); + + long duration = latLngAnimator.getDuration() - latLngAnimator.getCurrentPlayTime(); + + playAnimators(duration, ANIMATOR_LAYER_LATLNG, ANIMATOR_LAYER_GPS_BEARING); + } + + if (compassBearingAnimator != null) { + float currentLayerBearing = getPreviousLayerCompassBearing(); + float currentLayerBearingTarget = compassBearingAnimator.getTarget(); + createNewFloatAnimator(ANIMATOR_LAYER_COMPASS_BEARING, currentLayerBearing, currentLayerBearingTarget); + playAnimators( + compassAnimationEnabled ? COMPASS_UPDATE_RATE_MS : 0, + ANIMATOR_LAYER_COMPASS_BEARING); + } + } + + void cancelZoomAnimation() { + cancelAnimator(ANIMATOR_ZOOM); + } + + void cancelTiltAnimation() { + cancelAnimator(ANIMATOR_TILT); + } + + void cancelAllAnimations() { + for (int i = 0; i < animatorArray.size(); i++) { + @MapboxAnimator.Type int animatorType = animatorArray.keyAt(i); + cancelAnimator(animatorType); + } + } + + private void cancelAnimator(@MapboxAnimator.Type int animatorType) { + MapboxAnimator animator = animatorArray.get(animatorType); + if (animator != null) { + animator.cancel(); + animator.removeAllUpdateListeners(); + animator.removeAllListeners(); + animatorArray.put(animatorType, null); + } + } + + void setTrackingAnimationDurationMultiplier(float trackingAnimationDurationMultiplier) { + this.durationMultiplier = trackingAnimationDurationMultiplier; + } + + void setCompassAnimationEnabled(boolean compassAnimationEnabled) { + this.compassAnimationEnabled = compassAnimationEnabled; + } + + void setAccuracyAnimationEnabled(boolean accuracyAnimationEnabled) { + this.accuracyAnimationEnabled = accuracyAnimationEnabled; + } + + void setMaxAnimationFps(int maxAnimationFps) { + if (maxAnimationFps <= 0) { + Logger.e(TAG, "Max animation FPS cannot be less or equal to 0."); + return; + } + this.maxAnimationFps = maxAnimationFps; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationCameraController.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationCameraController.java new file mode 100644 index 000000000..79d2e02bf --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationCameraController.java @@ -0,0 +1,437 @@ +package com.mapbox.mapboxsdk.location; + +import android.content.Context; +import android.graphics.PointF; +import android.location.Location; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.view.MotionEvent; + +import com.mapbox.android.gestures.AndroidGesturesManager; +import com.mapbox.android.gestures.MoveGestureDetector; +import com.mapbox.android.gestures.RotateGestureDetector; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.camera.CameraUpdate; +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.location.modes.CameraMode; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.Transform; + +import java.util.HashSet; +import java.util.Set; + +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.TRANSITION_ANIMATION_DURATION_MS; + +final class LocationCameraController { + + @CameraMode.Mode + private int cameraMode; + + private final MapboxMap mapboxMap; + private final Transform transform; + private final OnCameraTrackingChangedListener internalCameraTrackingChangedListener; + private LocationComponentOptions options; + private boolean adjustFocalPoint; + + private final MoveGestureDetector moveGestureDetector; + private final OnCameraMoveInvalidateListener onCameraMoveInvalidateListener; + + private final AndroidGesturesManager initialGesturesManager; + private final AndroidGesturesManager internalGesturesManager; + + private boolean isTransitioning; + + LocationCameraController( + Context context, + MapboxMap mapboxMap, + Transform transform, + OnCameraTrackingChangedListener internalCameraTrackingChangedListener, + @NonNull LocationComponentOptions options, + OnCameraMoveInvalidateListener onCameraMoveInvalidateListener) { + this.mapboxMap = mapboxMap; + this.transform = transform; + + initialGesturesManager = mapboxMap.getGesturesManager(); + internalGesturesManager = new LocationGesturesManager(context); + moveGestureDetector = internalGesturesManager.getMoveGestureDetector(); + mapboxMap.addOnRotateListener(onRotateListener); + mapboxMap.addOnFlingListener(onFlingListener); + mapboxMap.addOnMoveListener(onMoveListener); + + this.internalCameraTrackingChangedListener = internalCameraTrackingChangedListener; + this.onCameraMoveInvalidateListener = onCameraMoveInvalidateListener; + initializeOptions(options); + } + + // Package private for testing purposes + LocationCameraController(MapboxMap mapboxMap, + Transform transform, + MoveGestureDetector moveGestureDetector, + OnCameraTrackingChangedListener internalCameraTrackingChangedListener, + OnCameraMoveInvalidateListener onCameraMoveInvalidateListener, + AndroidGesturesManager initialGesturesManager, + AndroidGesturesManager internalGesturesManager) { + this.mapboxMap = mapboxMap; + this.transform = transform; + this.moveGestureDetector = moveGestureDetector; + this.internalCameraTrackingChangedListener = internalCameraTrackingChangedListener; + this.onCameraMoveInvalidateListener = onCameraMoveInvalidateListener; + this.internalGesturesManager = internalGesturesManager; + this.initialGesturesManager = initialGesturesManager; + } + + void initializeOptions(LocationComponentOptions options) { + this.options = options; + if (options.trackingGesturesManagement()) { + if (mapboxMap.getGesturesManager() != internalGesturesManager) { + mapboxMap.setGesturesManager(internalGesturesManager, true, true); + } + adjustGesturesThresholds(); + } else if (mapboxMap.getGesturesManager() != initialGesturesManager) { + mapboxMap.setGesturesManager(initialGesturesManager, true, true); + } + } + + void setCameraMode(@CameraMode.Mode int cameraMode) { + setCameraMode(cameraMode, null, TRANSITION_ANIMATION_DURATION_MS, null, null, null, null); + } + + void setCameraMode(@CameraMode.Mode final int cameraMode, @Nullable Location lastLocation, + long transitionDuration, + @Nullable Double zoom, @Nullable Double bearing, @Nullable Double tilt, + @Nullable OnLocationCameraTransitionListener internalTransitionListener) { + if (this.cameraMode == cameraMode) { + return; + } + + final boolean wasTracking = isLocationTracking(); + this.cameraMode = cameraMode; + + if (cameraMode != CameraMode.NONE) { + mapboxMap.cancelTransitions(); + } + + adjustGesturesThresholds(); + notifyCameraTrackingChangeListener(wasTracking); + transitionToCurrentLocation( + wasTracking, lastLocation, transitionDuration, zoom, bearing, tilt, internalTransitionListener); + } + + /** + * Initiates a camera animation to the current location if location tracking was engaged. + * Notifies an internal listener when the transition's finished to invalidate animators and notify external listeners. + */ + private void transitionToCurrentLocation(boolean wasTracking, Location lastLocation, + long transitionDuration, + Double zoom, Double bearing, Double tilt, + final OnLocationCameraTransitionListener internalTransitionListener) { + if (!wasTracking && isLocationTracking() && lastLocation != null) { + isTransitioning = true; + LatLng target = new LatLng(lastLocation); + + CameraPosition.Builder builder = new CameraPosition.Builder().target(target); + if (zoom != null) { + builder.zoom(zoom); + } + if (tilt != null) { + builder.tilt(tilt); + } + if (bearing != null) { + builder.bearing(bearing); + } else { + if (isLocationBearingTracking()) { + builder.bearing(cameraMode == CameraMode.TRACKING_GPS_NORTH ? 0 : lastLocation.getBearing()); + } + } + + CameraUpdate update = CameraUpdateFactory.newCameraPosition(builder.build()); + MapboxMap.CancelableCallback callback = new MapboxMap.CancelableCallback() { + @Override + public void onCancel() { + isTransitioning = false; + if (internalTransitionListener != null) { + internalTransitionListener.onLocationCameraTransitionCanceled(cameraMode); + } + } + + @Override + public void onFinish() { + isTransitioning = false; + if (internalTransitionListener != null) { + internalTransitionListener.onLocationCameraTransitionFinished(cameraMode); + } + } + }; + + CameraPosition currentPosition = mapboxMap.getCameraPosition(); + if (Utils.immediateAnimation(mapboxMap.getProjection(), currentPosition.target, target)) { + transform.moveCamera( + mapboxMap, + update, + callback); + } else { + transform.animateCamera( + mapboxMap, + update, + (int) transitionDuration, + callback); + } + } else { + if (internalTransitionListener != null) { + internalTransitionListener.onLocationCameraTransitionFinished(cameraMode); + } + } + } + + int getCameraMode() { + return cameraMode; + } + + private void setBearing(float bearing) { + if (isTransitioning) { + return; + } + + transform.moveCamera(mapboxMap, CameraUpdateFactory.bearingTo(bearing), null); + onCameraMoveInvalidateListener.onInvalidateCameraMove(); + } + + private void setLatLng(@NonNull LatLng latLng) { + if (isTransitioning) { + return; + } + + transform.moveCamera(mapboxMap, CameraUpdateFactory.newLatLng(latLng), null); + onCameraMoveInvalidateListener.onInvalidateCameraMove(); + + if (adjustFocalPoint) { + PointF focalPoint = mapboxMap.getProjection().toScreenLocation(latLng); + mapboxMap.getUiSettings().setFocalPoint(focalPoint); + adjustFocalPoint = false; + } + } + + private void setZoom(float zoom) { + if (isTransitioning) { + return; + } + + transform.moveCamera(mapboxMap, CameraUpdateFactory.zoomTo(zoom), null); + onCameraMoveInvalidateListener.onInvalidateCameraMove(); + } + + private void setTilt(float tilt) { + if (isTransitioning) { + return; + } + + transform.moveCamera(mapboxMap, CameraUpdateFactory.tiltTo(tilt), null); + onCameraMoveInvalidateListener.onInvalidateCameraMove(); + } + + private final MapboxAnimator.AnimationsValueChangeListener latLngValueListener = + new MapboxAnimator.AnimationsValueChangeListener() { + @Override + public void onNewAnimationValue(LatLng value) { + setLatLng(value); + } + }; + + private final MapboxAnimator.AnimationsValueChangeListener gpsBearingValueListener = + new MapboxAnimator.AnimationsValueChangeListener() { + @Override + public void onNewAnimationValue(Float value) { + boolean trackingNorth = cameraMode == CameraMode.TRACKING_GPS_NORTH + && mapboxMap.getCameraPosition().bearing == 0; + + if (!trackingNorth) { + setBearing(value); + } + } + }; + + private final MapboxAnimator.AnimationsValueChangeListener compassBearingValueListener = + new MapboxAnimator.AnimationsValueChangeListener() { + @Override + public void onNewAnimationValue(Float value) { + if (cameraMode == CameraMode.TRACKING_COMPASS + || cameraMode == CameraMode.NONE_COMPASS) { + setBearing(value); + } + } + }; + + private final MapboxAnimator.AnimationsValueChangeListener zoomValueListener = + new MapboxAnimator.AnimationsValueChangeListener() { + @Override + public void onNewAnimationValue(Float value) { + setZoom(value); + } + }; + + private final MapboxAnimator.AnimationsValueChangeListener tiltValueListener = + new MapboxAnimator.AnimationsValueChangeListener() { + @Override + public void onNewAnimationValue(Float value) { + setTilt(value); + } + }; + + Set getAnimationListeners() { + Set holders = new HashSet<>(); + if (isLocationTracking()) { + holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_CAMERA_LATLNG, latLngValueListener)); + } + + if (isLocationBearingTracking()) { + holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_CAMERA_GPS_BEARING, gpsBearingValueListener)); + } + + if (isConsumingCompass()) { + holders.add(new AnimatorListenerHolder( + MapboxAnimator.ANIMATOR_CAMERA_COMPASS_BEARING, + compassBearingValueListener)); + } + + holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_ZOOM, zoomValueListener)); + holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_TILT, tiltValueListener)); + return holders; + } + + boolean isTransitioning() { + return isTransitioning; + } + + private void adjustGesturesThresholds() { + if (options.trackingGesturesManagement()) { + if (isLocationTracking()) { + adjustFocalPoint = true; + moveGestureDetector.setMoveThreshold(options.trackingInitialMoveThreshold()); + } else { + moveGestureDetector.setMoveThreshold(0f); + } + } + } + + boolean isConsumingCompass() { + return cameraMode == CameraMode.TRACKING_COMPASS + || cameraMode == CameraMode.NONE_COMPASS; + } + + private boolean isLocationTracking() { + return cameraMode == CameraMode.TRACKING + || cameraMode == CameraMode.TRACKING_COMPASS + || cameraMode == CameraMode.TRACKING_GPS + || cameraMode == CameraMode.TRACKING_GPS_NORTH; + } + + private boolean isBearingTracking() { + return cameraMode == CameraMode.NONE_COMPASS + || cameraMode == CameraMode.TRACKING_COMPASS + || cameraMode == CameraMode.NONE_GPS + || cameraMode == CameraMode.TRACKING_GPS + || cameraMode == CameraMode.TRACKING_GPS_NORTH; + } + + private boolean isLocationBearingTracking() { + return cameraMode == CameraMode.TRACKING_GPS + || cameraMode == CameraMode.TRACKING_GPS_NORTH + || cameraMode == CameraMode.NONE_GPS; + } + + private void notifyCameraTrackingChangeListener(boolean wasTracking) { + internalCameraTrackingChangedListener.onCameraTrackingChanged(cameraMode); + if (wasTracking && !isLocationTracking()) { + mapboxMap.getUiSettings().setFocalPoint(null); + internalCameraTrackingChangedListener.onCameraTrackingDismissed(); + } + } + + @NonNull + @VisibleForTesting + MapboxMap.OnMoveListener onMoveListener = new MapboxMap.OnMoveListener() { + private boolean interrupt; + + @Override + public void onMoveBegin(@NonNull MoveGestureDetector detector) { + if (options.trackingGesturesManagement() + && detector.getPointersCount() > 1 + && detector.getMoveThreshold() != options.trackingMultiFingerMoveThreshold() + && isLocationTracking()) { + detector.setMoveThreshold(options.trackingMultiFingerMoveThreshold()); + interrupt = true; + } else { + setCameraMode(CameraMode.NONE); + } + } + + @Override + public void onMove(@NonNull MoveGestureDetector detector) { + if (interrupt) { + detector.interrupt(); + return; + } + + if (isLocationTracking() || isBearingTracking()) { + setCameraMode(CameraMode.NONE); + detector.interrupt(); + } + } + + @Override + public void onMoveEnd(@NonNull MoveGestureDetector detector) { + if (options.trackingGesturesManagement() && !interrupt && isLocationTracking()) { + detector.setMoveThreshold(options.trackingInitialMoveThreshold()); + } + interrupt = false; + } + }; + + @NonNull + private MapboxMap.OnRotateListener onRotateListener = new MapboxMap.OnRotateListener() { + @Override + public void onRotateBegin(@NonNull RotateGestureDetector detector) { + if (isBearingTracking()) { + setCameraMode(CameraMode.NONE); + } + } + + @Override + public void onRotate(@NonNull RotateGestureDetector detector) { + // no implementation + } + + @Override + public void onRotateEnd(@NonNull RotateGestureDetector detector) { + // no implementation + } + }; + + @NonNull + private MapboxMap.OnFlingListener onFlingListener = new MapboxMap.OnFlingListener() { + @Override + public void onFling() { + setCameraMode(CameraMode.NONE); + } + }; + + private class LocationGesturesManager extends AndroidGesturesManager { + + LocationGesturesManager(Context context) { + super(context); + } + + @Override + public boolean onTouchEvent(@Nullable MotionEvent motionEvent) { + if (motionEvent != null) { + int action = motionEvent.getActionMasked(); + if (action == MotionEvent.ACTION_UP) { + adjustGesturesThresholds(); + } + } + return super.onTouchEvent(motionEvent); + } + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java new file mode 100644 index 000000000..46de3673e --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java @@ -0,0 +1,1586 @@ +package com.mapbox.mapboxsdk.location; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.hardware.SensorManager; +import android.location.Location; +import android.os.Looper; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresPermission; +import android.support.annotation.StyleRes; +import android.support.annotation.VisibleForTesting; +import android.view.WindowManager; + +import com.mapbox.android.core.location.LocationEngine; +import com.mapbox.android.core.location.LocationEngineCallback; +import com.mapbox.android.core.location.LocationEngineProvider; +import com.mapbox.android.core.location.LocationEngineRequest; +import com.mapbox.android.core.location.LocationEngineResult; +import com.mapbox.android.core.permissions.PermissionsManager; +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.camera.CameraUpdate; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.location.modes.CameraMode; +import com.mapbox.mapboxsdk.location.modes.RenderMode; +import com.mapbox.mapboxsdk.log.Logger; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraIdleListener; +import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveListener; +import com.mapbox.mapboxsdk.maps.MapboxMap.OnMapClickListener; +import com.mapbox.mapboxsdk.maps.Style; +import com.mapbox.mapboxsdk.maps.Transform; + +import java.lang.ref.WeakReference; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_FASTEST_INTERVAL_MILLIS; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_INTERVAL_MILLIS; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_TRACKING_TILT_ANIM_DURATION; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_TRACKING_ZOOM_ANIM_DURATION; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.TRANSITION_ANIMATION_DURATION_MS; + +/** + * The Location Component provides location awareness to your mobile application. Enabling this + * component provides a contextual experience to your users by showing an icon representing the users + * current location. A few different modes are offered to provide the right context to your users at + * the correct time. {@link RenderMode#NORMAL} simply shows the users location on the map + * represented as a dot. {@link RenderMode#COMPASS} mode allows you to display an arrow icon + * (by default) that points in the direction the device is pointing in. + * {@link RenderMode#GPS} can be used in conjunction with our Navigation SDK to + * display a larger icon (customized with {@link LocationComponentOptions#gpsDrawable()}) we call the user puck. + *

+ * This component also offers the ability to set a map camera behavior for tracking the user + * location. These different {@link CameraMode}s will track, stop tracking the location based on the + * mode set with {@link LocationComponent#setCameraMode(int)}. + *

+ * + * To get the component object use {@link MapboxMap#getLocationComponent()} and activate it with + * {@link #activateLocationComponent(Context, Style)} or one of the overloads. + * Then, manage its visibility with {@link #setLocationComponentEnabled(boolean)}. + * The component will not process location updates right after activation, but only after being enabled. + * + *

+ * Using this component requires you to request permission beforehand manually or using + * {@link PermissionsManager}. Either + * {@code ACCESS_COARSE_LOCATION} or {@code ACCESS_FINE_LOCATION} permissions can be requested for + * this component to work as expected. + *

+ * This component offers a default, built-in {@link LocationEngine} with some of the activation methods. + * This engine will be obtained by {@link LocationEngineProvider#getBestLocationEngine(Context, boolean)} which defaults + * to the {@link com.mapbox.android.core.location.MapboxFusedLocationEngineImpl}. If you'd like to utilize Google + * Play Services + * for more precise location updates, simply add the Google Play Location Services dependency in your build script. + * This will make the default engine the {@link com.mapbox.android.core.location.GoogleLocationEngineImpl} instead. + * After a custom engine is passed to the component, or the built-in is initialized, + * the location updates are going to be requested with the {@link LocationEngineRequest}, either a default one, + * or the one passed during the activation. + * When using any engine, requesting/removing the location updates is going to be managed internally. + *

+ * You can also push location updates to the component without any internal engine management. + * To achieve that, use {@link #activateLocationComponent(Context, Style, boolean)} with false. + * No engine is going to be initialized and you can push location updates with {@link #forceLocationUpdate(Location)}. + *

+ * For location puck animation purposes, like navigation, + * we recommend limiting the maximum zoom level of the map for the best user experience. + *

+ * Location Component doesn't support state saving out-of-the-box. + */ +public final class LocationComponent { + private static final String TAG = "Mbgl-LocationComponent"; + + @NonNull + private final MapboxMap mapboxMap; + @NonNull + private final Transform transform; + private Style style; + private LocationComponentOptions options; + @NonNull + private InternalLocationEngineProvider internalLocationEngineProvider = new InternalLocationEngineProvider(); + @Nullable + private LocationEngine locationEngine; + @NonNull + private LocationEngineRequest locationEngineRequest = + new LocationEngineRequest.Builder(DEFAULT_INTERVAL_MILLIS) + .setFastestInterval(DEFAULT_FASTEST_INTERVAL_MILLIS) + .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) + .build(); + private LocationEngineCallback currentLocationEngineListener + = new CurrentLocationEngineCallback(this); + private LocationEngineCallback lastLocationEngineListener + = new LastLocationEngineCallback(this); + + @Nullable + private CompassEngine compassEngine; + + private LocationLayerController locationLayerController; + private LocationCameraController locationCameraController; + + private LocationAnimatorCoordinator locationAnimatorCoordinator; + + /** + * Holds last location which is being returned in the {@link #getLastKnownLocation()} + * when there is no {@link #locationEngine} set or when the last location returned by the engine is null. + */ + @Nullable + private Location lastLocation; + private CameraPosition lastCameraPosition; + + /** + * Indicates whether the component has been initialized. + */ + private boolean isComponentInitialized; + + /** + * Indicates that the component is enabled and should be displaying location if Mapbox components are available and + * the lifecycle is in a started state. + */ + private boolean isEnabled; + + /** + * Indicated that component's lifecycle {@link #onStart()} method has been called. + * This allows Mapbox components enter started state and display data, and adds state safety for methods like + * {@link #setLocationComponentEnabled(boolean)} + */ + private boolean isComponentStarted; + + /** + * Indicates if Mapbox components are ready to be interacted with. This can differ from {@link #isComponentStarted} + * if the Mapbox style is being reloaded. + */ + private boolean isLayerReady; + + /** + * Indicates whether we are listening for compass updates. + */ + private boolean isListeningToCompass; + + private StaleStateManager staleStateManager; + private final CopyOnWriteArrayList onLocationStaleListeners + = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList onLocationClickListeners + = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList onLocationLongClickListeners + = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList onCameraTrackingChangedListeners + = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList onRenderModeChangedListeners + = new CopyOnWriteArrayList<>(); + + // Workaround for too frequent updates, see https://github.com/mapbox/mapbox-gl-native/issues/13587 + private long fastestInterval; + private long lastUpdateTime; + + /** + * Internal use. + *

+ * To get the component object use {@link MapboxMap#getLocationComponent()}. + */ + public LocationComponent(@NonNull MapboxMap mapboxMap, + @NonNull Transform transform, + @NonNull List developerAnimationListeners) { + this.mapboxMap = mapboxMap; + this.transform = transform; + developerAnimationListeners.add(developerAnimationListener); + } + + // used for creating a spy + LocationComponent() { + //noinspection ConstantConditions + mapboxMap = null; + transform = null; + } + + @VisibleForTesting + LocationComponent(@NonNull MapboxMap mapboxMap, + @NonNull Transform transform, + @NonNull List developerAnimationListeners, + @NonNull LocationEngineCallback currentListener, + @NonNull LocationEngineCallback lastListener, + @NonNull LocationLayerController locationLayerController, + @NonNull LocationCameraController locationCameraController, + @NonNull LocationAnimatorCoordinator locationAnimatorCoordinator, + @NonNull StaleStateManager staleStateManager, + @NonNull CompassEngine compassEngine, + @NonNull InternalLocationEngineProvider internalLocationEngineProvider) { + this.mapboxMap = mapboxMap; + this.transform = transform; + developerAnimationListeners.add(developerAnimationListener); + this.currentLocationEngineListener = currentListener; + this.lastLocationEngineListener = lastListener; + this.locationLayerController = locationLayerController; + this.locationCameraController = locationCameraController; + this.locationAnimatorCoordinator = locationAnimatorCoordinator; + this.staleStateManager = staleStateManager; + this.compassEngine = compassEngine; + this.internalLocationEngineProvider = internalLocationEngineProvider; + isComponentInitialized = true; + } + + /** + * This method initializes the component and needs to be called before any other operations are performed. + * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. + *

+ * Note: This method will initialize and use an internal {@link LocationEngine} when enabled. + * + * @param context the context + * @param style the proxy object for current map style. More info at {@link Style} + * @deprecated use {@link LocationComponentActivationOptions.Builder} instead + */ + @Deprecated + public void activateLocationComponent(@NonNull Context context, @NonNull Style style) { + activateLocationComponent(context, style, + LocationComponentOptions.createFromAttributes(context, R.style.mapbox_LocationComponent)); + } + + /** + * This method initializes the component and needs to be called before any other operations are performed. + * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. + * + * @param context the context + * @param style the proxy object for current map style. More info at {@link Style} + * @param useDefaultLocationEngine true if you want to initialize and use the built-in location engine or false if + * there should be no location engine initialized + * @deprecated use {@link LocationComponentActivationOptions.Builder} instead + */ + @Deprecated + public void activateLocationComponent(@NonNull Context context, @NonNull Style style, + boolean useDefaultLocationEngine) { + if (useDefaultLocationEngine) { + activateLocationComponent(context, style, R.style.mapbox_LocationComponent); + } else { + activateLocationComponent(context, style, null, R.style.mapbox_LocationComponent); + } + } + + /** + * This method initializes the component and needs to be called before any other operations are performed. + * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. + * + * @param context the context + * @param style the proxy object for current map style. More info at {@link Style} + * @param useDefaultLocationEngine true if you want to initialize and use the built-in location engine or false if + * there should be no location engine initialized + * @param locationEngineRequest the location request + * @deprecated use {@link LocationComponentActivationOptions.Builder} instead + */ + @Deprecated + public void activateLocationComponent(@NonNull Context context, @NonNull Style style, + boolean useDefaultLocationEngine, + @NonNull LocationEngineRequest locationEngineRequest) { + setLocationEngineRequest(locationEngineRequest); + if (useDefaultLocationEngine) { + activateLocationComponent(context, style, R.style.mapbox_LocationComponent); + } else { + activateLocationComponent(context, style, null, R.style.mapbox_LocationComponent); + } + } + + /** + * This method initializes the component and needs to be called before any other operations are performed. + * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. + * + * @param context the context + * @param style the proxy object for current map style. More info at {@link Style} + * @param useDefaultLocationEngine true if you want to initialize and use the built-in location engine or false if + * there should be no location engine initialized + * @param locationEngineRequest the location request + * @param options the options + * @deprecated use {@link LocationComponentActivationOptions.Builder} instead + */ + @Deprecated + public void activateLocationComponent(@NonNull Context context, @NonNull Style style, + boolean useDefaultLocationEngine, + @NonNull LocationEngineRequest locationEngineRequest, + @NonNull LocationComponentOptions options) { + setLocationEngineRequest(locationEngineRequest); + if (useDefaultLocationEngine) { + activateLocationComponent(context, style, options); + } else { + activateLocationComponent(context, style, null, options); + } + } + + /** + * This method initializes the component and needs to be called before any other operations are performed. + * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. + *

+ * Note: This method will initialize and use an internal {@link LocationEngine} when enabled. + * + * @param context the context + * @param style the proxy object for current map style. More info at {@link Style} + * @param styleRes the LocationComponent style res + * @deprecated use {@link LocationComponentActivationOptions.Builder} instead + */ + @Deprecated + public void activateLocationComponent(@NonNull Context context, @NonNull Style style, @StyleRes int styleRes) { + activateLocationComponent(context, style, LocationComponentOptions.createFromAttributes(context, styleRes)); + } + + /** + * This method initializes the component and needs to be called before any other operations are performed. + * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. + *

+ * Note: This method will initialize and use an internal {@link LocationEngine} when enabled. + *

+ * + * @param context the context + * @param style the proxy object for current map style. More info at {@link Style} + * @param options the options + * @deprecated use {@link LocationComponentActivationOptions.Builder} instead + */ + @Deprecated + public void activateLocationComponent(@NonNull Context context, @NonNull Style style, + @NonNull LocationComponentOptions options) { + initialize(context, style, options); + initializeLocationEngine(context); + applyStyle(options); + } + + /** + * This method initializes the component and needs to be called before any other operations are performed. + * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. + * + * @param context the context + * @param style the proxy object for current map style. More info at {@link Style} + * @param locationEngine the engine, or null if you'd like to only force location updates + * @param styleRes the LocationComponent style res + * @deprecated use {@link LocationComponentActivationOptions.Builder} instead + */ + @Deprecated + public void activateLocationComponent(@NonNull Context context, @NonNull Style style, + @Nullable LocationEngine locationEngine, @StyleRes int styleRes) { + activateLocationComponent(context, style, locationEngine, + LocationComponentOptions.createFromAttributes(context, styleRes)); + } + + /** + * This method initializes the component and needs to be called before any other operations are performed. + * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. + * + * @param context the context + * @param style the proxy object for current map style. More info at {@link Style} + * @param locationEngine the engine, or null if you'd like to only force location updates + * @param locationEngineRequest the location request + * @param styleRes the LocationComponent style res + * @deprecated use {@link LocationComponentActivationOptions.Builder} instead + */ + @Deprecated + public void activateLocationComponent(@NonNull Context context, @NonNull Style style, + @Nullable LocationEngine locationEngine, + @NonNull LocationEngineRequest locationEngineRequest, @StyleRes int styleRes) { + activateLocationComponent(context, style, locationEngine, locationEngineRequest, + LocationComponentOptions.createFromAttributes(context, styleRes)); + } + + /** + * This method will show the location icon and enable the camera tracking the location. + * + * @param context the context + * @param style the proxy object for current map style. More info at {@link Style} + * @param locationEngine the engine + * @deprecated use {@link LocationComponentActivationOptions.Builder} instead + */ + @Deprecated + public void activateLocationComponent(@NonNull Context context, @NonNull Style style, + @Nullable LocationEngine locationEngine) { + activateLocationComponent(context, style, locationEngine, R.style.mapbox_LocationComponent); + } + + /** + * This method will show the location icon and enable the camera tracking the location. + * + * @param context the context + * @param style the proxy object for current map style. More info at {@link Style} + * @param locationEngine the engine + * @param locationEngineRequest the location request + * @deprecated use {@link LocationComponentActivationOptions.Builder} instead + */ + @Deprecated + public void activateLocationComponent(@NonNull Context context, @NonNull Style style, + @Nullable LocationEngine locationEngine, + @NonNull LocationEngineRequest locationEngineRequest) { + activateLocationComponent(context, style, locationEngine, locationEngineRequest, R.style.mapbox_LocationComponent); + } + + /** + * This method initializes the component and needs to be called before any other operations are performed. + * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. + * + * @param locationEngine the engine, or null if you'd like to only force location updates + * @param style the proxy object for current map style. More info at {@link Style} + * @param options the options + * @deprecated use {@link LocationComponentActivationOptions.Builder} instead + */ + @Deprecated + public void activateLocationComponent(@NonNull Context context, @NonNull Style style, + @Nullable LocationEngine locationEngine, + @NonNull LocationComponentOptions options) { + initialize(context, style, options); + setLocationEngine(locationEngine); + applyStyle(options); + } + + /** + * This method initializes the component and needs to be called before any other operations are performed. + * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. + * + * @param context the context + * @param style the proxy object for current map style. More info at {@link Style} + * @param locationEngine the engine, or null if you'd like to only force location updates + * @param locationEngineRequest the location request + * @param options the options + * @deprecated use {@link LocationComponentActivationOptions.Builder} instead + */ + @Deprecated + public void activateLocationComponent(@NonNull Context context, @NonNull Style style, + @Nullable LocationEngine locationEngine, + @NonNull LocationEngineRequest locationEngineRequest, + @NonNull LocationComponentOptions options) { + initialize(context, style, options); + setLocationEngineRequest(locationEngineRequest); + setLocationEngine(locationEngine); + applyStyle(options); + } + + /** + * This method initializes the component and needs to be called before any other operations are performed. + * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. + * + * @param activationOptions a fully built {@link LocationComponentActivationOptions} object + */ + public void activateLocationComponent(@NonNull LocationComponentActivationOptions activationOptions) { + LocationComponentOptions options = activationOptions.locationComponentOptions(); + if (options == null) { + int styleRes = activationOptions.styleRes(); + if (styleRes == 0) { + styleRes = R.style.mapbox_LocationComponent; + } + options = LocationComponentOptions.createFromAttributes(activationOptions.context(), styleRes); + } + + // Initialize the LocationComponent with Context, the map's `Style`, and either custom LocationComponentOptions + // or backup options created from default/custom attributes + initialize(activationOptions.context(), activationOptions.style(), options); + + // Apply the LocationComponent styling + // TODO avoid doubling style initialization + applyStyle(options); + + // Set the LocationEngine request if one was given to LocationComponentActivationOptions + LocationEngineRequest locationEngineRequest = activationOptions.locationEngineRequest(); + if (locationEngineRequest != null) { + setLocationEngineRequest(locationEngineRequest); + } + + // Set the LocationEngine if one was given to LocationComponentActivationOptions + LocationEngine locationEngine = activationOptions.locationEngine(); + if (locationEngine != null) { + setLocationEngine(locationEngine); + } else { + if (activationOptions.useDefaultLocationEngine()) { + initializeLocationEngine(activationOptions.context()); + } else { + setLocationEngine(null); + } + } + } + + /** + * Manage component's visibility after activation. + * + * @param isEnabled true if the plugin should be visible and listen for location updates, false otherwise. + */ + @RequiresPermission(anyOf = {ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION}) + public void setLocationComponentEnabled(boolean isEnabled) { + checkActivationState(); + if (isEnabled) { + enableLocationComponent(); + } else { + disableLocationComponent(); + } + } + + /** + * Returns whether the plugin is enabled, meaning that location can be displayed and camera modes can be used. + * + * @return true if the plugin is enabled, false otherwise + */ + public boolean isLocationComponentEnabled() { + checkActivationState(); + return isEnabled; + } + + /** + * Sets the camera mode, which determines how the map camera will track the rendered location. + *

+ * When camera is transitioning to a new mode, it will reject inputs like {@link #zoomWhileTracking(double)} or + * {@link #tiltWhileTracking(double)}. + * Use {@link OnLocationCameraTransitionListener} to listen for the transition state. + *

+ *

    + *
  • {@link CameraMode#NONE}: No camera tracking
  • + *
  • {@link CameraMode#NONE_COMPASS}: Camera does not track location, but does track compass bearing
  • + *
  • {@link CameraMode#NONE_GPS}: Camera does not track location, but does track GPS bearing
  • + *
  • {@link CameraMode#TRACKING}: Camera tracks the user location
  • + *
  • {@link CameraMode#TRACKING_COMPASS}: Camera tracks the user location, with bearing provided by a compass
  • + *
  • {@link CameraMode#TRACKING_GPS}: Camera tracks the user location, with normalized bearing
  • + *
  • {@link CameraMode#TRACKING_GPS_NORTH}: Camera tracks the user location, with bearing always set to north
  • + *
+ * + * @param cameraMode one of the modes found in {@link CameraMode} + */ + public void setCameraMode(@CameraMode.Mode int cameraMode) { + setCameraMode(cameraMode, null); + } + + /** + * Sets the camera mode, which determines how the map camera will track the rendered location. + *

+ * When camera is transitioning to a new mode, it will reject inputs like {@link #zoomWhileTracking(double)} or + * {@link #tiltWhileTracking(double)}. + * Use {@link OnLocationCameraTransitionListener} to listen for the transition state. + *

+ *

    + *
  • {@link CameraMode#NONE}: No camera tracking
  • + *
  • {@link CameraMode#NONE_COMPASS}: Camera does not track location, but does track compass bearing
  • + *
  • {@link CameraMode#NONE_GPS}: Camera does not track location, but does track GPS bearing
  • + *
  • {@link CameraMode#TRACKING}: Camera tracks the user location
  • + *
  • {@link CameraMode#TRACKING_COMPASS}: Camera tracks the user location, with bearing provided by a compass
  • + *
  • {@link CameraMode#TRACKING_GPS}: Camera tracks the user location, with normalized bearing
  • + *
  • {@link CameraMode#TRACKING_GPS_NORTH}: Camera tracks the user location, with bearing always set to north
  • + *
+ * + * @param cameraMode one of the modes found in {@link CameraMode} + * @param transitionListener callback that's going to be invoked when the transition animation finishes + */ + public void setCameraMode(@CameraMode.Mode int cameraMode, + @Nullable OnLocationCameraTransitionListener transitionListener) { + setCameraMode(cameraMode, TRANSITION_ANIMATION_DURATION_MS, null, null, null, transitionListener); + } + + /** + * Sets the camera mode, which determines how the map camera will track the rendered location. + *

+ * When camera is transitioning to a new mode, it will reject inputs like {@link #zoomWhileTracking(double)} or + * {@link #tiltWhileTracking(double)}. + * Use {@link OnLocationCameraTransitionListener} to listen for the transition state. + *

+ * Set values of zoom, bearing and tilt that the camera will transition to. If null is passed to any of those, + * current value will be used for that parameter instead. + * If the camera is already tracking, provided values are ignored. + *

+ *

    + *
  • {@link CameraMode#NONE}: No camera tracking
  • + *
  • {@link CameraMode#NONE_COMPASS}: Camera does not track location, but does track compass bearing
  • + *
  • {@link CameraMode#NONE_GPS}: Camera does not track location, but does track GPS bearing
  • + *
  • {@link CameraMode#TRACKING}: Camera tracks the user location
  • + *
  • {@link CameraMode#TRACKING_COMPASS}: Camera tracks the user location, with bearing provided by a compass
  • + *
  • {@link CameraMode#TRACKING_GPS}: Camera tracks the user location, with normalized bearing
  • + *
  • {@link CameraMode#TRACKING_GPS_NORTH}: Camera tracks the user location, with bearing always set to north
  • + *
+ * + * @param cameraMode one of the modes found in {@link CameraMode} + * @param transitionDuration duration of the transition in milliseconds + * @param zoom target zoom, set to null to use current camera position + * @param bearing target bearing, set to null to use current camera position + * @param tilt target tilt, set to null to use current camera position + * @param transitionListener callback that's going to be invoked when the transition animation finishes + */ + public void setCameraMode(@CameraMode.Mode int cameraMode, + long transitionDuration, + @Nullable Double zoom, @Nullable Double bearing, @Nullable Double tilt, + @Nullable OnLocationCameraTransitionListener transitionListener) { + checkActivationState(); + locationCameraController.setCameraMode( + cameraMode, lastLocation, transitionDuration, zoom, bearing, tilt, + new CameraTransitionListener(transitionListener)); + updateCompassListenerState(true); + } + + /** + * Used to reset camera animators and notify listeners when the transition finishes. + */ + private class CameraTransitionListener implements OnLocationCameraTransitionListener { + + private final OnLocationCameraTransitionListener externalListener; + + private CameraTransitionListener(OnLocationCameraTransitionListener externalListener) { + this.externalListener = externalListener; + } + + @Override + public void onLocationCameraTransitionFinished(int cameraMode) { + if (externalListener != null) { + externalListener.onLocationCameraTransitionFinished(cameraMode); + } + reset(cameraMode); + } + + @Override + public void onLocationCameraTransitionCanceled(int cameraMode) { + if (externalListener != null) { + externalListener.onLocationCameraTransitionCanceled(cameraMode); + } + reset(cameraMode); + } + + private void reset(@CameraMode.Mode int cameraMode) { + locationAnimatorCoordinator.resetAllCameraAnimations(mapboxMap.getCameraPosition(), + cameraMode == CameraMode.TRACKING_GPS_NORTH); + } + } + + /** + * Provides the current camera mode being used to track the location or compass updates. + * + * @return the current camera mode + */ + @CameraMode.Mode + public int getCameraMode() { + checkActivationState(); + return locationCameraController.getCameraMode(); + } + + /** + * Sets the render mode, which determines how the location updates will be rendered on the map. + *

+ *

    + *
  • {@link RenderMode#NORMAL}: Shows user location, bearing ignored
  • + *
  • {@link RenderMode#COMPASS}: Shows user location with bearing considered from compass
  • + *
  • {@link RenderMode#GPS}: Shows user location with bearing considered from location
  • + *
+ * + * @param renderMode one of the modes found in {@link RenderMode} + */ + public void setRenderMode(@RenderMode.Mode int renderMode) { + checkActivationState(); + locationLayerController.setRenderMode(renderMode); + updateLayerOffsets(true); + updateCompassListenerState(true); + } + + /** + * Provides the current render mode being used to show + * the location and/or compass updates on the map. + * + * @return the current render mode + */ + @RenderMode.Mode + public int getRenderMode() { + checkActivationState(); + return locationLayerController.getRenderMode(); + } + + /** + * Returns the current location options being used. + * + * @return the current {@link LocationComponentOptions} + */ + public LocationComponentOptions getLocationComponentOptions() { + checkActivationState(); + return options; + } + + /** + * Apply a new component style with a style resource. + * + * @param styleRes a XML style overriding some or all the options + */ + public void applyStyle(@NonNull Context context, @StyleRes int styleRes) { + checkActivationState(); + applyStyle(LocationComponentOptions.createFromAttributes(context, styleRes)); + } + + /** + * Apply a new component style with location component options. + * + * @param options to update the current style + */ + public void applyStyle(@NonNull final LocationComponentOptions options) { + checkActivationState(); + LocationComponent.this.options = options; + if (mapboxMap.getStyle() != null) { + locationLayerController.applyStyle(options); + locationCameraController.initializeOptions(options); + staleStateManager.setEnabled(options.enableStaleState()); + staleStateManager.setDelayTime(options.staleStateTimeout()); + locationAnimatorCoordinator.setTrackingAnimationDurationMultiplier(options.trackingAnimationDurationMultiplier()); + locationAnimatorCoordinator.setCompassAnimationEnabled(options.compassAnimationEnabled()); + locationAnimatorCoordinator.setAccuracyAnimationEnabled(options.accuracyAnimationEnabled()); + updateMapWithOptions(options); + } + } + + /** + * Zooms to the desired zoom level. + * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}. + * If you are not using any of {@link CameraMode} modes, + * use one of {@link MapboxMap#moveCamera(CameraUpdate)}, + * {@link MapboxMap#easeCamera(CameraUpdate)} or {@link MapboxMap#animateCamera(CameraUpdate)} instead. + * + * @param zoomLevel The desired zoom level. + * @param animationDuration The zoom animation duration. + * @param callback The callback with finish/cancel information + */ + public void zoomWhileTracking(double zoomLevel, long animationDuration, + @Nullable MapboxMap.CancelableCallback callback) { + checkActivationState(); + if (!isLayerReady) { + return; + } else if (getCameraMode() == CameraMode.NONE) { + Logger.e(TAG, String.format("%s%s", + "LocationComponent#zoomWhileTracking method can only be used", + " when a camera mode other than CameraMode#NONE is engaged.")); + return; + } + locationAnimatorCoordinator.feedNewZoomLevel(zoomLevel, mapboxMap.getCameraPosition(), animationDuration, callback); + } + + /** + * Zooms to the desired zoom level. + * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}. + * If you are not using any of {@link CameraMode} modes, + * use one of {@link MapboxMap#moveCamera(CameraUpdate)}, + * {@link MapboxMap#easeCamera(CameraUpdate)} or {@link MapboxMap#animateCamera(CameraUpdate)} instead. + * + * @param zoomLevel The desired zoom level. + * @param animationDuration The zoom animation duration. + */ + public void zoomWhileTracking(double zoomLevel, long animationDuration) { + checkActivationState(); + zoomWhileTracking(zoomLevel, animationDuration, null); + } + + /** + * Zooms to the desired zoom level. + * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}. + * If you are not using any of {@link CameraMode} modes, + * use one of {@link MapboxMap#moveCamera(CameraUpdate)}, + * {@link MapboxMap#easeCamera(CameraUpdate)} or {@link MapboxMap#animateCamera(CameraUpdate)} instead. + * + * @param zoomLevel The desired zoom level. + */ + public void zoomWhileTracking(double zoomLevel) { + checkActivationState(); + zoomWhileTracking(zoomLevel, DEFAULT_TRACKING_ZOOM_ANIM_DURATION, null); + } + + /** + * Cancels animation started by {@link #zoomWhileTracking(double, long, MapboxMap.CancelableCallback)}. + */ + public void cancelZoomWhileTrackingAnimation() { + checkActivationState(); + locationAnimatorCoordinator.cancelZoomAnimation(); + } + + /** + * Tilts the camera. + * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}. + * If you are not using any of {@link CameraMode} modes, + * use one of {@link MapboxMap#moveCamera(CameraUpdate)}, + * {@link MapboxMap#easeCamera(CameraUpdate)} or {@link MapboxMap#animateCamera(CameraUpdate)} instead. + * + * @param tilt The desired camera tilt. + * @param animationDuration The tilt animation duration. + * @param callback The callback with finish/cancel information + */ + public void tiltWhileTracking(double tilt, long animationDuration, + @Nullable MapboxMap.CancelableCallback callback) { + checkActivationState(); + if (!isLayerReady) { + return; + } else if (getCameraMode() == CameraMode.NONE) { + Logger.e(TAG, String.format("%s%s", + "LocationComponent#tiltWhileTracking method can only be used", + " when a camera mode other than CameraMode#NONE is engaged.")); + return; + } + locationAnimatorCoordinator.feedNewTilt(tilt, mapboxMap.getCameraPosition(), animationDuration, callback); + } + + /** + * Tilts the camera. + * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}. + * If you are not using any of {@link CameraMode} modes, + * use one of {@link MapboxMap#moveCamera(CameraUpdate)}, + * {@link MapboxMap#easeCamera(CameraUpdate)} or {@link MapboxMap#animateCamera(CameraUpdate)} instead. + * + * @param tilt The desired camera tilt. + * @param animationDuration The tilt animation duration. + */ + public void tiltWhileTracking(double tilt, long animationDuration) { + checkActivationState(); + tiltWhileTracking(tilt, animationDuration, null); + } + + /** + * Tilts the camera. + * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}. + * If you are not using any of {@link CameraMode} modes, + * use one of {@link MapboxMap#moveCamera(CameraUpdate)}, + * {@link MapboxMap#easeCamera(CameraUpdate)} or {@link MapboxMap#animateCamera(CameraUpdate)} instead. + * + * @param tilt The desired camera tilt. + */ + public void tiltWhileTracking(double tilt) { + checkActivationState(); + tiltWhileTracking(tilt, DEFAULT_TRACKING_TILT_ANIM_DURATION, null); + } + + /** + * Cancels animation started by {@link #tiltWhileTracking(double, long, MapboxMap.CancelableCallback)}. + */ + public void cancelTiltWhileTrackingAnimation() { + checkActivationState(); + locationAnimatorCoordinator.cancelTiltAnimation(); + } + + /** + * Use to either force a location update or to manually control when the user location gets + * updated. + * + * @param location where the location icon is placed on the map + */ + public void forceLocationUpdate(@Nullable Location location) { + checkActivationState(); + updateLocation(location, false); + } + + /** + * Set max FPS at which location animators can output updates. The throttling will only impact the location puck + * and camera tracking smooth animations. + *

+ * Setting this will not impact any other animations schedule with {@link MapboxMap}, gesture animations or + * {@link #zoomWhileTracking(double)}/{@link #tiltWhileTracking(double)}. + *

+ * Use this setting to limit animation rate of the location puck on higher zoom levels to decrease the stress on + * the device's CPU which can directly improve battery life, without sacrificing UX. + *

+ * Example usage: + *

+   * {@code
+   * mapboxMap.addOnCameraIdleListener(new MapboxMap.OnCameraIdleListener() {
+   *   {@literal @}Override
+   *   public void onCameraIdle() {
+   *     double zoom = mapboxMap.getCameraPosition().zoom;
+   *     int maxAnimationFps;
+   *     if (zoom < 5) {
+   *       maxAnimationFps = 3;
+   *     } else if (zoom < 10) {
+   *       maxAnimationFps = 5;
+   *     } else if (zoom < 15) {
+   *       maxAnimationFps = 7;
+   *     } else if (zoom < 18) {
+   *       maxAnimationFps = 15;
+   *     } else {
+   *       maxAnimationFps = Integer.MAX_VALUE;
+   *     }
+   *     locationComponent.setMaxAnimationFps(maxAnimationFps);
+   *   }
+   * });
+   * }
+   * 
+ *

+ * If you're looking for a way to throttle the FPS of the whole map, including other animations and gestures, see + * {@link MapView#setMaximumFps(int)}. + * + * @param maxAnimationFps max location animation FPS + */ + public void setMaxAnimationFps(int maxAnimationFps) { + checkActivationState(); + locationAnimatorCoordinator.setMaxAnimationFps(maxAnimationFps); + } + + /** + * Set the location engine to update the current user location. + *

+ * If {@code null} is passed in, all updates will have to occur through the + * {@link LocationComponent#forceLocationUpdate(Location)} method. + * + * @param locationEngine a {@link LocationEngine} this component should use to handle updates + */ + @SuppressLint("MissingPermission") + public void setLocationEngine(@Nullable LocationEngine locationEngine) { + checkActivationState(); + if (this.locationEngine != null) { + // If internal location engines being used, extra steps need to be taken to deconstruct the instance. + this.locationEngine.removeLocationUpdates(currentLocationEngineListener); + this.locationEngine = null; + } + + if (locationEngine != null) { + fastestInterval = locationEngineRequest.getFastestInterval(); + this.locationEngine = locationEngine; + if (isLayerReady && isEnabled) { + setLastLocation(); + locationEngine.requestLocationUpdates( + locationEngineRequest, currentLocationEngineListener, Looper.getMainLooper()); + } + } else { + fastestInterval = 0; + } + } + + /** + * Set the location request that's going to be used when requesting location updates. + * + * @param locationEngineRequest the location request + */ + public void setLocationEngineRequest(@NonNull LocationEngineRequest locationEngineRequest) { + checkActivationState(); + this.locationEngineRequest = locationEngineRequest; + + // reset internal LocationEngine ref to re-request location updates if needed + setLocationEngine(locationEngine); + } + + /** + * Get the location request that's going to be used when requesting location updates. + */ + @NonNull + public LocationEngineRequest getLocationEngineRequest() { + checkActivationState(); + return locationEngineRequest; + } + + /** + * Returns the current {@link LocationEngine} being used for updating the user location. + * + * @return the {@link LocationEngine} being used to update the user location + */ + @Nullable + public LocationEngine getLocationEngine() { + checkActivationState(); + return locationEngine; + } + + /** + * Sets the compass engine used to provide compass heading values. + * + * @param compassEngine to be used + */ + public void setCompassEngine(@Nullable CompassEngine compassEngine) { + checkActivationState(); + if (this.compassEngine != null) { + updateCompassListenerState(false); + } + this.compassEngine = compassEngine; + updateCompassListenerState(true); + } + + /** + * Returns the compass engine used to provide compass heading values. + * + * @return compass engine currently being used + */ + @Nullable + public CompassEngine getCompassEngine() { + checkActivationState(); + return compassEngine; + } + + /** + * Get the last know location of the location component. + * + * @return the last known location + */ + @Nullable + public Location getLastKnownLocation() { + checkActivationState(); + return lastLocation; + } + + /** + * Adds a listener that gets invoked when the user clicks the displayed location. + *

+ * If there are registered location click listeners and the location is clicked, + * only {@link OnLocationClickListener#onLocationComponentClick()} is going to be delivered, + * {@link com.mapbox.mapboxsdk.maps.MapboxMap.OnMapClickListener#onMapClick(LatLng)} is going to be consumed + * and not pushed to the listeners registered after the component's activation. + * + * @param listener The location click listener that is invoked when the + * location is clicked + */ + public void addOnLocationClickListener(@NonNull OnLocationClickListener listener) { + onLocationClickListeners.add(listener); + } + + /** + * Removes the passed listener from the current list of location click listeners. + * + * @param listener to be removed + */ + public void removeOnLocationClickListener(@NonNull OnLocationClickListener listener) { + onLocationClickListeners.remove(listener); + } + + /** + * Adds a listener that gets invoked when the user long clicks the displayed location. + *

+ * If there are registered location long click listeners and the location is long clicked, + * only {@link OnLocationLongClickListener#onLocationComponentLongClick()} is going to be delivered, + * {@link com.mapbox.mapboxsdk.maps.MapboxMap.OnMapLongClickListener#onMapLongClick(LatLng)} is going to be consumed + * and not pushed to the listeners registered after the component's activation. + * + * @param listener The location click listener that is invoked when the + * location is clicked + */ + public void addOnLocationLongClickListener(@NonNull OnLocationLongClickListener listener) { + onLocationLongClickListeners.add(listener); + } + + /** + * Removes the passed listener from the current list of location long click listeners. + * + * @param listener to be removed + */ + public void removeOnLocationLongClickListener(@NonNull OnLocationLongClickListener listener) { + onLocationLongClickListeners.remove(listener); + } + + /** + * Adds a listener that gets invoked when camera tracking state changes. + * + * @param listener Listener that gets invoked when camera tracking state changes. + */ + public void addOnCameraTrackingChangedListener(@NonNull OnCameraTrackingChangedListener listener) { + onCameraTrackingChangedListeners.add(listener); + } + + /** + * Removes a listener that gets invoked when camera tracking state changes. + * + * @param listener Listener that gets invoked when camera tracking state changes. + */ + public void removeOnCameraTrackingChangedListener(@NonNull OnCameraTrackingChangedListener listener) { + onCameraTrackingChangedListeners.remove(listener); + } + + /** + * Adds a listener that gets invoked when render mode changes. + * + * @param listener Listener that gets invoked when render mode changes. + */ + public void addOnRenderModeChangedListener(@NonNull OnRenderModeChangedListener listener) { + onRenderModeChangedListeners.add(listener); + } + + /** + * Removes a listener that gets invoked when render mode changes. + * + * @param listener Listener that gets invoked when render mode changes. + */ + public void removeRenderModeChangedListener(@NonNull OnRenderModeChangedListener listener) { + onRenderModeChangedListeners.remove(listener); + } + + /** + * Adds the passed listener that gets invoked when user updates have stopped long enough for the last update + * to be considered stale. + *

+ * This timeout is set by {@link LocationComponentOptions#staleStateTimeout()}. + * + * @param listener invoked when last update is considered stale + */ + public void addOnLocationStaleListener(@NonNull OnLocationStaleListener listener) { + onLocationStaleListeners.add(listener); + } + + /** + * Removes the passed listener from the current list of stale listeners. + * + * @param listener to be removed from the list + */ + public void removeOnLocationStaleListener(@NonNull OnLocationStaleListener listener) { + onLocationStaleListeners.remove(listener); + } + + /** + * Internal use. + */ + public void onStart() { + isComponentStarted = true; + onLocationLayerStart(); + } + + /** + * Internal use. + */ + public void onStop() { + onLocationLayerStop(); + isComponentStarted = false; + } + + /** + * Internal use. + */ + public void onDestroy() { + } + + /** + * Internal use. + */ + public void onStartLoadingMap() { + onLocationLayerStop(); + } + + /** + * Internal use. + */ + public void onFinishLoadingStyle() { + if (isComponentInitialized) { + style = mapboxMap.getStyle(); + locationLayerController.initializeComponents(style, options); + locationCameraController.initializeOptions(options); + onLocationLayerStart(); + } + } + + @SuppressLint("MissingPermission") + private void onLocationLayerStart() { + if (!isComponentInitialized || !isComponentStarted || mapboxMap.getStyle() == null) { + return; + } + + if (!isLayerReady) { + isLayerReady = true; + mapboxMap.addOnCameraMoveListener(onCameraMoveListener); + mapboxMap.addOnCameraIdleListener(onCameraIdleListener); + if (options.enableStaleState()) { + staleStateManager.onStart(); + } + } + + if (isEnabled) { + if (locationEngine != null) { + try { + locationEngine.requestLocationUpdates( + locationEngineRequest, currentLocationEngineListener, Looper.getMainLooper()); + } catch (SecurityException se) { + Logger.e(TAG, "Unable to request location updates", se); + } + } + setCameraMode(locationCameraController.getCameraMode()); + setLastLocation(); + updateCompassListenerState(true); + setLastCompassHeading(); + } + } + + private void onLocationLayerStop() { + if (!isComponentInitialized || !isLayerReady || !isComponentStarted) { + return; + } + + isLayerReady = false; + staleStateManager.onStop(); + if (compassEngine != null) { + updateCompassListenerState(false); + } + locationAnimatorCoordinator.cancelAllAnimations(); + if (locationEngine != null) { + locationEngine.removeLocationUpdates(currentLocationEngineListener); + } + mapboxMap.removeOnCameraMoveListener(onCameraMoveListener); + mapboxMap.removeOnCameraIdleListener(onCameraIdleListener); + } + + private void initialize(@NonNull final Context context, @NonNull Style style, + @NonNull final LocationComponentOptions options) { + if (isComponentInitialized) { + return; + } + isComponentInitialized = true; + + if (!style.isFullyLoaded()) { + throw new IllegalStateException("Style is invalid, provide the most recently loaded one."); + } + + this.style = style; + this.options = options; + + mapboxMap.addOnMapClickListener(onMapClickListener); + mapboxMap.addOnMapLongClickListener(onMapLongClickListener); + + LayerSourceProvider sourceProvider = new LayerSourceProvider(); + LayerFeatureProvider featureProvider = new LayerFeatureProvider(); + LayerBitmapProvider bitmapProvider = new LayerBitmapProvider(context); + locationLayerController = new LocationLayerController(mapboxMap, style, sourceProvider, featureProvider, + bitmapProvider, options, renderModeChangedListener); + locationCameraController = new LocationCameraController( + context, mapboxMap, transform, cameraTrackingChangedListener, options, onCameraMoveInvalidateListener); + + locationAnimatorCoordinator = new LocationAnimatorCoordinator( + mapboxMap.getProjection(), + MapboxAnimatorSetProvider.getInstance(), + MapboxAnimatorProvider.getInstance() + ); + locationAnimatorCoordinator.setTrackingAnimationDurationMultiplier(options + .trackingAnimationDurationMultiplier()); + + WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + if (windowManager != null && sensorManager != null) { + compassEngine = new LocationComponentCompassEngine(windowManager, sensorManager); + } + staleStateManager = new StaleStateManager(onLocationStaleListener, options); + + updateMapWithOptions(options); + + setRenderMode(RenderMode.NORMAL); + setCameraMode(CameraMode.NONE); + + onLocationLayerStart(); + } + + private void initializeLocationEngine(@NonNull Context context) { + if (this.locationEngine != null) { + this.locationEngine.removeLocationUpdates(currentLocationEngineListener); + } + setLocationEngine(internalLocationEngineProvider.getBestLocationEngine(context, false)); + } + + private void updateCompassListenerState(boolean canListen) { + if (compassEngine != null) { + if (!canListen) { + // We shouldn't listen, simply unregistering + removeCompassListener(compassEngine); + return; + } + + if (!isComponentInitialized || !isComponentStarted || !isEnabled) { + return; + } + + if (locationCameraController.isConsumingCompass() || locationLayerController.isConsumingCompass()) { + // If we have a consumer, and not yet listening, then start listening + if (!isListeningToCompass) { + isListeningToCompass = true; + compassEngine.addCompassListener(compassListener); + } + } else { + // If we have no consumers, stop listening + removeCompassListener(compassEngine); + } + } + } + + private void removeCompassListener(@NonNull CompassEngine engine) { + if (isListeningToCompass) { + isListeningToCompass = false; + engine.removeCompassListener(compassListener); + } + } + + private void enableLocationComponent() { + isEnabled = true; + onLocationLayerStart(); + } + + private void disableLocationComponent() { + isEnabled = false; + locationLayerController.hide(); + onLocationLayerStop(); + } + + private void updateMapWithOptions(@NonNull LocationComponentOptions options) { + int[] padding = options.padding(); + if (padding != null) { + mapboxMap.setPadding( + padding[0], padding[1], padding[2], padding[3] + ); + } + } + + /** + * Updates the user location icon. + * + * @param location the latest user location + */ + private void updateLocation(@Nullable final Location location, boolean fromLastLocation) { + if (location == null) { + return; + } else if (!isLayerReady) { + lastLocation = location; + return; + } else { + long currentTime = SystemClock.elapsedRealtime(); + if (currentTime - lastUpdateTime < fastestInterval) { + return; + } else { + lastUpdateTime = currentTime; + } + } + + showLocationLayerIfHidden(); + + if (!fromLastLocation) { + staleStateManager.updateLatestLocationTime(); + } + CameraPosition currentCameraPosition = mapboxMap.getCameraPosition(); + boolean isGpsNorth = getCameraMode() == CameraMode.TRACKING_GPS_NORTH; + locationAnimatorCoordinator.feedNewLocation(location, currentCameraPosition, isGpsNorth); + updateAccuracyRadius(location, false); + lastLocation = location; + } + + private void showLocationLayerIfHidden() { + boolean isLocationLayerHidden = locationLayerController.isHidden(); + if (isEnabled && isComponentStarted && isLocationLayerHidden) { + locationLayerController.show(); + } + } + + private void updateCompassHeading(float heading) { + locationAnimatorCoordinator.feedNewCompassBearing(heading, mapboxMap.getCameraPosition()); + } + + /** + * If the locationEngine contains a last location value, we use it for the initial location layer + * position. + */ + @SuppressLint("MissingPermission") + private void setLastLocation() { + if (locationEngine != null) { + locationEngine.getLastLocation(lastLocationEngineListener); + } else { + updateLocation(getLastKnownLocation(), true); + } + } + + private void setLastCompassHeading() { + updateCompassHeading(compassEngine != null ? compassEngine.getLastHeading() : 0); + } + + @SuppressLint("MissingPermission") + private void updateLayerOffsets(boolean forceUpdate) { + CameraPosition position = mapboxMap.getCameraPosition(); + if (lastCameraPosition == null || forceUpdate) { + lastCameraPosition = position; + locationLayerController.updateForegroundBearing((float) position.bearing); + locationLayerController.updateForegroundOffset(position.tilt); + updateAccuracyRadius(getLastKnownLocation(), true); + return; + } + + if (position.bearing != lastCameraPosition.bearing) { + locationLayerController.updateForegroundBearing((float) position.bearing); + } + if (position.tilt != lastCameraPosition.tilt) { + locationLayerController.updateForegroundOffset(position.tilt); + } + if (position.zoom != lastCameraPosition.zoom) { + updateAccuracyRadius(getLastKnownLocation(), true); + } + lastCameraPosition = position; + } + + private void updateAccuracyRadius(Location location, boolean noAnimation) { + locationAnimatorCoordinator.feedNewAccuracyRadius(Utils.calculateZoomLevelRadius(mapboxMap, location), noAnimation); + } + + private void updateAnimatorListenerHolders() { + Set animationsValueChangeListeners = new HashSet<>(); + animationsValueChangeListeners.addAll(locationLayerController.getAnimationListeners()); + animationsValueChangeListeners.addAll(locationCameraController.getAnimationListeners()); + locationAnimatorCoordinator.updateAnimatorListenerHolders(animationsValueChangeListeners); + locationAnimatorCoordinator.resetAllCameraAnimations(mapboxMap.getCameraPosition(), + locationCameraController.getCameraMode() == CameraMode.TRACKING_GPS_NORTH); + locationAnimatorCoordinator.resetAllLayerAnimations(); + } + + @NonNull + private OnCameraMoveListener onCameraMoveListener = new OnCameraMoveListener() { + @Override + public void onCameraMove() { + updateLayerOffsets(false); + } + }; + + @NonNull + private OnCameraIdleListener onCameraIdleListener = new OnCameraIdleListener() { + @Override + public void onCameraIdle() { + updateLayerOffsets(false); + } + }; + + @NonNull + private OnMapClickListener onMapClickListener = new OnMapClickListener() { + @Override + public boolean onMapClick(@NonNull LatLng point) { + if (!onLocationClickListeners.isEmpty() && locationLayerController.onMapClick(point)) { + for (OnLocationClickListener listener : onLocationClickListeners) { + listener.onLocationComponentClick(); + } + return true; + } + return false; + } + }; + + @NonNull + private MapboxMap.OnMapLongClickListener onMapLongClickListener = new MapboxMap.OnMapLongClickListener() { + @Override + public boolean onMapLongClick(@NonNull LatLng point) { + if (!onLocationLongClickListeners.isEmpty() && locationLayerController.onMapClick(point)) { + for (OnLocationLongClickListener listener : onLocationLongClickListeners) { + listener.onLocationComponentLongClick(); + } + return true; + } + return false; + } + }; + + @NonNull + private OnLocationStaleListener onLocationStaleListener = new OnLocationStaleListener() { + @Override + public void onStaleStateChange(boolean isStale) { + locationLayerController.setLocationsStale(isStale); + + for (OnLocationStaleListener listener : onLocationStaleListeners) { + listener.onStaleStateChange(isStale); + } + } + }; + + @NonNull + private OnCameraMoveInvalidateListener onCameraMoveInvalidateListener = new OnCameraMoveInvalidateListener() { + @Override + public void onInvalidateCameraMove() { + onCameraMoveListener.onCameraMove(); + } + }; + + @NonNull + private CompassListener compassListener = new CompassListener() { + @Override + public void onCompassChanged(float userHeading) { + updateCompassHeading(userHeading); + } + + @Override + public void onCompassAccuracyChange(int compassStatus) { + // Currently don't handle this inside SDK + } + }; + + @VisibleForTesting + static final class CurrentLocationEngineCallback implements LocationEngineCallback { + private final WeakReference componentWeakReference; + + CurrentLocationEngineCallback(LocationComponent component) { + this.componentWeakReference = new WeakReference<>(component); + } + + @Override + public void onSuccess(LocationEngineResult result) { + LocationComponent component = componentWeakReference.get(); + if (component != null) { + component.updateLocation(result.getLastLocation(), false); + } + } + + @Override + public void onFailure(@NonNull Exception exception) { + Logger.e(TAG, "Failed to obtain location update", exception); + } + } + + @VisibleForTesting + static final class LastLocationEngineCallback implements LocationEngineCallback { + private final WeakReference componentWeakReference; + + LastLocationEngineCallback(LocationComponent component) { + this.componentWeakReference = new WeakReference<>(component); + } + + @Override + public void onSuccess(LocationEngineResult result) { + LocationComponent component = componentWeakReference.get(); + if (component != null) { + component.updateLocation(result.getLastLocation(), true); + } + } + + @Override + public void onFailure(@NonNull Exception exception) { + Logger.e(TAG, "Failed to obtain last location update", exception); + } + } + + @NonNull + @VisibleForTesting + OnCameraTrackingChangedListener cameraTrackingChangedListener = new OnCameraTrackingChangedListener() { + @Override + public void onCameraTrackingDismissed() { + for (OnCameraTrackingChangedListener listener : onCameraTrackingChangedListeners) { + listener.onCameraTrackingDismissed(); + } + } + + @Override + public void onCameraTrackingChanged(int currentMode) { + locationAnimatorCoordinator.cancelZoomAnimation(); + locationAnimatorCoordinator.cancelTiltAnimation(); + updateAnimatorListenerHolders(); + for (OnCameraTrackingChangedListener listener : onCameraTrackingChangedListeners) { + listener.onCameraTrackingChanged(currentMode); + } + } + }; + + @NonNull + @VisibleForTesting + OnRenderModeChangedListener renderModeChangedListener = new OnRenderModeChangedListener() { + @Override + public void onRenderModeChanged(int currentMode) { + updateAnimatorListenerHolders(); + for (OnRenderModeChangedListener listener : onRenderModeChangedListeners) { + listener.onRenderModeChanged(currentMode); + } + } + }; + + @NonNull + private final MapboxMap.OnDeveloperAnimationListener developerAnimationListener = + new MapboxMap.OnDeveloperAnimationListener() { + @Override + public void onDeveloperAnimationStarted() { + if (isComponentInitialized && isEnabled) { + setCameraMode(CameraMode.NONE); + } + } + }; + + static class InternalLocationEngineProvider { + LocationEngine getBestLocationEngine(@NonNull Context context, boolean background) { + return LocationEngineProvider.getBestLocationEngine(context, background); + } + } + + private void checkActivationState() { + if (!isComponentInitialized) { + throw new LocationComponentNotInitializedException(); + } + } + + /** + * Returns whether the location component is activated. + * + * @return true if the component is activated, false otherwise + */ + public boolean isLocationComponentActivated() { + return isComponentInitialized; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptions.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptions.java new file mode 100644 index 000000000..04987621e --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptions.java @@ -0,0 +1,244 @@ +package com.mapbox.mapboxsdk.location; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.mapbox.android.core.location.LocationEngine; +import com.mapbox.android.core.location.LocationEngineRequest; +import com.mapbox.mapboxsdk.maps.Style; + +/** + * A class which holds the various options for activating the Maps SDK's {@link LocationComponent} to eventually + * show the device location on the map. + */ +public class LocationComponentActivationOptions { + + private final Context context; + private final Style style; + private final LocationEngine locationEngine; + private final LocationEngineRequest locationEngineRequest; + private final LocationComponentOptions locationComponentOptions; + private final int styleRes; + private final boolean useDefaultLocationEngine; + + private LocationComponentActivationOptions(@NonNull Context context, @NonNull Style style, + @Nullable LocationEngine locationEngine, + @Nullable LocationEngineRequest locationEngineRequest, + @Nullable LocationComponentOptions locationComponentOptions, + int styleRes, + boolean useDefaultLocationEngine) { + this.context = context; + this.style = style; + this.locationEngine = locationEngine; + this.locationEngineRequest = locationEngineRequest; + this.locationComponentOptions = locationComponentOptions; + this.styleRes = styleRes; + this.useDefaultLocationEngine = useDefaultLocationEngine; + } + + /** + * Convenience method to retrieve a {@link LocationComponentActivationOptions} object which is ready to build with + * + * @return a builder object + */ + @NonNull + public static Builder builder(@NonNull Context context, @NonNull Style fullyLoadedMapStyle) { + return new LocationComponentActivationOptions.Builder(context, fullyLoadedMapStyle); + } + + /** + * The application's current context + * + * @return the application's current context + */ + @NonNull + public Context context() { + return context; + } + + /** + * The proxy object for current map style. More info at {@link Style} + * + * @return the map's fully loaded Style object + */ + @NonNull + public Style style() { + return style; + } + + /** + * The {@link LocationEngine} which the {@link LocationComponent} should use + * + * @return the engine + */ + @Nullable + public LocationEngine locationEngine() { + return locationEngine; + } + + /** + * The location request which the {@link LocationComponent} should use + * + * @return the LocationEngineRequest object + */ + @Nullable + public LocationEngineRequest locationEngineRequest() { + return locationEngineRequest; + } + + /** + * A built {@link LocationComponentOptions} object, which holds the various {@link LocationComponent} styling options + * + * @return the options for styling the actual LocationComponent + */ + @Nullable + public LocationComponentOptions locationComponentOptions() { + return locationComponentOptions; + } + + /** + * The LocationComponent style resource (e.g. R.style.style_name) + * + * @return the style resource. + */ + public int styleRes() { + return styleRes; + } + + /** + * True if you want to initialize and use the built-in location engine or false if there should be no + * location engine initialized + * + * @return whether the default LocationEngine is used + */ + public boolean useDefaultLocationEngine() { + return useDefaultLocationEngine; + } + + /** + * Builder class for constructing a new instance of {@link LocationComponentActivationOptions}. + */ + public static class Builder { + private final Context context; + private final Style style; + private LocationEngine locationEngine; + private LocationEngineRequest locationEngineRequest; + private LocationComponentOptions locationComponentOptions; + private int styleRes; + + /** + * Set to true as the default in case a true/false value isn't declared via the builder's + * {@link LocationComponentActivationOptions#useDefaultLocationEngine()} method. + *

+ * Please be aware that this activation boolean is ignored when a non-null + * {@link LocationEngine} is set via the builder's `locationEngine()` method. + */ + private boolean useDefaultLocationEngine = true; + + /** + * Constructor for the {@link LocationComponentActivationOptions} builder class. + * While other activation options are optional, the activation process always requires + * context and a fully-loaded map {@link Style} object, which is why the two are in this + * constructor. + */ + public Builder(@NonNull Context context, @NonNull Style style) { + this.context = context; + this.style = style; + } + + /** + * Deliver your own {@link LocationEngine} to the LocationComponent. + *

+ * The true/false + * {@link LocationComponentActivationOptions.Builder#useDefaultLocationEngine()} + * activation option is ignored when a non-null {@link LocationEngine} is set via + * this `locationEngine()` method. + * + * @param locationEngine a {@link LocationEngine} object + * @return the {@link Builder} object being constructed + */ + @NonNull + public Builder locationEngine(@Nullable LocationEngine locationEngine) { + this.locationEngine = locationEngine; + return this; + } + + /** + * @param locationEngineRequest the location request which the + * {@link LocationComponent} should use + * @return the {@link Builder} object being constructed + */ + public Builder locationEngineRequest(LocationEngineRequest locationEngineRequest) { + this.locationEngineRequest = locationEngineRequest; + return this; + } + + /** + * @param locationComponentOptions a built {@link LocationComponentOptions} object, + * which holds the various {@link LocationComponent} + * styling options + * @return the {@link Builder} object being constructed + */ + public Builder locationComponentOptions(LocationComponentOptions locationComponentOptions) { + this.locationComponentOptions = locationComponentOptions; + return this; + } + + /** + * @param styleRes the LocationComponent style resource (e.g. R.style.style_name) + * @return the {@link Builder} object being constructed + */ + public Builder styleRes(int styleRes) { + this.styleRes = styleRes; + return this; + } + + /** + * @param useDefaultLocationEngine true if you want to initialize and use the + * built-in location engine or false if there + * should be no location engine initialized + * This is ignored when null is set as the parameter + * for {@link LocationComponentActivationOptions#builder + * (Context, Style)#locationEngine()}. + * @return the {@link Builder} object being constructed + */ + public Builder useDefaultLocationEngine(boolean useDefaultLocationEngine) { + this.useDefaultLocationEngine = useDefaultLocationEngine; + return this; + } + + /** + * Method which actually builds the {@link LocationComponentActivationOptions} object while + * taking the various options into account. + * + * @return a built {@link LocationComponentActivationOptions} object + */ + public LocationComponentActivationOptions build() { + if (styleRes != 0 && locationComponentOptions != null) { + throw new IllegalArgumentException( + "You've provided both a style resource and a LocationComponentOptions object to the " + + "LocationComponentActivationOptions builder. You can't use both and " + + "you must choose one of the two to style the LocationComponent."); + } + if (context == null) { + throw new NullPointerException( + "Context in LocationComponentActivationOptions is null."); + } + if (style == null) { + throw new NullPointerException( + "Style in LocationComponentActivationOptions is null. Make sure the " + + "Style object isn't null. Wait for the map to fully load before passing " + + "the Style object to LocationComponentActivationOptions."); + } + if (!style.isFullyLoaded()) { + throw new IllegalArgumentException( + "Style in LocationComponentActivationOptions isn't fully loaded. Wait for the " + + "map to fully load before passing the Style object to " + + "LocationComponentActivationOptions."); + } + return new LocationComponentActivationOptions(context, style, locationEngine, + locationEngineRequest, locationComponentOptions, styleRes, useDefaultLocationEngine); + } + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentCompassEngine.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentCompassEngine.java new file mode 100644 index 000000000..ce3823303 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentCompassEngine.java @@ -0,0 +1,268 @@ +package com.mapbox.mapboxsdk.location; + +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.Surface; +import android.view.WindowManager; + +import com.mapbox.mapboxsdk.log.Logger; + +import java.util.ArrayList; +import java.util.List; + +/** + * This manager class handles compass events such as starting the tracking of device bearing, or + * when a new compass update occurs. + */ +class LocationComponentCompassEngine implements CompassEngine, SensorEventListener { + + private static final String TAG = "Mbgl-LocationComponentCompassEngine"; + + // The rate sensor events will be delivered at. As the Android documentation states, this is only + // a hint to the system and the events might actually be received faster or slower then this + // specified rate. Since the minimum Android API levels about 9, we are able to set this value + // ourselves rather than using one of the provided constants which deliver updates too quickly for + // our use case. The default is set to 100ms + static final int SENSOR_DELAY_MICROS = 100 * 1000; + // Filtering coefficient 0 < ALPHA < 1 + private static final float ALPHA = 0.45f; + + @NonNull + private final WindowManager windowManager; + @NonNull + private final SensorManager sensorManager; + private final List compassListeners = new ArrayList<>(); + + // Not all devices have a compassSensor + @Nullable + private Sensor compassSensor; + @Nullable + private Sensor gravitySensor; + @Nullable + private Sensor magneticFieldSensor; + + @NonNull + private float[] truncatedRotationVectorValue = new float[4]; + @NonNull + private float[] rotationMatrix = new float[9]; + private float[] rotationVectorValue; + private float lastHeading; + private int lastAccuracySensorStatus; + + private long compassUpdateNextTimestamp; + @Nullable + private float[] gravityValues = new float[3]; + @Nullable + private float[] magneticValues = new float[3]; + + /** + * Construct a new instance of the this class. A internal compass listeners needed to separate it + * from the cleared list of public listeners. + */ + LocationComponentCompassEngine(@NonNull WindowManager windowManager, @NonNull SensorManager sensorManager) { + this.windowManager = windowManager; + this.sensorManager = sensorManager; + compassSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); + if (compassSensor == null) { + if (isGyroscopeAvailable()) { + Logger.d(TAG, "Rotation vector sensor not supported on device, " + + "falling back to orientation."); + compassSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); + } else { + Logger.d(TAG, "Rotation vector sensor not supported on device, " + + "falling back to accelerometer and magnetic field."); + gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + magneticFieldSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + } + } + } + + @Override + public void addCompassListener(@NonNull CompassListener compassListener) { + if (compassListeners.isEmpty()) { + registerSensorListeners(); + } + compassListeners.add(compassListener); + } + + @Override + public void removeCompassListener(@NonNull CompassListener compassListener) { + compassListeners.remove(compassListener); + if (compassListeners.isEmpty()) { + unregisterSensorListeners(); + } + } + + @Override + public int getLastAccuracySensorStatus() { + return lastAccuracySensorStatus; + } + + @Override + public float getLastHeading() { + return lastHeading; + } + + @Override + public void onSensorChanged(@NonNull SensorEvent event) { + // check when the last time the compass was updated, return if too soon. + long currentTime = SystemClock.elapsedRealtime(); + if (currentTime < compassUpdateNextTimestamp) { + return; + } + if (lastAccuracySensorStatus == SensorManager.SENSOR_STATUS_UNRELIABLE) { + Logger.d(TAG, "Compass sensor is unreliable, device calibration is needed."); + return; + } + if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) { + rotationVectorValue = getRotationVectorFromSensorEvent(event); + updateOrientation(); + } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) { + notifyCompassChangeListeners((event.values[0] + 360) % 360); + } else if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { + gravityValues = lowPassFilter(getRotationVectorFromSensorEvent(event), gravityValues); + updateOrientation(); + } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) { + magneticValues = lowPassFilter(getRotationVectorFromSensorEvent(event), magneticValues); + updateOrientation(); + } + + // Update the compassUpdateNextTimestamp + compassUpdateNextTimestamp = currentTime + LocationComponentConstants.COMPASS_UPDATE_RATE_MS; + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + if (lastAccuracySensorStatus != accuracy) { + for (CompassListener compassListener : compassListeners) { + compassListener.onCompassAccuracyChange(accuracy); + } + lastAccuracySensorStatus = accuracy; + } + } + + private boolean isGyroscopeAvailable() { + return sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null; + } + + @SuppressWarnings("SuspiciousNameCombination") + private void updateOrientation() { + if (rotationVectorValue != null) { + SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVectorValue); + } else { + // Get rotation matrix given the gravity and geomagnetic matrices + SensorManager.getRotationMatrix(rotationMatrix, null, gravityValues, magneticValues); + } + + final int worldAxisForDeviceAxisX; + final int worldAxisForDeviceAxisY; + + // Remap the axes as if the device screen was the instrument panel, + // and adjust the rotation matrix for the device orientation. + switch (windowManager.getDefaultDisplay().getRotation()) { + case Surface.ROTATION_90: + worldAxisForDeviceAxisX = SensorManager.AXIS_Z; + worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X; + break; + case Surface.ROTATION_180: + worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X; + worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Z; + break; + case Surface.ROTATION_270: + worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Z; + worldAxisForDeviceAxisY = SensorManager.AXIS_X; + break; + case Surface.ROTATION_0: + default: + worldAxisForDeviceAxisX = SensorManager.AXIS_X; + worldAxisForDeviceAxisY = SensorManager.AXIS_Z; + break; + } + + float[] adjustedRotationMatrix = new float[9]; + SensorManager.remapCoordinateSystem(rotationMatrix, worldAxisForDeviceAxisX, + worldAxisForDeviceAxisY, adjustedRotationMatrix); + + // Transform rotation matrix into azimuth/pitch/roll + float[] orientation = new float[3]; + SensorManager.getOrientation(adjustedRotationMatrix, orientation); + + // The x-axis is all we care about here. + notifyCompassChangeListeners((float) Math.toDegrees(orientation[0])); + } + + private void notifyCompassChangeListeners(float heading) { + for (CompassListener compassListener : compassListeners) { + compassListener.onCompassChanged(heading); + } + lastHeading = heading; + } + + private void registerSensorListeners() { + if (isCompassSensorAvailable()) { + // Does nothing if the sensors already registered. + sensorManager.registerListener(this, compassSensor, SENSOR_DELAY_MICROS); + } else { + sensorManager.registerListener(this, gravitySensor, SENSOR_DELAY_MICROS); + sensorManager.registerListener(this, magneticFieldSensor, SENSOR_DELAY_MICROS); + } + } + + private void unregisterSensorListeners() { + if (isCompassSensorAvailable()) { + sensorManager.unregisterListener(this, compassSensor); + } else { + sensorManager.unregisterListener(this, gravitySensor); + sensorManager.unregisterListener(this, magneticFieldSensor); + } + } + + private boolean isCompassSensorAvailable() { + return compassSensor != null; + } + + /** + * Helper function, that filters newValues, considering previous values + * + * @param newValues array of float, that contains new data + * @param smoothedValues array of float, that contains previous state + * @return float filtered array of float + */ + @NonNull + private float[] lowPassFilter(@NonNull float[] newValues, @Nullable float[] smoothedValues) { + if (smoothedValues == null) { + return newValues; + } + for (int i = 0; i < newValues.length; i++) { + smoothedValues[i] = smoothedValues[i] + ALPHA * (newValues[i] - smoothedValues[i]); + } + return smoothedValues; + } + + /** + * Pulls out the rotation vector from a SensorEvent, with a maximum length + * vector of four elements to avoid potential compatibility issues. + * + * @param event the sensor event + * @return the events rotation vector, potentially truncated + */ + @NonNull + private float[] getRotationVectorFromSensorEvent(@NonNull SensorEvent event) { + if (event.values.length > 4) { + // On some Samsung devices SensorManager.getRotationMatrixFromVector + // appears to throw an exception if rotation vector has length > 4. + // For the purposes of this class the first 4 values of the + // rotation vector are sufficient (see crbug.com/335298 for details). + // Only affects Android 4.3 + System.arraycopy(event.values, 0, truncatedRotationVectorValue, 0, 4); + return truncatedRotationVectorValue; + } else { + return event.values; + } + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java new file mode 100644 index 000000000..f2158584c --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java @@ -0,0 +1,94 @@ +package com.mapbox.mapboxsdk.location; + +/** + * Contains all the constants being used for the {@link LocationComponent}. + */ +public final class LocationComponentConstants { + + // Controls the compass update rate in milliseconds + static final long COMPASS_UPDATE_RATE_MS = 500; + + // Sets the transition animation duration when switching camera modes. + static final long TRANSITION_ANIMATION_DURATION_MS = 750; + + // Sets the max allowed time for the location icon animation from one LatLng to another. + static final long MAX_ANIMATION_DURATION_MS = 2000; + + // Sets the duration of change of accuracy radius when a different value is provided. + static final long ACCURACY_RADIUS_ANIMATION_DURATION = 250; + + // Default animation duration for zooming while tracking. + static final long DEFAULT_TRACKING_ZOOM_ANIM_DURATION = 750; + + // Default animation duration for tilting while tracking. + static final long DEFAULT_TRACKING_TILT_ANIM_DURATION = 1250; + + // Threshold value to perform immediate camera/layer position update. + static final double INSTANT_LOCATION_TRANSITION_THRESHOLD = 50_000; + + // Default interval between location updates + static final long DEFAULT_INTERVAL_MILLIS = 1000; + + // Default fastest acceptable interval between location updates + static final long DEFAULT_FASTEST_INTERVAL_MILLIS = 1000; + + // Sources + + /** + * Source ID of the location's GeoJsonSource. + */ + public static final String LOCATION_SOURCE = "mapbox-location-source"; + + static final String PROPERTY_GPS_BEARING = "mapbox-property-gps-bearing"; + static final String PROPERTY_COMPASS_BEARING = "mapbox-property-compass-bearing"; + static final String PROPERTY_LOCATION_STALE = "mapbox-property-location-stale"; + static final String PROPERTY_ACCURACY_RADIUS = "mapbox-property-accuracy-radius"; + static final String PROPERTY_ACCURACY_COLOR = "mapbox-property-accuracy-color"; + static final String PROPERTY_ACCURACY_ALPHA = "mapbox-property-accuracy-alpha"; + static final String PROPERTY_FOREGROUND_ICON_OFFSET = "mapbox-property-foreground-icon-offset"; + static final String PROPERTY_SHADOW_ICON_OFFSET = "mapbox-property-shadow-icon-offset"; + static final String PROPERTY_FOREGROUND_ICON = "mapbox-property-foreground-icon"; + static final String PROPERTY_BACKGROUND_ICON = "mapbox-property-background-icon"; + static final String PROPERTY_FOREGROUND_STALE_ICON = "mapbox-property-foreground-stale-icon"; + static final String PROPERTY_BACKGROUND_STALE_ICON = "mapbox-property-background-stale-icon"; + static final String PROPERTY_BEARING_ICON = "mapbox-property-shadow-icon"; + + // Layers + + /** + * Layer ID of the location shadow. + */ + public static final String SHADOW_LAYER = "mapbox-location-shadow-layer"; + + /** + * Layer ID of the location foreground icon. + */ + public static final String FOREGROUND_LAYER = "mapbox-location-foreground-layer"; + + /** + * Layer ID of the location background icon. + */ + public static final String BACKGROUND_LAYER = "mapbox-location-background-layer"; + + /** + * Layer ID of the location accuracy. + */ + public static final String ACCURACY_LAYER = "mapbox-location-accuracy-layer"; + + /** + * Layer ID of the location bearing icon. + */ + public static final String BEARING_LAYER = "mapbox-location-bearing-layer"; + + // Icons + static final String FOREGROUND_ICON = "mapbox-location-icon"; + static final String BACKGROUND_ICON = "mapbox-location-stroke-icon"; + static final String FOREGROUND_STALE_ICON = "mapbox-location-stale-icon"; + static final String BACKGROUND_STALE_ICON = "mapbox-location-background-stale-icon"; + static final String SHADOW_ICON = "mapbox-location-shadow-icon"; + static final String BEARING_ICON = "mapbox-location-bearing-icon"; + + private LocationComponentConstants() { + // Class should not be initialized + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentNotInitializedException.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentNotInitializedException.java new file mode 100644 index 000000000..82a73b973 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentNotInitializedException.java @@ -0,0 +1,8 @@ +package com.mapbox.mapboxsdk.location; + +final class LocationComponentNotInitializedException extends RuntimeException { + LocationComponentNotInitializedException() { + super("The LocationComponent has to be activated with one of the LocationComponent#activateLocationComponent" + + " overloads before any other methods are invoked."); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java new file mode 100644 index 000000000..48c89622c --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java @@ -0,0 +1,1778 @@ +package com.mapbox.mapboxsdk.location; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.ColorInt; +import android.support.annotation.Dimension; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StyleRes; + +import com.mapbox.android.gestures.AndroidGesturesManager; +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.style.layers.Layer; + +import java.util.Arrays; + +/** + * This class exposes options for the Location Component. The options can be set by defining a + * style in your apps style.xml file and passing in directly into the {@link LocationComponent} + * class. Alternatively, if properties need to be changed at runtime depending on a specific state, + * you can build an instance of this class, setting the values you desire, and then passing it into + * either the {@link LocationComponent} activation method (if it isn't initialized yet) or + * {@link LocationComponent#applyStyle(LocationComponentOptions)}. + *

+ * When the {@link #createFromAttributes(Context, int)} methods called, any attributes not found + * inside the style will revert back to using their default set values. Likewise, when building a + * new {@link LocationComponentOptions} class using the builder, any options neglecting to be set will + * reset to their default values. + *

+ * If you would like to keep your custom style changes while modifying a single attribute, you can + * get the currently used options object using {@link LocationComponent#getLocationComponentOptions()} + * and it's {@code toBuilder} method to modify a single entry while also maintaining the other + * settings. Once your modifications have been made, you'll need to pass it back into the location + * component using {@link LocationComponent#applyStyle(LocationComponentOptions)}. + */ +public class LocationComponentOptions implements Parcelable { + + /** + * Default accuracy alpha + */ + private static final float ACCURACY_ALPHA_DEFAULT = 0.15f; + + /** + * Default icon scale factor when the map is zoomed out + */ + private static final float MIN_ZOOM_ICON_SCALE_DEFAULT = 0.6f; + + /** + * Default icon scale factor when the map is zoomed in + */ + private static final float MAX_ZOOM_ICON_SCALE_DEFAULT = 1f; + + /** + * Default map padding + */ + private static final int[] PADDING_DEFAULT = {0, 0, 0, 0}; + + /** + * The default value which is used when the stale state is enabled + */ + private static final long STALE_STATE_DELAY_MS = 30_000L; + + /** + * Default animation duration multiplier + */ + private static final float TRACKING_ANIMATION_DURATION_MULTIPLIER_DEFAULT = 1.1f; + + private float accuracyAlpha; + private int accuracyColor; + private int backgroundDrawableStale; + @Nullable + private String backgroundStaleName; + private int foregroundDrawableStale; + @Nullable + private String foregroundStaleName; + private int gpsDrawable; + @Nullable + private String gpsName; + private int foregroundDrawable; + @Nullable + private String foregroundName; + private int backgroundDrawable; + @Nullable + private String backgroundName; + private int bearingDrawable; + @Nullable + private String bearingName; + @Nullable + private Integer bearingTintColor; + @Nullable + private Integer foregroundTintColor; + @Nullable + private Integer backgroundTintColor; + @Nullable + private Integer foregroundStaleTintColor; + @Nullable + private Integer backgroundStaleTintColor; + private float elevation; + private boolean enableStaleState; + private long staleStateTimeout; + @Nullable + private int[] padding; + private float maxZoomIconScale; + private float minZoomIconScale; + private boolean trackingGesturesManagement; + private float trackingInitialMoveThreshold; + private float trackingMultiFingerMoveThreshold; + private String layerAbove; + private String layerBelow; + private float trackingAnimationDurationMultiplier; + private boolean compassAnimationEnabled; + private boolean accuracyAnimationEnabled; + + public LocationComponentOptions( + float accuracyAlpha, + int accuracyColor, + int backgroundDrawableStale, + @Nullable String backgroundStaleName, + int foregroundDrawableStale, + @Nullable String foregroundStaleName, + int gpsDrawable, + @Nullable String gpsName, + int foregroundDrawable, + @Nullable String foregroundName, + int backgroundDrawable, + @Nullable String backgroundName, + int bearingDrawable, + @Nullable String bearingName, + @Nullable Integer bearingTintColor, + @Nullable Integer foregroundTintColor, + @Nullable Integer backgroundTintColor, + @Nullable Integer foregroundStaleTintColor, + @Nullable Integer backgroundStaleTintColor, + float elevation, + boolean enableStaleState, + long staleStateTimeout, + @Nullable int[] padding, + float maxZoomIconScale, + float minZoomIconScale, + boolean trackingGesturesManagement, + float trackingInitialMoveThreshold, + float trackingMultiFingerMoveThreshold, + String layerAbove, + String layerBelow, + float trackingAnimationDurationMultiplier, + boolean compassAnimationEnabled, + boolean accuracyAnimationEnabled) { + this.accuracyAlpha = accuracyAlpha; + this.accuracyColor = accuracyColor; + this.backgroundDrawableStale = backgroundDrawableStale; + this.backgroundStaleName = backgroundStaleName; + this.foregroundDrawableStale = foregroundDrawableStale; + this.foregroundStaleName = foregroundStaleName; + this.gpsDrawable = gpsDrawable; + this.gpsName = gpsName; + this.foregroundDrawable = foregroundDrawable; + this.foregroundName = foregroundName; + this.backgroundDrawable = backgroundDrawable; + this.backgroundName = backgroundName; + this.bearingDrawable = bearingDrawable; + this.bearingName = bearingName; + this.bearingTintColor = bearingTintColor; + this.foregroundTintColor = foregroundTintColor; + this.backgroundTintColor = backgroundTintColor; + this.foregroundStaleTintColor = foregroundStaleTintColor; + this.backgroundStaleTintColor = backgroundStaleTintColor; + this.elevation = elevation; + this.enableStaleState = enableStaleState; + this.staleStateTimeout = staleStateTimeout; + if (padding == null) { + throw new NullPointerException("Null padding"); + } + this.padding = padding; + this.maxZoomIconScale = maxZoomIconScale; + this.minZoomIconScale = minZoomIconScale; + this.trackingGesturesManagement = trackingGesturesManagement; + this.trackingInitialMoveThreshold = trackingInitialMoveThreshold; + this.trackingMultiFingerMoveThreshold = trackingMultiFingerMoveThreshold; + this.layerAbove = layerAbove; + this.layerBelow = layerBelow; + this.trackingAnimationDurationMultiplier = trackingAnimationDurationMultiplier; + this.compassAnimationEnabled = compassAnimationEnabled; + this.accuracyAnimationEnabled = accuracyAnimationEnabled; + } + + /** + * Construct a new Location Component Options class using the attributes found within a style + * resource. It's important to note that you only need to define the attributes you plan to + * change and can safely ignore the other attributes which will be set to their default value. + * + * @param context your activity's context used for acquiring resources + * @param styleRes the style id where your custom attributes are defined + * @return a new {@link LocationComponentOptions} object with the settings you defined in your style + * resource + */ + @NonNull + public static LocationComponentOptions createFromAttributes(@NonNull Context context, + @StyleRes int styleRes) { + + TypedArray typedArray = context.obtainStyledAttributes( + styleRes, R.styleable.mapbox_LocationComponent); + + LocationComponentOptions.Builder builder = new LocationComponentOptions.Builder() + .enableStaleState(true) + .staleStateTimeout(STALE_STATE_DELAY_MS) + .maxZoomIconScale(MAX_ZOOM_ICON_SCALE_DEFAULT) + .minZoomIconScale(MIN_ZOOM_ICON_SCALE_DEFAULT) + .padding(PADDING_DEFAULT); + + builder.foregroundDrawable(typedArray.getResourceId( + R.styleable.mapbox_LocationComponent_mapbox_foregroundDrawable, -1)); + if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_foregroundTintColor)) { + builder.foregroundTintColor(typedArray.getColor( + R.styleable.mapbox_LocationComponent_mapbox_foregroundTintColor, -1)); + } + builder.backgroundDrawable(typedArray.getResourceId( + R.styleable.mapbox_LocationComponent_mapbox_backgroundDrawable, -1)); + if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_backgroundTintColor)) { + builder.backgroundTintColor(typedArray.getColor( + R.styleable.mapbox_LocationComponent_mapbox_backgroundTintColor, -1)); + } + builder.foregroundDrawableStale(typedArray.getResourceId( + R.styleable.mapbox_LocationComponent_mapbox_foregroundDrawableStale, -1)); + if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_foregroundStaleTintColor)) { + builder.foregroundStaleTintColor(typedArray.getColor( + R.styleable.mapbox_LocationComponent_mapbox_foregroundStaleTintColor, -1)); + } + builder.backgroundDrawableStale(typedArray.getResourceId( + R.styleable.mapbox_LocationComponent_mapbox_backgroundDrawableStale, -1)); + if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_backgroundStaleTintColor)) { + builder.backgroundStaleTintColor(typedArray.getColor( + R.styleable.mapbox_LocationComponent_mapbox_backgroundStaleTintColor, -1)); + } + builder.bearingDrawable(typedArray.getResourceId( + R.styleable.mapbox_LocationComponent_mapbox_bearingDrawable, -1)); + if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_bearingTintColor)) { + builder.bearingTintColor(typedArray.getColor( + R.styleable.mapbox_LocationComponent_mapbox_bearingTintColor, -1)); + } + if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_enableStaleState)) { + builder.enableStaleState(typedArray.getBoolean( + R.styleable.mapbox_LocationComponent_mapbox_enableStaleState, true)); + } + if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_staleStateTimeout)) { + builder.staleStateTimeout(typedArray.getInteger( + R.styleable.mapbox_LocationComponent_mapbox_staleStateTimeout, (int) STALE_STATE_DELAY_MS)); + } + builder.gpsDrawable(typedArray.getResourceId( + R.styleable.mapbox_LocationComponent_mapbox_gpsDrawable, -1)); + float elevation = typedArray.getDimension( + R.styleable.mapbox_LocationComponent_mapbox_elevation, 0); + builder.accuracyColor(typedArray.getColor( + R.styleable.mapbox_LocationComponent_mapbox_accuracyColor, -1)); + builder.accuracyAlpha(typedArray.getFloat( + R.styleable.mapbox_LocationComponent_mapbox_accuracyAlpha, ACCURACY_ALPHA_DEFAULT)); + builder.elevation(elevation); + + builder.trackingGesturesManagement(typedArray.getBoolean( + R.styleable.mapbox_LocationComponent_mapbox_trackingGesturesManagement, false)); + builder.trackingInitialMoveThreshold(typedArray.getDimension( + R.styleable.mapbox_LocationComponent_mapbox_trackingInitialMoveThreshold, + context.getResources().getDimension(R.dimen.mapbox_locationComponentTrackingInitialMoveThreshold))); + builder.trackingMultiFingerMoveThreshold(typedArray.getDimension( + R.styleable.mapbox_LocationComponent_mapbox_trackingMultiFingerMoveThreshold, + context.getResources().getDimension(R.dimen.mapbox_locationComponentTrackingMultiFingerMoveThreshold))); + + builder.padding(new int[] { + typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingLeft, 0), + typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingTop, 0), + typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingRight, 0), + typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingBottom, 0), + }); + + builder.layerAbove( + typedArray.getString(R.styleable.mapbox_LocationComponent_mapbox_layer_above)); + + builder.layerBelow( + typedArray.getString(R.styleable.mapbox_LocationComponent_mapbox_layer_below)); + + float minScale = typedArray.getFloat( + R.styleable.mapbox_LocationComponent_mapbox_minZoomIconScale, MIN_ZOOM_ICON_SCALE_DEFAULT); + float maxScale = typedArray.getFloat( + R.styleable.mapbox_LocationComponent_mapbox_maxZoomIconScale, MAX_ZOOM_ICON_SCALE_DEFAULT); + builder.minZoomIconScale(minScale); + builder.maxZoomIconScale(maxScale); + + float trackingAnimationDurationMultiplier = typedArray.getFloat( + R.styleable.mapbox_LocationComponent_mapbox_trackingAnimationDurationMultiplier, + TRACKING_ANIMATION_DURATION_MULTIPLIER_DEFAULT + ); + builder.trackingAnimationDurationMultiplier(trackingAnimationDurationMultiplier); + + builder.compassAnimationEnabled = typedArray.getBoolean( + R.styleable.mapbox_LocationComponent_mapbox_compassAnimationEnabled, true + ); + + builder.accuracyAnimationEnabled = typedArray.getBoolean( + R.styleable.mapbox_LocationComponent_mapbox_accuracyAnimationEnabled, true + ); + + typedArray.recycle(); + + return builder.build(); + } + + /** + * Takes the currently constructed {@link LocationComponentOptions} object and provides it's builder + * with all the values set matching the values in this instance. This allows you to modify a + * single attribute and then rebuild the object. + * + * @return the builder which contains the values defined in this current instance as defaults. + */ + @NonNull + public Builder toBuilder() { + return new Builder(this); + } + + /** + * Build a new instance of the {@link LocationComponentOptions} class with all the attributes set + * automatically to their defined defaults in this library. This allows you to adjust a few + * attributes while leaving the rest alone and maintaining their default behavior. + * + * @param context your activities context used to acquire the style resource + * @return the builder which contains the default values defined by the style resource + */ + @NonNull + public static Builder builder(@NonNull Context context) { + return LocationComponentOptions.createFromAttributes(context, + R.style.mapbox_LocationComponent).toBuilder(); + } + + /** + * Set the opacity of the accuracy view to a value from 0 to 1, where 0 means the accuracy view is + * completely transparent and 1 means the view is completely opaque. + * References style attribute R.styleable#LocationComponent_accuracyAlpha + * + * @return the opacity of the accuracy view + */ + public float accuracyAlpha() { + return accuracyAlpha; + } + + /** + * Solid color to use as the accuracy view color property. + * References style attribute R.styleable#LocationComponent_accuracyColor + * + * @return the color of the accuracy view + */ + @ColorInt + public int accuracyColor() { + return accuracyColor; + } + + /** + * Defines the drawable used for the stale background icon. + * References style attribute R.styleable#LocationComponent_backgroundDrawableStale + * + * @return the drawable resource ID + */ + @DrawableRes + public int backgroundDrawableStale() { + return backgroundDrawableStale; + } + + /** + * String image name, identical to one used in + * the first parameter of {@link com.mapbox.mapboxsdk.maps.Style.Builder#addImage(String, Bitmap)}, the + * component, will use this image in place of the provided or default mapbox_foregroundDrawableStale. + *

+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded + * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style. + *

+ * + * @return String icon or maki-icon name + */ + @Nullable + public String backgroundStaleName() { + return backgroundStaleName; + } + + /** + * Defines the drawable used for the stale foreground icon. + * References style attribute R.styleable#LocationComponent_foregroundDrawableStale + * + * @return the drawable resource ID + */ + @DrawableRes + public int foregroundDrawableStale() { + return foregroundDrawableStale; + } + + /** + * String image name, identical to one used in + * the first parameter of {@link com.mapbox.mapboxsdk.maps.Style.Builder#addImage(String, Bitmap)}, the + * component, will used this image in place of the provided or default mapbox_foregroundDrawableStale. + *

+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded + * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style. + *

+ * + * @return String icon or maki-icon name + */ + @Nullable + public String foregroundStaleName() { + return foregroundStaleName; + } + + /** + * Defines the drawable used for the navigation state icon. + * References style attribute R.styleable#LocationComponent_gpsDrawable + * + * @return the drawable resource ID + */ + @DrawableRes + public int gpsDrawable() { + return gpsDrawable; + } + + /** + * String image name, identical to one used in + * the first parameter of {@link com.mapbox.mapboxsdk.maps.Style.Builder#addImage(String, Bitmap)}, the + * component, will used this image in place of the provided or default mapbox_gpsDrawable. + *

+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded + * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style. + *

+ * + * @return String icon or maki-icon name + */ + @Nullable + public String gpsName() { + return gpsName; + } + + /** + * Supply a Drawable that is to be rendered on top of all of the content in the Location LayerComponent layer stack. + * References style attribute R.styleable#LocationComponent_foregroundDrawable + * + * @return the drawable resource used for the foreground layer + */ + @DrawableRes + public int foregroundDrawable() { + return foregroundDrawable; + } + + /** + * String image name, identical to one used in + * the first parameter of {@link com.mapbox.mapboxsdk.maps.Style.Builder#addImage(String, Bitmap)}, the + * component, will used this image in place of the provided or default mapbox_foregroundDrawable. + *

+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded + * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style. + *

+ * + * @return String icon or maki-icon name + */ + @Nullable + public String foregroundName() { + return foregroundName; + } + + /** + * Defines the drawable used for the background state icon. + * References style attribute R.styleable#LocationComponent_backgroundDrawable + * + * @return the drawable resource ID + */ + @DrawableRes + public int backgroundDrawable() { + return backgroundDrawable; + } + + /** + * String image name, identical to one used in + * the first parameter of {@link com.mapbox.mapboxsdk.maps.Style.Builder#addImage(String, Bitmap)}, the + * component, will used this image in place of the provided or default mapbox_backgroundDrawable. + *

+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded + * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style. + *

+ * + * @return String icon or maki-icon name + */ + @Nullable + public String backgroundName() { + return backgroundName; + } + + /** + * Defines the drawable used for the bearing icon. + * References style attribute R.styleable#LocationComponent_bearingDrawable + * + * @return the drawable resource ID + */ + @DrawableRes + public int bearingDrawable() { + return bearingDrawable; + } + + /** + * String image name, identical to one used in + * the first parameter of {@link com.mapbox.mapboxsdk.maps.Style.Builder#addImage(String, Bitmap)}, the + * component, will used this image in place of the provided or default mapbox_bearingDrawable. + *

+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded + * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style. + *

+ * + * @return String icon or maki-icon name + */ + @Nullable + public String bearingName() { + return bearingName; + } + + /** + * Defines the bearing icon color as an integer. + * References style attribute R.styleable#LocationComponent_bearingTintColor + * + * @return the color integer resource + */ + @ColorInt + @Nullable + public Integer bearingTintColor() { + return bearingTintColor; + } + + /** + * Defines the foreground color as an integer. + * References style attribute R.styleable#LocationComponent_foregroundTintColor + * + * @return the color integer resource + */ + @ColorInt + @Nullable + public Integer foregroundTintColor() { + return foregroundTintColor; + } + + /** + * Defines the background color as an integer. + * References style attribute R.styleable#LocationComponent_backgroundTintColor + * + * @return the color integer resource + */ + @ColorInt + @Nullable + public Integer backgroundTintColor() { + return backgroundTintColor; + } + + /** + * Defines the foreground stale color as an integer. + * References style attribute R.styleable#LocationComponent_foregroundStaleTintColor + * + * @return the color integer resource + */ + @ColorInt + @Nullable + public Integer foregroundStaleTintColor() { + return foregroundStaleTintColor; + } + + /** + * Defines the background stale color as an integer. + * References style attribute R.styleable#LocationComponent_backgroundStaleTintColor + * + * @return the color integer resource + */ + @ColorInt + @Nullable + public Integer backgroundStaleTintColor() { + return backgroundStaleTintColor; + } + + /** + * Sets the base elevation of this view, in pixels. To turn off the shadow that appears under + * the location icon, set the elevation to 0. + * References style attribute R.styleable#LocationComponent_elevation + * + * @return the elevation currently set for the location component icon + */ + @Dimension + public float elevation() { + return elevation; + } + + /** + * Enable or disable to stale state mode. This mode indicates to the user that the location being + * displayed on the map hasn't been updated in a specific amount of time. + * References style attribute R.styleable#LocationComponent_enableStaleState + * + * @return whether the stale state mode is enabled or not + */ + public boolean enableStaleState() { + return enableStaleState; + } + + /** + * Set the delay before the location icon becomes stale. The timer begins approximately when a new + * location update comes in and using this defined time, if an update hasn't occured by the end, + * the location is considered stale. + * References style attribute R.styleable#LocationComponent_staleStateDelay + * + * @return the duration in milliseconds which it should take before the location is + * considered stale + */ + public long staleStateTimeout() { + return staleStateTimeout; + } + + /** + * Sets the distance from the edges of the map view’s frame to the edges of the map + * view’s logical viewport. + *

+ *

+ * When the value of this property is equal to {0,0,0,0}, viewport + * properties such as `centerCoordinate` assume a viewport that matches the map + * view’s frame. Otherwise, those properties are inset, excluding part of the + * frame from the viewport. For instance, if the only the top edge is inset, the + * map center is effectively shifted downward. + *

+ * + * @return integer array of padding values + */ + @Nullable + @SuppressWarnings("mutable") + public int[] padding() { + return padding; + } + + /** + * The scale factor of the location icon when the map is zoomed in. + * Scaling is linear. + * + * @return icon scale factor + */ + public float maxZoomIconScale() { + return maxZoomIconScale; + } + + /** + * The scale factor of the location icon when the map is zoomed out. + * Scaling is linear. + * + * @return icon scale factor + */ + public float minZoomIconScale() { + return minZoomIconScale; + } + + /** + * Returns whether gesture threshold should be adjusted when camera is in one of the tracking modes. + * This will adjust the focal point and increase thresholds to enable camera manipulation, + * like zooming in and out, without breaking tracking. + *

+ * Note: If set to true, this can overwrite some of the gesture thresholds + * and the custom {@link com.mapbox.android.gestures.AndroidGesturesManager} that was set with + * {@link com.mapbox.mapboxsdk.maps.MapboxMap#setGesturesManager(AndroidGesturesManager, boolean, boolean)}. + * + * @return true if gestures are adjusted when in one of the camera tracking modes, false otherwise + * @see Builder#trackingInitialMoveThreshold(float) + * @see Builder#trackingMultiFingerMoveThreshold(float) + */ + public boolean trackingGesturesManagement() { + return trackingGesturesManagement; + } + + /** + * Minimum single pointer movement in pixels required to break camera tracking. + * + * @return the minimum movement + */ + public float trackingInitialMoveThreshold() { + return trackingInitialMoveThreshold; + } + + /** + * Minimum multi pointer movement in pixels required to break camera tracking (for example during scale gesture). + * + * @return the minimum movement + */ + public float trackingMultiFingerMoveThreshold() { + return trackingMultiFingerMoveThreshold; + } + + /** + * Gets the id of the layer that's referenced when placing the component on the map using + * {@link com.mapbox.mapboxsdk.maps.Style#addLayerAbove(Layer, String)}. + *

+ * The component is going to placed directly above this layer. + * + * @return layerAbove the id of the layer the component is going to placed directly above. + */ + public String layerAbove() { + return layerAbove; + } + + /** + * Gets the id of the layer that's referenced when placing the component on the map using + * {@link com.mapbox.mapboxsdk.maps.Style#addLayerBelow(Layer, String)}. + *

+ * The component is going to placed directly below this layer. + * + * @return layerBelow the id of the layer the component is going to placed directly below. + */ + public String layerBelow() { + return layerBelow; + } + + /** + * Get the tracking animation duration multiplier. + * + * @return tracking animation duration multiplier + */ + public float trackingAnimationDurationMultiplier() { + return trackingAnimationDurationMultiplier; + } + + /** + * Enable or disable smooth animation of compass values for {@link com.mapbox.mapboxsdk.location.modes.CameraMode} + * and {@link com.mapbox.mapboxsdk.location.modes.RenderMode}. + * + * @return whether smooth compass animation is enabled + */ + public boolean compassAnimationEnabled() { + return compassAnimationEnabled; + } + + /** + * Enable or disable smooth animation of the accuracy circle around the user's position. + * + * @return whether smooth animation of the accuracy circle is enabled + */ + public boolean accuracyAnimationEnabled() { + return accuracyAnimationEnabled; + } + + @NonNull + @Override + public String toString() { + return "LocationComponentOptions{" + + "accuracyAlpha=" + accuracyAlpha + ", " + + "accuracyColor=" + accuracyColor + ", " + + "backgroundDrawableStale=" + backgroundDrawableStale + ", " + + "backgroundStaleName=" + backgroundStaleName + ", " + + "foregroundDrawableStale=" + foregroundDrawableStale + ", " + + "foregroundStaleName=" + foregroundStaleName + ", " + + "gpsDrawable=" + gpsDrawable + ", " + + "gpsName=" + gpsName + ", " + + "foregroundDrawable=" + foregroundDrawable + ", " + + "foregroundName=" + foregroundName + ", " + + "backgroundDrawable=" + backgroundDrawable + ", " + + "backgroundName=" + backgroundName + ", " + + "bearingDrawable=" + bearingDrawable + ", " + + "bearingName=" + bearingName + ", " + + "bearingTintColor=" + bearingTintColor + ", " + + "foregroundTintColor=" + foregroundTintColor + ", " + + "backgroundTintColor=" + backgroundTintColor + ", " + + "foregroundStaleTintColor=" + foregroundStaleTintColor + ", " + + "backgroundStaleTintColor=" + backgroundStaleTintColor + ", " + + "elevation=" + elevation + ", " + + "enableStaleState=" + enableStaleState + ", " + + "staleStateTimeout=" + staleStateTimeout + ", " + + "padding=" + Arrays.toString(padding) + ", " + + "maxZoomIconScale=" + maxZoomIconScale + ", " + + "minZoomIconScale=" + minZoomIconScale + ", " + + "trackingGesturesManagement=" + trackingGesturesManagement + ", " + + "trackingInitialMoveThreshold=" + trackingInitialMoveThreshold + ", " + + "trackingMultiFingerMoveThreshold=" + trackingMultiFingerMoveThreshold + ", " + + "layerAbove=" + layerAbove + + "layerBelow=" + layerBelow + + "trackingAnimationDurationMultiplier=" + trackingAnimationDurationMultiplier + + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + LocationComponentOptions options = (LocationComponentOptions) o; + + if (Float.compare(options.accuracyAlpha, accuracyAlpha) != 0) { + return false; + } + if (accuracyColor != options.accuracyColor) { + return false; + } + if (backgroundDrawableStale != options.backgroundDrawableStale) { + return false; + } + if (foregroundDrawableStale != options.foregroundDrawableStale) { + return false; + } + if (gpsDrawable != options.gpsDrawable) { + return false; + } + if (foregroundDrawable != options.foregroundDrawable) { + return false; + } + if (backgroundDrawable != options.backgroundDrawable) { + return false; + } + if (bearingDrawable != options.bearingDrawable) { + return false; + } + if (Float.compare(options.elevation, elevation) != 0) { + return false; + } + if (enableStaleState != options.enableStaleState) { + return false; + } + if (staleStateTimeout != options.staleStateTimeout) { + return false; + } + if (Float.compare(options.maxZoomIconScale, maxZoomIconScale) != 0) { + return false; + } + if (Float.compare(options.minZoomIconScale, minZoomIconScale) != 0) { + return false; + } + if (trackingGesturesManagement != options.trackingGesturesManagement) { + return false; + } + if (Float.compare(options.trackingInitialMoveThreshold, trackingInitialMoveThreshold) != 0) { + return false; + } + if (Float.compare(options.trackingMultiFingerMoveThreshold, trackingMultiFingerMoveThreshold) != 0) { + return false; + } + if (Float.compare(options.trackingAnimationDurationMultiplier, trackingAnimationDurationMultiplier) != 0) { + return false; + } + if (compassAnimationEnabled != options.compassAnimationEnabled) { + return false; + } + if (accuracyAnimationEnabled != options.accuracyAnimationEnabled) { + return false; + } + if (backgroundStaleName != null ? !backgroundStaleName.equals(options.backgroundStaleName) : + options.backgroundStaleName != null) { + return false; + } + if (foregroundStaleName != null ? !foregroundStaleName.equals(options.foregroundStaleName) : + options.foregroundStaleName != null) { + return false; + } + if (gpsName != null ? !gpsName.equals(options.gpsName) : options.gpsName != null) { + return false; + } + if (foregroundName != null ? !foregroundName.equals(options.foregroundName) : options.foregroundName != null) { + return false; + } + if (backgroundName != null ? !backgroundName.equals(options.backgroundName) : options.backgroundName != null) { + return false; + } + if (bearingName != null ? !bearingName.equals(options.bearingName) : options.bearingName != null) { + return false; + } + if (bearingTintColor != null ? !bearingTintColor.equals(options.bearingTintColor) : + options.bearingTintColor != null) { + return false; + } + if (foregroundTintColor != null ? !foregroundTintColor.equals(options.foregroundTintColor) : + options.foregroundTintColor != null) { + return false; + } + if (backgroundTintColor != null ? !backgroundTintColor.equals(options.backgroundTintColor) : + options.backgroundTintColor != null) { + return false; + } + if (foregroundStaleTintColor != null ? !foregroundStaleTintColor.equals(options.foregroundStaleTintColor) : + options.foregroundStaleTintColor != null) { + return false; + } + if (backgroundStaleTintColor != null ? !backgroundStaleTintColor.equals(options.backgroundStaleTintColor) : + options.backgroundStaleTintColor != null) { + return false; + } + if (!Arrays.equals(padding, options.padding)) { + return false; + } + if (layerAbove != null ? !layerAbove.equals(options.layerAbove) : options.layerAbove != null) { + return false; + } + return layerBelow != null ? layerBelow.equals(options.layerBelow) : options.layerBelow == null; + } + + @Override + public int hashCode() { + int result = (accuracyAlpha != +0.0f ? Float.floatToIntBits(accuracyAlpha) : 0); + result = 31 * result + accuracyColor; + result = 31 * result + backgroundDrawableStale; + result = 31 * result + (backgroundStaleName != null ? backgroundStaleName.hashCode() : 0); + result = 31 * result + foregroundDrawableStale; + result = 31 * result + (foregroundStaleName != null ? foregroundStaleName.hashCode() : 0); + result = 31 * result + gpsDrawable; + result = 31 * result + (gpsName != null ? gpsName.hashCode() : 0); + result = 31 * result + foregroundDrawable; + result = 31 * result + (foregroundName != null ? foregroundName.hashCode() : 0); + result = 31 * result + backgroundDrawable; + result = 31 * result + (backgroundName != null ? backgroundName.hashCode() : 0); + result = 31 * result + bearingDrawable; + result = 31 * result + (bearingName != null ? bearingName.hashCode() : 0); + result = 31 * result + (bearingTintColor != null ? bearingTintColor.hashCode() : 0); + result = 31 * result + (foregroundTintColor != null ? foregroundTintColor.hashCode() : 0); + result = 31 * result + (backgroundTintColor != null ? backgroundTintColor.hashCode() : 0); + result = 31 * result + (foregroundStaleTintColor != null ? foregroundStaleTintColor.hashCode() : 0); + result = 31 * result + (backgroundStaleTintColor != null ? backgroundStaleTintColor.hashCode() : 0); + result = 31 * result + (elevation != +0.0f ? Float.floatToIntBits(elevation) : 0); + result = 31 * result + (enableStaleState ? 1 : 0); + result = 31 * result + (int) (staleStateTimeout ^ (staleStateTimeout >>> 32)); + result = 31 * result + Arrays.hashCode(padding); + result = 31 * result + (maxZoomIconScale != +0.0f ? Float.floatToIntBits(maxZoomIconScale) : 0); + result = 31 * result + (minZoomIconScale != +0.0f ? Float.floatToIntBits(minZoomIconScale) : 0); + result = 31 * result + (trackingGesturesManagement ? 1 : 0); + result = 31 * result + (trackingInitialMoveThreshold != +0.0f + ? Float.floatToIntBits(trackingInitialMoveThreshold) : 0); + result = 31 * result + (trackingMultiFingerMoveThreshold != +0.0f + ? Float.floatToIntBits(trackingMultiFingerMoveThreshold) : 0); + result = 31 * result + (layerAbove != null ? layerAbove.hashCode() : 0); + result = 31 * result + (layerBelow != null ? layerBelow.hashCode() : 0); + result = 31 * result + (trackingAnimationDurationMultiplier != +0.0f + ? Float.floatToIntBits(trackingAnimationDurationMultiplier) : 0); + result = 31 * result + (compassAnimationEnabled ? 1 : 0); + result = 31 * result + (accuracyAnimationEnabled ? 1 : 0); + return result; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public LocationComponentOptions createFromParcel(Parcel in) { + return new LocationComponentOptions( + in.readFloat(), + in.readInt(), + in.readInt(), + in.readInt() == 0 ? in.readString() : null, + in.readInt(), + in.readInt() == 0 ? in.readString() : null, + in.readInt(), + in.readInt() == 0 ? in.readString() : null, + in.readInt(), + in.readInt() == 0 ? in.readString() : null, + in.readInt(), + in.readInt() == 0 ? in.readString() : null, + in.readInt(), + in.readInt() == 0 ? in.readString() : null, + in.readInt() == 0 ? in.readInt() : null, + in.readInt() == 0 ? in.readInt() : null, + in.readInt() == 0 ? in.readInt() : null, + in.readInt() == 0 ? in.readInt() : null, + in.readInt() == 0 ? in.readInt() : null, + in.readFloat(), + in.readInt() == 1, + in.readLong(), + in.createIntArray(), + in.readFloat(), + in.readFloat(), + in.readInt() == 1, + in.readFloat(), + in.readFloat(), + in.readString(), + in.readString(), + in.readFloat(), + in.readInt() == 1, + in.readInt() == 1 + ); + } + + @Override + public LocationComponentOptions[] newArray(int size) { + return new LocationComponentOptions[size]; + } + }; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeFloat(accuracyAlpha()); + dest.writeInt(accuracyColor()); + dest.writeInt(backgroundDrawableStale()); + if (backgroundStaleName() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeString(backgroundStaleName()); + } + dest.writeInt(foregroundDrawableStale()); + if (foregroundStaleName() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeString(foregroundStaleName()); + } + dest.writeInt(gpsDrawable()); + if (gpsName() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeString(gpsName()); + } + dest.writeInt(foregroundDrawable()); + if (foregroundName() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeString(foregroundName()); + } + dest.writeInt(backgroundDrawable()); + if (backgroundName() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeString(backgroundName()); + } + dest.writeInt(bearingDrawable()); + if (bearingName() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeString(bearingName()); + } + if (bearingTintColor() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeInt(bearingTintColor()); + } + if (foregroundTintColor() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeInt(foregroundTintColor()); + } + if (backgroundTintColor() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeInt(backgroundTintColor()); + } + if (foregroundStaleTintColor() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeInt(foregroundStaleTintColor()); + } + if (backgroundStaleTintColor() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeInt(backgroundStaleTintColor()); + } + dest.writeFloat(elevation()); + dest.writeInt(enableStaleState() ? 1 : 0); + dest.writeLong(staleStateTimeout()); + dest.writeIntArray(padding()); + dest.writeFloat(maxZoomIconScale()); + dest.writeFloat(minZoomIconScale()); + dest.writeInt(trackingGesturesManagement() ? 1 : 0); + dest.writeFloat(trackingInitialMoveThreshold()); + dest.writeFloat(trackingMultiFingerMoveThreshold()); + dest.writeString(layerAbove()); + dest.writeString(layerBelow()); + dest.writeFloat(trackingAnimationDurationMultiplier); + dest.writeInt(compassAnimationEnabled() ? 1 : 0); + dest.writeInt(accuracyAnimationEnabled() ? 1 : 0); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Builder class for constructing a new instance of {@link LocationComponentOptions}. + */ + public static class Builder { + + /** + * Build a new instance of this {@link LocationComponentOptions} class. + * + * @return a new instance of {@link LocationComponentOptions} + */ + @NonNull + public LocationComponentOptions build() { + LocationComponentOptions locationComponentOptions = autoBuild(); + if (locationComponentOptions.accuracyAlpha() < 0 || locationComponentOptions.accuracyAlpha() > 1) { + throw new IllegalArgumentException( + "Accuracy alpha value must be between 0.0 and 1.0."); + } + + if (locationComponentOptions.elevation() < 0f) { + throw new IllegalArgumentException("Invalid shadow size " + + locationComponentOptions.elevation() + ". Must be >= 0"); + } + + if (locationComponentOptions.layerAbove() != null && locationComponentOptions.layerBelow() != null) { + throw new IllegalArgumentException("You cannot set both layerAbove and layerBelow options." + + "Choose one or the other."); + } + + return locationComponentOptions; + } + + private Float accuracyAlpha; + private Integer accuracyColor; + private Integer backgroundDrawableStale; + @Nullable + private String backgroundStaleName; + private Integer foregroundDrawableStale; + @Nullable + private String foregroundStaleName; + private Integer gpsDrawable; + @Nullable + private String gpsName; + private Integer foregroundDrawable; + @Nullable + private String foregroundName; + private Integer backgroundDrawable; + @Nullable + private String backgroundName; + private Integer bearingDrawable; + @Nullable + private String bearingName; + @Nullable + private Integer bearingTintColor; + @Nullable + private Integer foregroundTintColor; + @Nullable + private Integer backgroundTintColor; + @Nullable + private Integer foregroundStaleTintColor; + @Nullable + private Integer backgroundStaleTintColor; + private Float elevation; + private Boolean enableStaleState; + private Long staleStateTimeout; + @Nullable + private int[] padding; + private Float maxZoomIconScale; + private Float minZoomIconScale; + private Boolean trackingGesturesManagement; + private Float trackingInitialMoveThreshold; + private Float trackingMultiFingerMoveThreshold; + private String layerAbove; + private String layerBelow; + private Float trackingAnimationDurationMultiplier; + private Boolean compassAnimationEnabled; + private Boolean accuracyAnimationEnabled; + + Builder() { + } + + private Builder(LocationComponentOptions source) { + this.accuracyAlpha = source.accuracyAlpha(); + this.accuracyColor = source.accuracyColor(); + this.backgroundDrawableStale = source.backgroundDrawableStale(); + this.backgroundStaleName = source.backgroundStaleName(); + this.foregroundDrawableStale = source.foregroundDrawableStale(); + this.foregroundStaleName = source.foregroundStaleName(); + this.gpsDrawable = source.gpsDrawable(); + this.gpsName = source.gpsName(); + this.foregroundDrawable = source.foregroundDrawable(); + this.foregroundName = source.foregroundName(); + this.backgroundDrawable = source.backgroundDrawable(); + this.backgroundName = source.backgroundName(); + this.bearingDrawable = source.bearingDrawable(); + this.bearingName = source.bearingName(); + this.bearingTintColor = source.bearingTintColor(); + this.foregroundTintColor = source.foregroundTintColor(); + this.backgroundTintColor = source.backgroundTintColor(); + this.foregroundStaleTintColor = source.foregroundStaleTintColor(); + this.backgroundStaleTintColor = source.backgroundStaleTintColor(); + this.elevation = source.elevation(); + this.enableStaleState = source.enableStaleState(); + this.staleStateTimeout = source.staleStateTimeout(); + this.padding = source.padding(); + this.maxZoomIconScale = source.maxZoomIconScale(); + this.minZoomIconScale = source.minZoomIconScale(); + this.trackingGesturesManagement = source.trackingGesturesManagement(); + this.trackingInitialMoveThreshold = source.trackingInitialMoveThreshold(); + this.trackingMultiFingerMoveThreshold = source.trackingMultiFingerMoveThreshold(); + this.layerAbove = source.layerAbove(); + this.layerBelow = source.layerBelow(); + this.trackingAnimationDurationMultiplier = source.trackingAnimationDurationMultiplier(); + this.compassAnimationEnabled = source.compassAnimationEnabled(); + this.accuracyAnimationEnabled = source.accuracyAnimationEnabled(); + } + + /** + * Set the opacity of the accuracy view to a value from 0 to 1, where 0 means the accuracy view + * is completely transparent and 1 means the view is completely opaque. + * References style attribute R.styleable#LocationComponent_accuracyAlpha + * + * @param accuracyAlpha the opacity of the accuracy view + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder accuracyAlpha(float accuracyAlpha) { + this.accuracyAlpha = accuracyAlpha; + return this; + } + + /** + * Solid color to use as the accuracy view color property. + * References style attribute R.styleable#LocationComponent_accuracyColor + * + * @param accuracyColor the color of the accuracy view + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder accuracyColor(int accuracyColor) { + this.accuracyColor = accuracyColor; + return this; + } + + /** + * Defines the drawable used for the stale background icon. + * References style attribute R.styleable#LocationComponent_backgroundDrawableStale + * + * @param backgroundDrawableStale the drawable resource ID + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder backgroundDrawableStale(int backgroundDrawableStale) { + this.backgroundDrawableStale = backgroundDrawableStale; + return this; + } + + /** + * Given a String image name, identical to one used in + * the first parameter of {@link com.mapbox.mapboxsdk.maps.Style.Builder#addImage(String, Bitmap)}, the + * component, will used this image in place of the provided or default mapbox_backgroundDrawableStale. + *

+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded + * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style. + *

+ * + * @param backgroundStaleName String icon or maki-icon name + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder backgroundStaleName(@Nullable String backgroundStaleName) { + this.backgroundStaleName = backgroundStaleName; + return this; + } + + /** + * Defines the drawable used for the stale foreground icon. + * References style attribute R.styleable#LocationComponent_foregroundDrawableStale + * + * @param foregroundDrawableStale the drawable resource ID + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder foregroundDrawableStale(int foregroundDrawableStale) { + this.foregroundDrawableStale = foregroundDrawableStale; + return this; + } + + /** + * Given a String image name, identical to one used in + * the first parameter of {@link com.mapbox.mapboxsdk.maps.Style.Builder#addImage(String, Bitmap)}, the + * component, will used this image in place of the provided or default mapbox_foregroundDrawableStale. + *

+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded + * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style. + *

+ * + * @param foregroundStaleName String icon or maki-icon name + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder foregroundStaleName(@Nullable String foregroundStaleName) { + this.foregroundStaleName = foregroundStaleName; + return this; + } + + /** + * Defines the drawable used for the navigation state icon. + * References style attribute R.styleable#LocationComponent_gpsDrawable + * + * @param gpsDrawable the drawable resource ID + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder gpsDrawable(int gpsDrawable) { + this.gpsDrawable = gpsDrawable; + return this; + } + + /** + * Given a String image name, identical to one used in + * the first parameter of {@link com.mapbox.mapboxsdk.maps.Style.Builder#addImage(String, Bitmap)}, the + * component, will used this image in place of the provided or default mapbox_gpsDrawable. + *

+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded + * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style. + *

+ * + * @param gpsName String icon or maki-icon name + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder gpsName(@Nullable String gpsName) { + this.gpsName = gpsName; + return this; + } + + /** + * Supply a Drawable that is to be rendered on top of all of the content in the Location Component layer stack. + * References style attribute R.styleable#LocationComponent_foregroundDrawable + * + * @param foregroundDrawable the drawable resource used for the foreground layer + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder foregroundDrawable(int foregroundDrawable) { + this.foregroundDrawable = foregroundDrawable; + return this; + } + + /** + * Given a String image name, identical to one used in + * the first parameter of {@link com.mapbox.mapboxsdk.maps.Style.Builder#addImage(String, Bitmap)}, the + * component, will used this image in place of the provided or default mapbox_foregroundDrawable. + *

+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded + * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style. + *

+ * + * @param foregroundName String icon or maki-icon name + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder foregroundName(@Nullable String foregroundName) { + this.foregroundName = foregroundName; + return this; + } + + /** + * Defines the drawable used for the background state icon. + * References style attribute R.styleable#LocationComponent_backgroundDrawable + * + * @param backgroundDrawable the drawable resource ID + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder backgroundDrawable(int backgroundDrawable) { + this.backgroundDrawable = backgroundDrawable; + return this; + } + + /** + * Given a String image name, identical to one used in + * the first parameter of {@link com.mapbox.mapboxsdk.maps.Style.Builder#addImage(String, Bitmap)}, the + * component, will used this image in place of the provided or default mapbox_backgroundDrawable. + *

+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded + * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style. + *

+ * + * @param backgroundName String icon or maki-icon name + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder backgroundName(@Nullable String backgroundName) { + this.backgroundName = backgroundName; + return this; + } + + /** + * Defines the drawable used for the bearing icon. + * References style attribute R.styleable#LocationComponent_bearingDrawable + * + * @param bearingDrawable the drawable resource ID + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder bearingDrawable(int bearingDrawable) { + this.bearingDrawable = bearingDrawable; + return this; + } + + /** + * Given a String image name, identical to one used in + * the first parameter of {@link com.mapbox.mapboxsdk.maps.Style.Builder#addImage(String, Bitmap)}, the + * component, will used this image in place of the provided or default mapbox_bearingDrawable. + *

+ * A maki-icon name (example: "circle-15") may also be provided. These are images that can be loaded + * with certain styles. Note, this will fail if the provided icon name is not provided by the loaded map style. + *

+ * + * @param bearingName String icon or maki-icon name + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder bearingName(@Nullable String bearingName) { + this.bearingName = bearingName; + return this; + } + + /** + * Defines the bearing icon color as an integer. + * References style attribute R.styleable#LocationComponent_bearingTintColor + * + * @param bearingTintColor the color integer resource + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder bearingTintColor(@Nullable Integer bearingTintColor) { + this.bearingTintColor = bearingTintColor; + return this; + } + + /** + * Defines the foreground color as an integer. + * References style attribute R.styleable#LocationComponent_foregroundTintColor + * + * @param foregroundTintColor the color integer resource + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder foregroundTintColor(@Nullable Integer foregroundTintColor) { + this.foregroundTintColor = foregroundTintColor; + return this; + } + + /** + * Defines the background color as an integer. + * References style attribute R.styleable#LocationComponent_backgroundTintColor + * + * @param backgroundTintColor the color integer resource + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder backgroundTintColor(@Nullable Integer backgroundTintColor) { + this.backgroundTintColor = backgroundTintColor; + return this; + } + + /** + * Defines the foreground stale color as an integer. + * References style attribute R.styleable#LocationComponent_foregroundStaleTintColor + * + * @param foregroundStaleTintColor the color integer resource + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder foregroundStaleTintColor(@Nullable Integer foregroundStaleTintColor) { + this.foregroundStaleTintColor = foregroundStaleTintColor; + return this; + } + + /** + * Defines the background stale color as an integer. + * References style attribute R.styleable#LocationComponent_backgroundStaleTintColor + * + * @param backgroundStaleTintColor the color integer resource + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder backgroundStaleTintColor(@Nullable Integer backgroundStaleTintColor) { + this.backgroundStaleTintColor = backgroundStaleTintColor; + return this; + } + + /** + * Sets the base elevation of this view, in pixels. + * References style attribute R.styleable#LocationComponent_elevation + * + * @param elevation the elevation currently set for the location icon + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder elevation(float elevation) { + this.elevation = elevation; + return this; + } + + /** + * Enable or disable to stale state mode. This mode indicates to the user that the location + * being displayed on the map hasn't been updated in a specific amount of time. + * References style attribute R.styleable#LocationComponent_enableStaleState + * + * @param enabled whether the stale state mode is enabled or not + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder enableStaleState(boolean enabled) { + this.enableStaleState = enabled; + return this; + } + + /** + * Set the timeout before the location icon becomes stale. The timer begins approximately when a + * new location update comes in and using this defined time, if an update hasn't occurred by the + * end, the location is considered stale. + * References style attribute R.styleable#LocationComponent_staleStateTimeout + * + * @param timeout the duration in milliseconds which it should take before the location is + * considered stale + * @return this builder for chaining options together + */ + @NonNull + public LocationComponentOptions.Builder staleStateTimeout(long timeout) { + this.staleStateTimeout = timeout; + return this; + } + + /** + * Sets the distance from the edges of the map view’s frame to the edges of the map + * view’s logical viewport. + *

+ *

+ * When the value of this property is equal to {0,0,0,0}, viewport + * properties such as `centerCoordinate` assume a viewport that matches the map + * view’s frame. Otherwise, those properties are inset, excluding part of the + * frame from the viewport. For instance, if the only the top edge is inset, the + * map center is effectively shifted downward. + *

+ * + * @param padding The margins for the map in pixels (left, top, right, bottom). + * @deprecated Use {@link CameraPosition.Builder#padding(double, double, double, double)} + * or {@link CameraUpdateFactory#paddingTo(double, double, double, double)} instead. + */ + @NonNull + @Deprecated + public LocationComponentOptions.Builder padding(@Nullable int[] padding) { + if (padding == null) { + throw new NullPointerException("Null padding"); + } + this.padding = padding; + return this; + } + + /** + * Sets the scale factor of the location icon when the map is zoomed in. + * Scaling is linear and the new pixel size of the image will be the original pixel size multiplied by the argument. + *

+ * Set both this and {@link #minZoomIconScale(float)} to 1f to disable location icon scaling. + *

+ * Scaling is based on the maps minimum and maximum zoom levels in time of component's style application. + * + * @param maxZoomIconScale icon scale factor + */ + @NonNull + public LocationComponentOptions.Builder maxZoomIconScale(float maxZoomIconScale) { + this.maxZoomIconScale = maxZoomIconScale; + return this; + } + + /** + * Sets the scale factor of the location icon when the map is zoomed out. + * Scaling is linear and the new pixel size of the image will be the original pixel size multiplied by the argument. + *

+ * Set both this and {@link #maxZoomIconScale(float)} to 1f to disable location icon scaling. + *

+ * Scaling is based on the maps minimum and maximum zoom levels in time of component's style application. + * + * @param minZoomIconScale icon scale factor + */ + @NonNull + public LocationComponentOptions.Builder minZoomIconScale(float minZoomIconScale) { + this.minZoomIconScale = minZoomIconScale; + return this; + } + + /** + * Set whether gesture threshold should be adjusted when camera is in one of the tracking modes. + * This will adjust the focal point and increase thresholds to enable camera manipulation, + * like zooming in and out, without breaking tracking. + *

+ * Note: This can overwrite some of the gesture thresholds + * and the custom {@link com.mapbox.android.gestures.AndroidGesturesManager} that was set with + * {@link com.mapbox.mapboxsdk.maps.MapboxMap#setGesturesManager(AndroidGesturesManager, boolean, boolean)}. + * + * @param trackingGesturesManagement true if gestures should be adjusted when in one of the camera tracking modes, + * false otherwise + * @see Builder#trackingInitialMoveThreshold(float) + * @see Builder#trackingMultiFingerMoveThreshold(float) + */ + @NonNull + public LocationComponentOptions.Builder trackingGesturesManagement(boolean trackingGesturesManagement) { + this.trackingGesturesManagement = trackingGesturesManagement; + return this; + } + + /** + * Sets minimum single pointer movement (map pan) in pixels required to break camera tracking. + * + * @param moveThreshold the minimum movement + */ + @NonNull + public LocationComponentOptions.Builder trackingInitialMoveThreshold(float moveThreshold) { + this.trackingInitialMoveThreshold = moveThreshold; + return this; + } + + /** + * Sets minimum multi pointer movement (map pan) in pixels required to break camera tracking + * (for example during scale gesture). + * + * @param moveThreshold the minimum movement + */ + @NonNull + public LocationComponentOptions.Builder trackingMultiFingerMoveThreshold(float moveThreshold) { + this.trackingMultiFingerMoveThreshold = moveThreshold; + return this; + } + + /** + * Sets the id of the layer that's referenced when placing the component on the map using + * {@link com.mapbox.mapboxsdk.maps.Style#addLayerAbove(Layer, String)}. + *

+ * The component is going to placed directly above this layer. + * + * @param layerAbove the id of the layer the component is going to placed directly above. + */ + @NonNull + public LocationComponentOptions.Builder layerAbove(String layerAbove) { + this.layerAbove = layerAbove; + return this; + } + + /** + * Sets the id of the layer that's referenced when placing the component on the map using + * {@link com.mapbox.mapboxsdk.maps.Style#addLayerBelow(Layer, String)}. + *

+ * The component is going to placed directly below this layer. + * + * @param layerBelow the id of the layer the component is going to placed directly below. + */ + @NonNull + public LocationComponentOptions.Builder layerBelow(String layerBelow) { + this.layerBelow = layerBelow; + return this; + } + + /** + * Sets the tracking animation duration multiplier. + * + * @param trackingAnimationDurationMultiplier the tracking animation duration multiplier + */ + @NonNull + public LocationComponentOptions.Builder trackingAnimationDurationMultiplier( + float trackingAnimationDurationMultiplier) { + this.trackingAnimationDurationMultiplier = trackingAnimationDurationMultiplier; + return this; + } + + /** + * Enable or disable smooth animation of compass values for {@link com.mapbox.mapboxsdk.location.modes.CameraMode} + * and {@link com.mapbox.mapboxsdk.location.modes.RenderMode}. + * + * @return whether smooth compass animation is enabled + */ + public LocationComponentOptions.Builder compassAnimationEnabled(Boolean compassAnimationEnabled) { + this.compassAnimationEnabled = compassAnimationEnabled; + return this; + } + + /** + * Enable or disable smooth animation of the accuracy circle around the user's position. + * + * @return whether smooth animation of the accuracy circle is enabled + */ + public Builder accuracyAnimationEnabled(Boolean accuracyAnimationEnabled) { + this.accuracyAnimationEnabled = accuracyAnimationEnabled; + return this; + } + + @Nullable + LocationComponentOptions autoBuild() { + String missing = ""; + if (this.accuracyAlpha == null) { + missing += " accuracyAlpha"; + } + if (this.accuracyColor == null) { + missing += " accuracyColor"; + } + if (this.backgroundDrawableStale == null) { + missing += " backgroundDrawableStale"; + } + if (this.foregroundDrawableStale == null) { + missing += " foregroundDrawableStale"; + } + if (this.gpsDrawable == null) { + missing += " gpsDrawable"; + } + if (this.foregroundDrawable == null) { + missing += " foregroundDrawable"; + } + if (this.backgroundDrawable == null) { + missing += " backgroundDrawable"; + } + if (this.bearingDrawable == null) { + missing += " bearingDrawable"; + } + if (this.elevation == null) { + missing += " elevation"; + } + if (this.enableStaleState == null) { + missing += " enableStaleState"; + } + if (this.staleStateTimeout == null) { + missing += " staleStateTimeout"; + } + if (this.padding == null) { + missing += " padding"; + } + if (this.maxZoomIconScale == null) { + missing += " maxZoomIconScale"; + } + if (this.minZoomIconScale == null) { + missing += " minZoomIconScale"; + } + if (this.trackingGesturesManagement == null) { + missing += " trackingGesturesManagement"; + } + if (this.trackingInitialMoveThreshold == null) { + missing += " trackingInitialMoveThreshold"; + } + if (this.trackingMultiFingerMoveThreshold == null) { + missing += " trackingMultiFingerMoveThreshold"; + } + if (this.trackingAnimationDurationMultiplier == null) { + missing += " trackingAnimationDurationMultiplier"; + } + if (!missing.isEmpty()) { + throw new IllegalStateException("Missing required properties:" + missing); + } + return new LocationComponentOptions( + this.accuracyAlpha, + this.accuracyColor, + this.backgroundDrawableStale, + this.backgroundStaleName, + this.foregroundDrawableStale, + this.foregroundStaleName, + this.gpsDrawable, + this.gpsName, + this.foregroundDrawable, + this.foregroundName, + this.backgroundDrawable, + this.backgroundName, + this.bearingDrawable, + this.bearingName, + this.bearingTintColor, + this.foregroundTintColor, + this.backgroundTintColor, + this.foregroundStaleTintColor, + this.backgroundStaleTintColor, + this.elevation, + this.enableStaleState, + this.staleStateTimeout, + this.padding, + this.maxZoomIconScale, + this.minZoomIconScale, + trackingGesturesManagement, + this.trackingInitialMoveThreshold, + this.trackingMultiFingerMoveThreshold, + this.layerAbove, + this.layerBelow, + this.trackingAnimationDurationMultiplier, + this.compassAnimationEnabled, + this.accuracyAnimationEnabled); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentPositionManager.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentPositionManager.java new file mode 100644 index 000000000..f2e4d0bb4 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentPositionManager.java @@ -0,0 +1,48 @@ +package com.mapbox.mapboxsdk.location; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.mapbox.mapboxsdk.maps.Style; +import com.mapbox.mapboxsdk.style.layers.Layer; + +class LocationComponentPositionManager { + + @NonNull + private final Style style; + + @Nullable + private String layerAbove; + + @Nullable + private String layerBelow; + + LocationComponentPositionManager(@NonNull Style style, @Nullable String layerAbove, @Nullable String layerBelow) { + this.style = style; + this.layerAbove = layerAbove; + this.layerBelow = layerBelow; + } + + /** + * Returns true whenever layer above/below configuration has changed and requires re-layout. + */ + boolean update(@Nullable String layerAbove, @Nullable String layerBelow) { + boolean requiresUpdate = + !(this.layerAbove == layerAbove || (this.layerAbove != null && this.layerAbove.equals(layerAbove))) + || !(this.layerBelow == layerBelow || (this.layerBelow != null && this.layerBelow.equals(layerBelow))); + + this.layerAbove = layerAbove; + this.layerBelow = layerBelow; + return requiresUpdate; + } + + void addLayerToMap(@NonNull Layer layer) { + if (layerAbove != null) { + style.addLayerAbove(layer, layerAbove); + } else if (layerBelow != null) { + style.addLayerBelow(layer, layerBelow); + } else { + style.addLayer(layer); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java new file mode 100644 index 000000000..54f8ee6d1 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java @@ -0,0 +1,464 @@ +package com.mapbox.mapboxsdk.location; + +import android.graphics.Bitmap; +import android.graphics.PointF; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.mapbox.geojson.Feature; +import com.mapbox.geojson.Point; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.location.modes.RenderMode; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.Style; +import com.mapbox.mapboxsdk.style.layers.Layer; +import com.mapbox.mapboxsdk.style.layers.SymbolLayer; +import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.ACCURACY_LAYER; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_LAYER; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_STALE_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BEARING_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BEARING_LAYER; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_LAYER; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_STALE_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.LOCATION_SOURCE; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_ALPHA; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_COLOR; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_RADIUS; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BACKGROUND_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BACKGROUND_STALE_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_BEARING_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_COMPASS_BEARING; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON_OFFSET; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_STALE_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_GPS_BEARING; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_LOCATION_STALE; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_SHADOW_ICON_OFFSET; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_LAYER; +import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate; +import static com.mapbox.mapboxsdk.style.expressions.Expression.linear; +import static com.mapbox.mapboxsdk.style.expressions.Expression.stop; +import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom; +import static com.mapbox.mapboxsdk.style.layers.Property.NONE; +import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility; +import static com.mapbox.mapboxsdk.utils.ColorUtils.colorToRgbaString; + +final class LocationLayerController { + + @RenderMode.Mode + private int renderMode; + + private final MapboxMap mapboxMap; + private Style style; + private final LayerSourceProvider layerSourceProvider; + private final LayerBitmapProvider bitmapProvider; + private LocationComponentOptions options; + private final OnRenderModeChangedListener internalRenderModeChangedListener; + + @VisibleForTesting + final Set layerSet = new HashSet<>(); + private Feature locationFeature; + private GeoJsonSource locationSource; + + private boolean isHidden = true; + + private LocationComponentPositionManager positionManager; + + LocationLayerController(MapboxMap mapboxMap, Style style, LayerSourceProvider layerSourceProvider, + LayerFeatureProvider featureProvider, LayerBitmapProvider bitmapProvider, + @NonNull LocationComponentOptions options, + @NonNull OnRenderModeChangedListener internalRenderModeChangedListener) { + this.mapboxMap = mapboxMap; + this.style = style; + this.layerSourceProvider = layerSourceProvider; + this.bitmapProvider = bitmapProvider; + this.locationFeature = featureProvider.generateLocationFeature(locationFeature, options); + this.internalRenderModeChangedListener = internalRenderModeChangedListener; + initializeComponents(style, options); + } + + void initializeComponents(Style style, LocationComponentOptions options) { + this.style = style; + this.positionManager = new LocationComponentPositionManager(style, options.layerAbove(), options.layerBelow()); + addLocationSource(); + addLayers(); + applyStyle(options); + + if (isHidden) { + hide(); + } else { + show(); + } + } + + void applyStyle(@NonNull LocationComponentOptions options) { + if (positionManager.update(options.layerAbove(), options.layerBelow())) { + removeLayers(); + addLayers(); + if (isHidden) { + hide(); + } + } + + this.options = options; + + float elevation = options.elevation(); + // Only add icon elevation if the values greater than 0. + if (elevation > 0) { + styleShadow(options); + } + styleForeground(options); + styleBackground(options); + styleBearing(options); + styleAccuracy(options.accuracyAlpha(), options.accuracyColor()); + styleScaling(options); + determineIconsSource(options); + + if (!isHidden) { + show(); + } + } + + void setRenderMode(@RenderMode.Mode int renderMode) { + if (this.renderMode == renderMode) { + return; + } + this.renderMode = renderMode; + + styleForeground(options); + determineIconsSource(options); + if (!isHidden) { + show(); + } + internalRenderModeChangedListener.onRenderModeChanged(renderMode); + } + + int getRenderMode() { + return renderMode; + } + + // + // Layer action + // + + void show() { + isHidden = false; + boolean isStale = locationFeature.getBooleanProperty(PROPERTY_LOCATION_STALE); + switch (renderMode) { + case RenderMode.NORMAL: + setLayerVisibility(SHADOW_LAYER, true); + setLayerVisibility(FOREGROUND_LAYER, true); + setLayerVisibility(BACKGROUND_LAYER, true); + setLayerVisibility(ACCURACY_LAYER, !isStale); + setLayerVisibility(BEARING_LAYER, false); + break; + case RenderMode.COMPASS: + setLayerVisibility(SHADOW_LAYER, true); + setLayerVisibility(FOREGROUND_LAYER, true); + setLayerVisibility(BACKGROUND_LAYER, true); + setLayerVisibility(ACCURACY_LAYER, !isStale); + setLayerVisibility(BEARING_LAYER, true); + break; + case RenderMode.GPS: + setLayerVisibility(SHADOW_LAYER, false); + setLayerVisibility(FOREGROUND_LAYER, true); + setLayerVisibility(BACKGROUND_LAYER, true); + setLayerVisibility(ACCURACY_LAYER, false); + setLayerVisibility(BEARING_LAYER, false); + break; + default: + break; + } + } + + void hide() { + isHidden = true; + for (String layerId : layerSet) { + setLayerVisibility(layerId, false); + } + } + + void updateForegroundOffset(double tilt) { + JsonArray foregroundJsonArray = new JsonArray(); + foregroundJsonArray.add(0f); + foregroundJsonArray.add((float) (-0.05 * tilt)); + locationFeature.addProperty(PROPERTY_FOREGROUND_ICON_OFFSET, foregroundJsonArray); + + JsonArray backgroundJsonArray = new JsonArray(); + backgroundJsonArray.add(0f); + backgroundJsonArray.add((float) (0.05 * tilt)); + locationFeature.addProperty(PROPERTY_SHADOW_ICON_OFFSET, backgroundJsonArray); + + refreshSource(); + } + + void updateForegroundBearing(float bearing) { + if (renderMode != RenderMode.GPS) { + setBearingProperty(PROPERTY_GPS_BEARING, bearing); + } + } + + boolean isHidden() { + return isHidden; + } + + boolean isConsumingCompass() { + return renderMode == RenderMode.COMPASS; + } + + private void setLayerVisibility(@NonNull String layerId, boolean visible) { + Layer layer = style.getLayer(layerId); + if (layer != null) { + String targetVisibility = visible ? VISIBLE : NONE; + if (!layer.getVisibility().value.equals(targetVisibility)) { + layer.setProperties(visibility(visible ? VISIBLE : NONE)); + } + } + } + + private void addLayers() { + // positions the top-most reference layer + Layer layer = layerSourceProvider.generateLayer(BEARING_LAYER); + positionManager.addLayerToMap(layer); + layerSet.add(layer.getId()); + + // adds remaining layers while keeping the order + addSymbolLayer(FOREGROUND_LAYER, BEARING_LAYER); + addSymbolLayer(BACKGROUND_LAYER, FOREGROUND_LAYER); + addSymbolLayer(SHADOW_LAYER, BACKGROUND_LAYER); + addAccuracyLayer(); + } + + private void addSymbolLayer(@NonNull String layerId, @NonNull String beforeLayerId) { + Layer layer = layerSourceProvider.generateLayer(layerId); + addLayerToMap(layer, beforeLayerId); + } + + private void addAccuracyLayer() { + Layer accuracyLayer = layerSourceProvider.generateAccuracyLayer(); + addLayerToMap(accuracyLayer, BACKGROUND_LAYER); + } + + private void addLayerToMap(Layer layer, @NonNull String idBelowLayer) { + style.addLayerBelow(layer, idBelowLayer); + layerSet.add(layer.getId()); + } + + private void removeLayers() { + for (String layerId : layerSet) { + style.removeLayer(layerId); + } + layerSet.clear(); + } + + private void setBearingProperty(@NonNull String propertyId, float bearing) { + locationFeature.addNumberProperty(propertyId, bearing); + refreshSource(); + } + + private void updateAccuracyRadius(float accuracy) { + locationFeature.addNumberProperty(PROPERTY_ACCURACY_RADIUS, accuracy); + refreshSource(); + } + + // + // Source actions + // + + private void addLocationSource() { + locationSource = layerSourceProvider.generateSource(locationFeature); + style.addSource(locationSource); + } + + private void refreshSource() { + GeoJsonSource source = style.getSourceAs(LOCATION_SOURCE); + if (source != null) { + locationSource.setGeoJson(locationFeature); + } + } + + private void setLocationPoint(Point locationPoint) { + JsonObject properties = locationFeature.properties(); + if (properties != null) { + locationFeature = Feature.fromGeometry(locationPoint, properties); + refreshSource(); + } + } + + // + // Styling + // + + private void styleBackground(LocationComponentOptions options) { + Bitmap backgroundBitmap = bitmapProvider.generateBitmap( + options.backgroundDrawable(), options.backgroundTintColor() + ); + Bitmap backgroundStaleBitmap = bitmapProvider.generateBitmap( + options.backgroundDrawableStale(), options.backgroundStaleTintColor() + ); + style.addImage(BACKGROUND_ICON, backgroundBitmap); + style.addImage(BACKGROUND_STALE_ICON, backgroundStaleBitmap); + } + + private void styleShadow(@NonNull LocationComponentOptions options) { + style.addImage(SHADOW_ICON, bitmapProvider.generateShadowBitmap(options)); + } + + private void styleBearing(LocationComponentOptions options) { + Bitmap bearingBitmap = bitmapProvider.generateBitmap(options.bearingDrawable(), options.bearingTintColor()); + style.addImage(BEARING_ICON, bearingBitmap); + } + + private void styleAccuracy(float accuracyAlpha, @ColorInt int accuracyColor) { + locationFeature.addNumberProperty(PROPERTY_ACCURACY_ALPHA, accuracyAlpha); + locationFeature.addStringProperty(PROPERTY_ACCURACY_COLOR, colorToRgbaString(accuracyColor)); + refreshSource(); + } + + private void styleForeground(LocationComponentOptions options) { + Bitmap foregroundBitmap = bitmapProvider.generateBitmap( + options.foregroundDrawable(), options.foregroundTintColor() + ); + Bitmap foregroundBitmapStale = bitmapProvider.generateBitmap( + options.foregroundDrawableStale(), options.foregroundStaleTintColor() + ); + if (renderMode == RenderMode.GPS) { + foregroundBitmap = bitmapProvider.generateBitmap( + options.gpsDrawable(), options.foregroundTintColor() + ); + foregroundBitmapStale = bitmapProvider.generateBitmap( + options.gpsDrawable(), options.foregroundStaleTintColor() + ); + } + style.addImage(FOREGROUND_ICON, foregroundBitmap); + style.addImage(FOREGROUND_STALE_ICON, foregroundBitmapStale); + } + + private void styleScaling(@NonNull LocationComponentOptions options) { + for (String layerId : layerSet) { + Layer layer = style.getLayer(layerId); + if (layer instanceof SymbolLayer) { + layer.setProperties( + iconSize( + interpolate(linear(), zoom(), + stop(mapboxMap.getMinZoomLevel(), options.minZoomIconScale()), + stop(mapboxMap.getMaxZoomLevel(), options.maxZoomIconScale()) + ) + ) + ); + } + } + } + + private void determineIconsSource(LocationComponentOptions options) { + String foregroundIconString = buildIconString( + renderMode == RenderMode.GPS ? options.gpsName() : options.foregroundName(), FOREGROUND_ICON); + String foregroundStaleIconString = buildIconString(options.foregroundStaleName(), FOREGROUND_STALE_ICON); + String backgroundIconString = buildIconString(options.backgroundName(), BACKGROUND_ICON); + String backgroundStaleIconString = buildIconString(options.backgroundStaleName(), BACKGROUND_STALE_ICON); + String bearingIconString = buildIconString(options.bearingName(), BEARING_ICON); + + locationFeature.addStringProperty(PROPERTY_FOREGROUND_ICON, foregroundIconString); + locationFeature.addStringProperty(PROPERTY_BACKGROUND_ICON, backgroundIconString); + locationFeature.addStringProperty(PROPERTY_FOREGROUND_STALE_ICON, foregroundStaleIconString); + locationFeature.addStringProperty(PROPERTY_BACKGROUND_STALE_ICON, backgroundStaleIconString); + locationFeature.addStringProperty(PROPERTY_BEARING_ICON, bearingIconString); + refreshSource(); + } + + @NonNull + private String buildIconString(@Nullable String bitmapName, @NonNull String drawableName) { + if (bitmapName != null) { + return bitmapName; + } + return drawableName; + } + + void setLocationsStale(boolean isStale) { + locationFeature.addBooleanProperty(PROPERTY_LOCATION_STALE, isStale); + refreshSource(); + if (renderMode != RenderMode.GPS) { + setLayerVisibility(ACCURACY_LAYER, !isStale); + } + } + + // + // Map click event + // + + boolean onMapClick(@NonNull LatLng point) { + PointF screenLoc = mapboxMap.getProjection().toScreenLocation(point); + List features = mapboxMap.queryRenderedFeatures(screenLoc, + BACKGROUND_LAYER, + FOREGROUND_LAYER, + BEARING_LAYER + ); + return !features.isEmpty(); + } + + private final MapboxAnimator.AnimationsValueChangeListener latLngValueListener = + new MapboxAnimator.AnimationsValueChangeListener() { + @Override + public void onNewAnimationValue(LatLng value) { + Point point = Point.fromLngLat(value.getLongitude(), value.getLatitude()); + setLocationPoint(point); + } + }; + + private final MapboxAnimator.AnimationsValueChangeListener gpsBearingValueListener = + new MapboxAnimator.AnimationsValueChangeListener() { + @Override + public void onNewAnimationValue(Float value) { + setBearingProperty(PROPERTY_GPS_BEARING, value); + } + }; + + private final MapboxAnimator.AnimationsValueChangeListener compassBearingValueListener = + new MapboxAnimator.AnimationsValueChangeListener() { + @Override + public void onNewAnimationValue(Float value) { + setBearingProperty(PROPERTY_COMPASS_BEARING, value); + } + }; + + private final MapboxAnimator.AnimationsValueChangeListener accuracyValueListener = + new MapboxAnimator.AnimationsValueChangeListener() { + @Override + public void onNewAnimationValue(Float value) { + updateAccuracyRadius(value); + } + }; + + Set getAnimationListeners() { + Set holders = new HashSet<>(); + holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_LAYER_LATLNG, latLngValueListener)); + + if (renderMode == RenderMode.GPS) { + holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_LAYER_GPS_BEARING, gpsBearingValueListener)); + } else if (renderMode == RenderMode.COMPASS) { + holders.add( + new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_LAYER_COMPASS_BEARING, compassBearingValueListener)); + } + + if (renderMode == RenderMode.COMPASS || renderMode == RenderMode.NORMAL) { + holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_LAYER_ACCURACY, accuracyValueListener)); + } + + return holders; + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java new file mode 100644 index 000000000..dff7369cd --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java @@ -0,0 +1,95 @@ +package com.mapbox.mapboxsdk.location; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TypeEvaluator; +import android.animation.ValueAnimator; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Abstract class for all of the location component animators. + * + * @param Data type that will be animated. + */ +abstract class MapboxAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener { + @Retention(RetentionPolicy.SOURCE) + @IntDef( { + ANIMATOR_LAYER_LATLNG, + ANIMATOR_CAMERA_LATLNG, + ANIMATOR_LAYER_GPS_BEARING, + ANIMATOR_LAYER_COMPASS_BEARING, + ANIMATOR_CAMERA_GPS_BEARING, + ANIMATOR_CAMERA_COMPASS_BEARING, + ANIMATOR_LAYER_ACCURACY, + ANIMATOR_ZOOM, + ANIMATOR_TILT + }) + @interface Type { + } + + static final int ANIMATOR_LAYER_LATLNG = 0; + static final int ANIMATOR_CAMERA_LATLNG = 1; + static final int ANIMATOR_LAYER_GPS_BEARING = 2; + static final int ANIMATOR_LAYER_COMPASS_BEARING = 3; + static final int ANIMATOR_CAMERA_GPS_BEARING = 4; + static final int ANIMATOR_CAMERA_COMPASS_BEARING = 5; + static final int ANIMATOR_LAYER_ACCURACY = 6; + static final int ANIMATOR_ZOOM = 7; + static final int ANIMATOR_TILT = 8; + + private final AnimationsValueChangeListener updateListener; + private final K target; + private K animatedValue; + + private final double minUpdateInterval; + private long timeElapsed; + + MapboxAnimator(@NonNull K previous, @NonNull K target, @NonNull AnimationsValueChangeListener updateListener, + int maxAnimationFps) { + minUpdateInterval = 1E9 / maxAnimationFps; + setObjectValues(previous, target); + setEvaluator(provideEvaluator()); + this.updateListener = updateListener; + this.target = target; + addUpdateListener(this); + addListener(new AnimatorListener()); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + animatedValue = (K) animation.getAnimatedValue(); + + long currentTime = System.nanoTime(); + if ((currentTime - timeElapsed) < minUpdateInterval) { + return; + } + + postUpdates(); + timeElapsed = currentTime; + } + + private class AnimatorListener extends AnimatorListenerAdapter { + @Override + public void onAnimationEnd(Animator animation) { + postUpdates(); + } + } + + private void postUpdates() { + updateListener.onNewAnimationValue(animatedValue); + } + + K getTarget() { + return target; + } + + abstract TypeEvaluator provideEvaluator(); + + interface AnimationsValueChangeListener { + void onNewAnimationValue(K value); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java new file mode 100644 index 000000000..938f4ec74 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java @@ -0,0 +1,40 @@ +package com.mapbox.mapboxsdk.location; + +import android.support.annotation.Nullable; + +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.maps.MapboxMap; + +final class MapboxAnimatorProvider { + + private static MapboxAnimatorProvider INSTANCE; + + private MapboxAnimatorProvider() { + // private constructor + } + + public static MapboxAnimatorProvider getInstance() { + if (INSTANCE == null) { + INSTANCE = new MapboxAnimatorProvider(); + } + return INSTANCE; + } + + MapboxLatLngAnimator latLngAnimator(LatLng previous, LatLng target, + MapboxAnimator.AnimationsValueChangeListener updateListener, + int maxAnimationFps) { + return new MapboxLatLngAnimator(previous, target, updateListener, maxAnimationFps); + } + + MapboxFloatAnimator floatAnimator(Float previous, Float target, + MapboxAnimator.AnimationsValueChangeListener updateListener, + int maxAnimationFps) { + return new MapboxFloatAnimator(previous, target, updateListener, maxAnimationFps); + } + + MapboxCameraAnimatorAdapter cameraAnimator(Float previous, Float target, + MapboxAnimator.AnimationsValueChangeListener updateListener, + @Nullable MapboxMap.CancelableCallback cancelableCallback) { + return new MapboxCameraAnimatorAdapter(previous, target, updateListener, cancelableCallback); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java new file mode 100644 index 000000000..1d09f8ae7 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java @@ -0,0 +1,32 @@ +package com.mapbox.mapboxsdk.location; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.support.annotation.NonNull; +import android.view.animation.Interpolator; + +import java.util.List; + +class MapboxAnimatorSetProvider { + private static MapboxAnimatorSetProvider instance; + + private MapboxAnimatorSetProvider() { + // private constructor + } + + static MapboxAnimatorSetProvider getInstance() { + if (instance == null) { + instance = new MapboxAnimatorSetProvider(); + } + return instance; + } + + void startAnimation(@NonNull List animators, @NonNull Interpolator interpolator, + long duration) { + AnimatorSet locationAnimatorSet = new AnimatorSet(); + locationAnimatorSet.playTogether(animators); + locationAnimatorSet.setInterpolator(interpolator); + locationAnimatorSet.setDuration(duration); + locationAnimatorSet.start(); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxCameraAnimatorAdapter.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxCameraAnimatorAdapter.java new file mode 100644 index 000000000..24bb82a2d --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxCameraAnimatorAdapter.java @@ -0,0 +1,36 @@ +package com.mapbox.mapboxsdk.location; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.support.annotation.Nullable; + +import com.mapbox.mapboxsdk.maps.MapboxMap; + +class MapboxCameraAnimatorAdapter extends MapboxFloatAnimator { + @Nullable + private final MapboxMap.CancelableCallback cancelableCallback; + + MapboxCameraAnimatorAdapter(Float previous, Float target, + AnimationsValueChangeListener updateListener, + @Nullable MapboxMap.CancelableCallback cancelableCallback) { + super(previous, target, updateListener, Integer.MAX_VALUE); + this.cancelableCallback = cancelableCallback; + addListener(new MapboxAnimatorListener()); + } + + private final class MapboxAnimatorListener extends AnimatorListenerAdapter { + @Override + public void onAnimationCancel(Animator animation) { + if (cancelableCallback != null) { + cancelableCallback.onCancel(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (cancelableCallback != null) { + cancelableCallback.onFinish(); + } + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxFloatAnimator.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxFloatAnimator.java new file mode 100644 index 000000000..d7d518bc8 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxFloatAnimator.java @@ -0,0 +1,17 @@ +package com.mapbox.mapboxsdk.location; + +import android.animation.FloatEvaluator; +import android.animation.TypeEvaluator; +import android.support.annotation.NonNull; + +class MapboxFloatAnimator extends MapboxAnimator { + MapboxFloatAnimator(Float previous, Float target, AnimationsValueChangeListener updateListener, int maxAnimationFps) { + super(previous, target, updateListener, maxAnimationFps); + } + + @NonNull + @Override + TypeEvaluator provideEvaluator() { + return new FloatEvaluator(); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxLatLngAnimator.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxLatLngAnimator.java new file mode 100644 index 000000000..507069114 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxLatLngAnimator.java @@ -0,0 +1,20 @@ +package com.mapbox.mapboxsdk.location; + +import android.animation.TypeEvaluator; +import android.support.annotation.NonNull; + +import com.mapbox.mapboxsdk.geometry.LatLng; + +class MapboxLatLngAnimator extends MapboxAnimator { + + MapboxLatLngAnimator(LatLng previous, LatLng target, AnimationsValueChangeListener updateListener, + int maxAnimationFps) { + super(previous, target, updateListener, maxAnimationFps); + } + + @NonNull + @Override + TypeEvaluator provideEvaluator() { + return new LatLngEvaluator(); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraMoveInvalidateListener.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraMoveInvalidateListener.java new file mode 100644 index 000000000..6594e543f --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraMoveInvalidateListener.java @@ -0,0 +1,7 @@ +package com.mapbox.mapboxsdk.location; + +interface OnCameraMoveInvalidateListener { + + void onInvalidateCameraMove(); + +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraTrackingChangedListener.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraTrackingChangedListener.java new file mode 100644 index 000000000..1ea80d26a --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnCameraTrackingChangedListener.java @@ -0,0 +1,21 @@ +package com.mapbox.mapboxsdk.location; + +import com.mapbox.mapboxsdk.location.modes.CameraMode; + +/** + * Listener that gets invoked when camera tracking state changes. + */ +public interface OnCameraTrackingChangedListener { + /** + * Invoked whenever camera tracking is broken. + * This callback gets invoked just after {@link #onCameraTrackingChanged(int)}, if needed. + */ + void onCameraTrackingDismissed(); + + /** + * Invoked on every {@link CameraMode} change. + * + * @param currentMode current active {@link CameraMode}. + */ + void onCameraTrackingChanged(@CameraMode.Mode int currentMode); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationCameraTransitionListener.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationCameraTransitionListener.java new file mode 100644 index 000000000..824432efc --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationCameraTransitionListener.java @@ -0,0 +1,24 @@ +package com.mapbox.mapboxsdk.location; + +import com.mapbox.mapboxsdk.location.modes.CameraMode; + +/** + * Callback for {@link CameraMode } transition state. + */ +public interface OnLocationCameraTransitionListener { + /** + * Invoked when the camera mode transition animation has been finished. + * + * @param cameraMode camera mode change that initiated the transition + */ + void onLocationCameraTransitionFinished(@CameraMode.Mode int cameraMode); + + /** + * Invoked when the camera mode transition animation has been canceled. + *

+ * The camera mode is set regardless of the cancellation of the transition animation. + * + * @param cameraMode camera mode change that initiated the transition + */ + void onLocationCameraTransitionCanceled(@CameraMode.Mode int cameraMode); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationClickListener.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationClickListener.java new file mode 100644 index 000000000..fdd2aeb81 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationClickListener.java @@ -0,0 +1,14 @@ +package com.mapbox.mapboxsdk.location; + +/** + * The Location Component exposes an API for listening to when the user clicks on the location + * layer icon visible on the map. When this event occurs, the {@link #onLocationComponentClick()} method + * gets invoked. + */ +public interface OnLocationClickListener { + + /** + * Called whenever user clicks on the location layer drawn on the map. + */ + void onLocationComponentClick(); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationLongClickListener.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationLongClickListener.java new file mode 100644 index 000000000..0c34614f3 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationLongClickListener.java @@ -0,0 +1,14 @@ +package com.mapbox.mapboxsdk.location; + +/** + * The Location Component exposes an API for listening to when the user long clicks on the location + * layer icon visible on the map. when this event occurs, the {@link #onLocationComponentLongClick()} method + * gets invoked. + */ +public interface OnLocationLongClickListener { + + /** + * Called whenever user long clicks on the location layer drawn on the map. + */ + void onLocationComponentLongClick(); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationStaleListener.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationStaleListener.java new file mode 100644 index 000000000..6bbe7f34b --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnLocationStaleListener.java @@ -0,0 +1,17 @@ +package com.mapbox.mapboxsdk.location; + +/** + * Listener that can be added as a callback when the last location update + * is considered stale. + *

+ * The time from the last location update that determines if a location update + * is stale or not is provided by {@link LocationComponentOptions#staleStateTimeout()}. + */ +public interface OnLocationStaleListener { + + /** + * Called when the stale state changes. + * @param isStale true if location is stale, false otherwise + */ + void onStaleStateChange(boolean isStale); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnRenderModeChangedListener.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnRenderModeChangedListener.java new file mode 100644 index 000000000..12ac220c5 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/OnRenderModeChangedListener.java @@ -0,0 +1,16 @@ +package com.mapbox.mapboxsdk.location; + +import com.mapbox.mapboxsdk.location.modes.RenderMode; + +/** + * Listener that gets invoked when layer render mode changes. + */ +public interface OnRenderModeChangedListener { + + /** + * Invoked on every {@link RenderMode} change. + * + * @param currentMode current active {@link RenderMode}. + */ + void onRenderModeChanged(@RenderMode.Mode int currentMode); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/StaleStateManager.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/StaleStateManager.java new file mode 100644 index 000000000..74c2e3a73 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/StaleStateManager.java @@ -0,0 +1,99 @@ +package com.mapbox.mapboxsdk.location; + +import android.os.Handler; +import android.os.Message; +import android.support.annotation.NonNull; + +import java.lang.ref.WeakReference; + +/** + * Class controls the location stale state when the {@link android.location.Location} hasn't + * been updated in 'x' amount of time. {@link LocationComponentOptions#staleStateTimeout()} can be used to + * control the amount of time before the location's considered stale. + * {@link LocationComponentOptions#enableStaleState()} is available for disabling this behaviour. + */ +class StaleStateManager { + + private boolean isEnabled; + private final OnLocationStaleListener innerOnLocationStaleListeners; + @NonNull + private final StaleMessageHandler handler; + private boolean isStale = true; + private long delayTime; + + private final int staleStateMessage = 1; + + StaleStateManager(OnLocationStaleListener innerListener, LocationComponentOptions options) { + innerOnLocationStaleListeners = innerListener; + handler = new StaleMessageHandler(this); + isEnabled = options.enableStaleState(); + delayTime = options.staleStateTimeout(); + } + + void setEnabled(boolean enabled) { + if (enabled) { + setState(isStale); + } else if (isEnabled) { + onStop(); + innerOnLocationStaleListeners.onStaleStateChange(false); + } + isEnabled = enabled; + } + + boolean isStale() { + return isStale; + } + + void updateLatestLocationTime() { + setState(false); + postTheCallback(); + } + + void setDelayTime(long delayTime) { + this.delayTime = delayTime; + if (handler.hasMessages(staleStateMessage)) { + postTheCallback(); + } + } + + void onStart() { + if (!isStale) { + postTheCallback(); + } + } + + void onStop() { + handler.removeCallbacksAndMessages(null); + } + + private void postTheCallback() { + handler.removeCallbacksAndMessages(null); + handler.sendEmptyMessageDelayed(staleStateMessage, delayTime); + } + + private void setState(boolean stale) { + if (stale != isStale) { + isStale = stale; + if (isEnabled) { + innerOnLocationStaleListeners.onStaleStateChange(stale); + } + } + } + + private static class StaleMessageHandler extends Handler { + + private final WeakReference managerWeakReference; + + private StaleMessageHandler(StaleStateManager staleStateManager) { + this.managerWeakReference = new WeakReference<>(staleStateManager); + } + + @Override + public void handleMessage(Message msg) { + StaleStateManager manager = managerWeakReference.get(); + if (manager != null) { + manager.setState(true); + } + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/Utils.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/Utils.java new file mode 100644 index 000000000..178183ca0 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/Utils.java @@ -0,0 +1,121 @@ +package com.mapbox.mapboxsdk.location; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.LayerDrawable; +import android.location.Location; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.log.Logger; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.Projection; + +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.INSTANT_LOCATION_TRANSITION_THRESHOLD; + +public final class Utils { + + private static final String TAG = "Mbgl-com.mapbox.mapboxsdk.location.Utils"; + + private Utils() { + // Class should not be initialized + } + + /** + * Util for finding the shortest path from the current rotated degree to the new degree. + * + * @param heading the new position of the rotation + * @param previousHeading the current position of the rotation + * @return the shortest degree of rotation possible + */ + public static float shortestRotation(float heading, float previousHeading) { + double diff = previousHeading - heading; + if (diff > 180.0f) { + heading += 360.0f; + } else if (diff < -180.0f) { + heading -= 360.f; + } + return heading; + } + + /** + * Normalizes an angle to be in the [0, 360] range. + * + * @param angle the provided angle + * @return the normalized angle + */ + public static float normalize(float angle) { + return (angle % 360 + 360) % 360; + } + + static Bitmap generateShadow(Drawable drawable, float elevation) { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + try { + drawable.draw(canvas); + } catch (IllegalArgumentException ex) { + if (ex.getMessage().equals("radius must be > 0") && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Logger.w(TAG, + "Location's shadow gradient drawable has a radius <= 0px, resetting to 1px in order to avoid crashing"); + ensureShadowGradientRadius(drawable); + return generateShadow(drawable, elevation); + } else { + throw ex; + } + } + bitmap = Bitmap.createScaledBitmap(bitmap, + toEven(width + elevation), toEven(height + elevation), false); + return bitmap; + } + + /** + * We need to ensure that the radius of any {@link GradientDrawable} is greater than 0 for API levels < 21. + * + * @see mapbox-gl-native-#15026 + */ + private static void ensureShadowGradientRadius(Drawable drawable) { + if (drawable instanceof GradientDrawable) { + ((GradientDrawable) drawable).setGradientRadius(1); + } else if (drawable instanceof LayerDrawable) { + LayerDrawable layerDrawable = (LayerDrawable) drawable; + for (int i = 0; i < layerDrawable.getNumberOfLayers(); i++) { + Drawable layers = layerDrawable.getDrawable(i); + if (layers instanceof GradientDrawable) { + ((GradientDrawable) layers).setGradientRadius(1); + } + } + } + } + + static float calculateZoomLevelRadius(@NonNull MapboxMap mapboxMap, @Nullable Location location) { + if (location == null) { + return 0; + } + double metersPerPixel = mapboxMap.getProjection().getMetersPerPixelAtLatitude(location.getLatitude()); + return (float) (location.getAccuracy() * (1 / metersPerPixel)); + } + + static boolean immediateAnimation(@NonNull Projection projection, @NonNull LatLng current, @NonNull LatLng target) { + double metersPerPixel = projection.getMetersPerPixelAtLatitude((current.getLatitude() + target.getLatitude()) / 2); + double distance = current.distanceTo(target); + return distance / metersPerPixel > INSTANT_LOCATION_TRANSITION_THRESHOLD; + } + + /** + * Casts the value to an even integer. + */ + private static int toEven(float value) { + int i = (int) (value + .5f); + if (i % 2 == 1) { + return i - 1; + } + return i; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/CameraMode.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/CameraMode.java new file mode 100644 index 000000000..51806a33c --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/CameraMode.java @@ -0,0 +1,66 @@ +package com.mapbox.mapboxsdk.location.modes; + +import android.location.Location; +import android.support.annotation.IntDef; + +import com.mapbox.mapboxsdk.location.LocationComponent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Contains the variety of camera modes which determine how the camera will track + * the user location. + */ +public final class CameraMode { + + private CameraMode() { + // Class should not be initialized + } + + /** + * Determine the camera tracking behavior in the {@link LocationComponent}. + */ + @IntDef( {NONE, NONE_COMPASS, NONE_GPS, TRACKING, TRACKING_COMPASS, TRACKING_GPS, TRACKING_GPS_NORTH}) + @Retention(RetentionPolicy.SOURCE) + public @interface Mode { + } + + /** + * No camera tracking. + */ + public static final int NONE = 0x00000008; + + /** + * Camera does not track location, but does track compass bearing. + */ + public static final int NONE_COMPASS = 0x00000010; + + /** + * Camera does not track location, but does track GPS {@link Location} bearing. + */ + public static final int NONE_GPS = 0x00000016; + + /** + * Camera tracks the user location. + */ + public static final int TRACKING = 0x00000018; + + /** + * Camera tracks the user location, with bearing + * provided by a compass. + */ + public static final int TRACKING_COMPASS = 0x00000020; + + /** + * Camera tracks the user location, with bearing + * provided by a normalized {@link Location#getBearing()}. + */ + public static final int TRACKING_GPS = 0x00000022; + + /** + * Camera tracks the user location, with bearing + * always set to north (0). + */ + public static final int TRACKING_GPS_NORTH = 0x00000024; +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/RenderMode.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/RenderMode.java new file mode 100644 index 000000000..3d20313df --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/RenderMode.java @@ -0,0 +1,45 @@ +package com.mapbox.mapboxsdk.location.modes; + +import android.support.annotation.IntDef; + +import com.mapbox.mapboxsdk.location.CompassEngine; +import com.mapbox.mapboxsdk.location.LocationComponent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Contains the variety of ways the user location can be rendered on the map. + */ +public final class RenderMode { + + private RenderMode() { + // Class should not be initialized + } + + /** + * One of these constants should be used with {@link LocationComponent#setRenderMode(int)}. + * Mode can be switched at anytime by calling the {@code setLocationLayerMode} method passing + * in the new mode you'd like the location layer to be in. + */ + @IntDef( {COMPASS, GPS, NORMAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface Mode { + } + + /** + * Basic tracking is enabled, bearing ignored. + */ + public static final int NORMAL = 0x00000012; + + /** + * Tracking the user location with bearing considered + * from a {@link CompassEngine}. + */ + public static final int COMPASS = 0x00000004; + + /** + * Tracking the user location with bearing considered from {@link android.location.Location}. + */ + public static final int GPS = 0x00000008; +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/package-info.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/package-info.java new file mode 100644 index 000000000..70dd8cf27 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains the Mapbox Location layer component camera and render modes. + */ +package com.mapbox.mapboxsdk.location.modes; \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/package-info.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/package-info.java new file mode 100644 index 000000000..d37eddffc --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains the Mapbox Location layer component. + */ +package com.mapbox.mapboxsdk.location; \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/Logger.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/Logger.java new file mode 100644 index 000000000..b6c4bc872 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/Logger.java @@ -0,0 +1,321 @@ +package com.mapbox.mapboxsdk.log; + +import android.support.annotation.IntDef; +import android.support.annotation.Keep; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + + +/** + * Logger for the Mapbox Maps SDK for Android + *

+ * Default implementation relies on {@link Log}. + * Alternative implementations can be set with {@link #setLoggerDefinition(LoggerDefinition)}. + *

+ */ +@Keep +public final class Logger { + + private static final LoggerDefinition DEFAULT = new LoggerDefinition() { + + @Override + public void v(String tag, String msg) { + Log.v(tag, msg); + } + + @Override + public void v(String tag, String msg, Throwable tr) { + Log.v(tag, msg, tr); + } + + @Override + public void d(String tag, String msg) { + Log.d(tag, msg); + } + + @Override + public void d(String tag, String msg, Throwable tr) { + Log.d(tag, msg, tr); + } + + @Override + public void i(String tag, String msg) { + Log.i(tag, msg); + } + + @Override + public void i(String tag, String msg, Throwable tr) { + Log.i(tag, msg, tr); + } + + @Override + public void w(String tag, String msg) { + Log.w(tag, msg); + } + + @Override + public void w(String tag, String msg, Throwable tr) { + Log.w(tag, msg, tr); + } + + @Override + public void e(String tag, String msg) { + Log.e(tag, msg); + } + + @Override + public void e(String tag, String msg, Throwable tr) { + Log.e(tag, msg, tr); + } + }; + + private static volatile LoggerDefinition logger = DEFAULT; + + @LogLevel + private static int logLevel; + + /** + * Set the verbosity of the Logger. + *

+ * This configuration can be used to have more granular control over which logs are emitted by the + * Mapbox Maps SDK for Android. + *

+ * + * @param logLevel the verbosity level + */ + public static void setVerbosity(@LogLevel int logLevel) { + Logger.logLevel = logLevel; + } + + /** + * Replace the current used logger definition. + * + * @param loggerDefinition the definition of the logger + */ + public static void setLoggerDefinition(LoggerDefinition loggerDefinition) { + logger = loggerDefinition; + } + + /** + * Send a verbose log message. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + public static void v(String tag, String msg) { + if (logLevel <= VERBOSE) { + logger.v(tag, msg); + } + } + + /** + * Send a verbose log message and log the exception. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + public static void v(String tag, String msg, Throwable tr) { + if (logLevel <= VERBOSE) { + logger.v(tag, msg, tr); + } + } + + /** + * Send a debug log message. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + public static void d(String tag, String msg) { + if (logLevel <= DEBUG) { + logger.d(tag, msg); + } + } + + /** + * Send a debug log message and log the exception. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + public static void d(String tag, String msg, Throwable tr) { + if (logLevel <= DEBUG) { + logger.d(tag, msg, tr); + } + } + + /** + * Send an info log message. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + public static void i(String tag, String msg) { + if (logLevel <= INFO) { + logger.i(tag, msg); + } + } + + /** + * Send an info log message and log the exception. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + public static void i(String tag, String msg, Throwable tr) { + if (logLevel <= INFO) { + logger.i(tag, msg, tr); + } + } + + /** + * Send a warning log message. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + public static void w(String tag, String msg) { + if (logLevel <= WARN) { + logger.w(tag, msg); + } + } + + /** + * Send a warning log message and log the exception. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + public static void w(String tag, String msg, Throwable tr) { + if (logLevel <= WARN) { + logger.w(tag, msg, tr); + } + } + + /** + * Send an error log message. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + public static void e(String tag, String msg) { + if (logLevel <= ERROR) { + logger.e(tag, msg); + } + } + + /** + * Send an error log message and log the exception. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + public static void e(String tag, String msg, Throwable tr) { + if (logLevel <= ERROR) { + logger.e(tag, msg, tr); + } + } + + /** + * Send a log message based on severity. + * + * @param severity the log severity + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param message The message you would like logged. + */ + public static void log(int severity, String tag, String message) { + switch (severity) { + case Log.VERBOSE: + Logger.v(tag, message); + break; + case Log.DEBUG: + Logger.d(tag, message); + break; + case Log.INFO: + Logger.i(tag, message); + break; + case Log.WARN: + Logger.w(tag, message); + break; + case Log.ERROR: + Logger.e(tag, message); + break; + default: + throw new UnsupportedOperationException(); + } + } + + /** + * Priority constant for the println method; use Logger.v + *

+ * This log level will print all logs. + *

+ */ + public static final int VERBOSE = Log.VERBOSE; + + /** + * Priority constant for the println method; use Logger.d. + *

+ * This log level will print all logs except verbose. + *

+ */ + public static final int DEBUG = Log.DEBUG; + + /** + * Priority constant for the println method; use Logger.i. + *

+ * This log level will print all logs except verbose and debug. + *

+ */ + public static final int INFO = Log.INFO; + + /** + * Priority constant for the println method; use Logger.w. + *

+ * This log level will print only warn and error logs. + *

+ */ + public static final int WARN = Log.WARN; + + /** + * Priority constant for the println method; use Logger.e. + *

+ * This log level will print only error logs. + *

+ */ + public static final int ERROR = Log.ERROR; + + /** + * Priority constant for the println method. + *

+ * This log level won't print any logs. + *

+ */ + public static final int NONE = 99; + + /** + * Log level indicates which logs are allowed to be emitted by the Mapbox Maps SDK for Android. + */ + @IntDef( {VERBOSE, DEBUG, INFO, WARN, ERROR, NONE}) + @Retention(RetentionPolicy.SOURCE) + public @interface LogLevel { + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/LoggerDefinition.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/LoggerDefinition.java new file mode 100644 index 000000000..60b6eb000 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/LoggerDefinition.java @@ -0,0 +1,102 @@ +package com.mapbox.mapboxsdk.log; + +/** + * Definition of a logger for the Mapbox Maps SDK for Android. + */ +public interface LoggerDefinition { + + /** + * Send a verbose log message. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + void v(String tag, String msg); + + /** + * Send a verbose log message and log the exception. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + void v(String tag, String msg, Throwable tr); + + /** + * Send a debug log message. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + void d(String tag, String msg); + + /** + * Send a debug log message and log the exception. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + void d(String tag, String msg, Throwable tr); + + /** + * Send an info log message. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + void i(String tag, String msg); + + /** + * Send an info log message and log the exception. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + void i(String tag, String msg, Throwable tr); + + /** + * Send a warning log message. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + void w(String tag, String msg); + + /** + * Send a warning log message and log the exception. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + void w(String tag, String msg, Throwable tr); + + /** + * Send an error log message. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + */ + void e(String tag, String msg); + + /** + * Send an error log message and log the exception. + * + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param msg The message you would like logged. + * @param tr An exception to log + */ + void e(String tag, String msg, Throwable tr); +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/package-info.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/package-info.java new file mode 100644 index 000000000..3476ff7cd --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/log/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains the Mapbox Maps Android Log API classes. + */ +package com.mapbox.mapboxsdk.log; \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationContainer.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationContainer.java new file mode 100644 index 000000000..e5bf51279 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationContainer.java @@ -0,0 +1,87 @@ +package com.mapbox.mapboxsdk.maps; + + +import android.support.annotation.NonNull; +import android.support.v4.util.LongSparseArray; + +import com.mapbox.mapboxsdk.annotations.Annotation; + +import java.util.ArrayList; +import java.util.List; + +/** + * Encapsulates {@link Annotation}'s functionality.. + */ +class AnnotationContainer implements Annotations { + + private final NativeMap nativeMap; + private final LongSparseArray annotations; + + AnnotationContainer(NativeMap nativeMap, LongSparseArray annotations) { + this.nativeMap = nativeMap; + this.annotations = annotations; + } + + @Override + public Annotation obtainBy(long id) { + return annotations.get(id); + } + + @NonNull + @Override + public List obtainAll() { + List annotations = new ArrayList<>(); + for (int i = 0; i < this.annotations.size(); i++) { + annotations.add(this.annotations.get(this.annotations.keyAt(i))); + } + return annotations; + } + + @Override + public void removeBy(long id) { + if (nativeMap != null) { + nativeMap.removeAnnotation(id); + } + annotations.remove(id); + } + + @Override + public void removeBy(@NonNull Annotation annotation) { + long id = annotation.getId(); + removeBy(id); + } + + @Override + public void removeBy(@NonNull List annotationList) { + int count = annotationList.size(); + long[] ids = new long[count]; + for (int i = 0; i < count; i++) { + ids[i] = annotationList.get(i).getId(); + } + + removeNativeAnnotations(ids); + + for (long id : ids) { + annotations.remove(id); + } + } + + @Override + public void removeAll() { + int count = annotations.size(); + long[] ids = new long[count]; + for (int i = 0; i < count; i++) { + ids[i] = annotations.keyAt(i); + } + + removeNativeAnnotations(ids); + + annotations.clear(); + } + + private void removeNativeAnnotations(long[] ids) { + if (nativeMap != null) { + nativeMap.removeAnnotations(ids); + } + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationManager.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationManager.java new file mode 100644 index 000000000..8875e7164 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationManager.java @@ -0,0 +1,519 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.Bitmap; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.util.LongSparseArray; +import android.view.View; +import com.mapbox.mapboxsdk.Mapbox; +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.annotations.Annotation; +import com.mapbox.mapboxsdk.annotations.BaseMarkerOptions; +import com.mapbox.mapboxsdk.annotations.Marker; +import com.mapbox.mapboxsdk.annotations.Polygon; +import com.mapbox.mapboxsdk.annotations.PolygonOptions; +import com.mapbox.mapboxsdk.annotations.Polyline; +import com.mapbox.mapboxsdk.annotations.PolylineOptions; +import com.mapbox.mapboxsdk.log.Logger; + +import java.util.ArrayList; +import java.util.List; + +/** + * Responsible for managing and tracking state of Annotations linked to Map. All events related to + * annotations that occur on {@link MapboxMap} are forwarded to this class. + *

+ * Responsible for referencing {@link InfoWindowManager}. + *

+ *

+ * Exposes convenience methods to add/remove/update all subtypes of annotations found in + * com.mapbox.mapboxsdk.annotations. + *

+ */ +class AnnotationManager { + + private static final String TAG = "Mbgl-AnnotationManager"; + + private static final long NO_ANNOTATION_ID = -1; + + @NonNull + private final MapView mapView; + private final IconManager iconManager; + private final InfoWindowManager infoWindowManager = new InfoWindowManager(); + private final LongSparseArray annotationsArray; + private final List selectedMarkers = new ArrayList<>(); + + private MapboxMap mapboxMap; + @Nullable + private MapboxMap.OnMarkerClickListener onMarkerClickListener; + @Nullable + private MapboxMap.OnPolygonClickListener onPolygonClickListener; + @Nullable + private MapboxMap.OnPolylineClickListener onPolylineClickListener; + + private Annotations annotations; + private ShapeAnnotations shapeAnnotations; + private Markers markers; + private Polygons polygons; + private Polylines polylines; + + AnnotationManager(@NonNull MapView mapView, LongSparseArray annotationsArray, + IconManager iconManager, Annotations annotations, Markers markers, Polygons polygons, + Polylines polylines, ShapeAnnotations shapeAnnotations) { + this.mapView = mapView; + this.annotationsArray = annotationsArray; + this.iconManager = iconManager; + this.annotations = annotations; + this.markers = markers; + this.polygons = polygons; + this.polylines = polylines; + this.shapeAnnotations = shapeAnnotations; + } + + // TODO refactor MapboxMap out for Projection and Transform + // Requires removing MapboxMap from Annotations by using Peer model from #6912 + @NonNull + AnnotationManager bind(MapboxMap mapboxMap) { + this.mapboxMap = mapboxMap; + return this; + } + + void update() { + infoWindowManager.update(); + } + + // + // Annotations + // + + Annotation getAnnotation(long id) { + return annotations.obtainBy(id); + } + + List getAnnotations() { + return annotations.obtainAll(); + } + + void removeAnnotation(long id) { + annotations.removeBy(id); + } + + void removeAnnotation(@NonNull Annotation annotation) { + if (annotation instanceof Marker) { + Marker marker = (Marker) annotation; + marker.hideInfoWindow(); + if (selectedMarkers.contains(marker)) { + selectedMarkers.remove(marker); + } + // do icon cleanup + iconManager.iconCleanup(marker.getIcon()); + } + annotations.removeBy(annotation); + } + + void removeAnnotations(@NonNull List annotationList) { + for (Annotation annotation : annotationList) { + if (annotation instanceof Marker) { + Marker marker = (Marker) annotation; + marker.hideInfoWindow(); + if (selectedMarkers.contains(marker)) { + selectedMarkers.remove(marker); + } + iconManager.iconCleanup(marker.getIcon()); + } + } + annotations.removeBy(annotationList); + } + + void removeAnnotations() { + Annotation annotation; + int count = annotationsArray.size(); + long[] ids = new long[count]; + selectedMarkers.clear(); + for (int i = 0; i < count; i++) { + ids[i] = annotationsArray.keyAt(i); + annotation = annotationsArray.get(ids[i]); + if (annotation instanceof Marker) { + Marker marker = (Marker) annotation; + marker.hideInfoWindow(); + iconManager.iconCleanup(marker.getIcon()); + } + } + annotations.removeAll(); + } + + // + // Markers + // + + Marker addMarker(@NonNull BaseMarkerOptions markerOptions, @NonNull MapboxMap mapboxMap) { + return markers.addBy(markerOptions, mapboxMap); + } + + List addMarkers(@NonNull List markerOptionsList, @NonNull MapboxMap mapboxMap) { + return markers.addBy(markerOptionsList, mapboxMap); + } + + void updateMarker(@NonNull Marker updatedMarker, @NonNull MapboxMap mapboxMap) { + if (!isAddedToMap(updatedMarker)) { + logNonAdded(updatedMarker); + return; + } + markers.update(updatedMarker, mapboxMap); + } + + List getMarkers() { + return markers.obtainAll(); + } + + @NonNull + List getMarkersInRect(@NonNull RectF rectangle) { + return markers.obtainAllIn(rectangle); + } + + void reloadMarkers() { + markers.reload(); + } + + // + // Polygons + // + + Polygon addPolygon(@NonNull PolygonOptions polygonOptions, @NonNull MapboxMap mapboxMap) { + return polygons.addBy(polygonOptions, mapboxMap); + } + + List addPolygons(@NonNull List polygonOptionsList, @NonNull MapboxMap mapboxMap) { + return polygons.addBy(polygonOptionsList, mapboxMap); + } + + void updatePolygon(@NonNull Polygon polygon) { + if (!isAddedToMap(polygon)) { + logNonAdded(polygon); + return; + } + polygons.update(polygon); + } + + List getPolygons() { + return polygons.obtainAll(); + } + + // + // Polylines + // + + Polyline addPolyline(@NonNull PolylineOptions polylineOptions, @NonNull MapboxMap mapboxMap) { + return polylines.addBy(polylineOptions, mapboxMap); + } + + List addPolylines(@NonNull List polylineOptionsList, @NonNull MapboxMap mapboxMap) { + return polylines.addBy(polylineOptionsList, mapboxMap); + } + + void updatePolyline(@NonNull Polyline polyline) { + if (!isAddedToMap(polyline)) { + logNonAdded(polyline); + return; + } + polylines.update(polyline); + } + + List getPolylines() { + return polylines.obtainAll(); + } + + // TODO Refactor from here still in progress + void setOnMarkerClickListener(@Nullable MapboxMap.OnMarkerClickListener listener) { + onMarkerClickListener = listener; + } + + void setOnPolygonClickListener(@Nullable MapboxMap.OnPolygonClickListener listener) { + onPolygonClickListener = listener; + } + + void setOnPolylineClickListener(@Nullable MapboxMap.OnPolylineClickListener listener) { + onPolylineClickListener = listener; + } + + void selectMarker(@NonNull Marker marker) { + if (selectedMarkers.contains(marker)) { + return; + } + + // Need to deselect any currently selected annotation first + if (!infoWindowManager.isAllowConcurrentMultipleOpenInfoWindows()) { + deselectMarkers(); + } + + if (infoWindowManager.isInfoWindowValidForMarker(marker) || infoWindowManager.getInfoWindowAdapter() != null) { + infoWindowManager.add(marker.showInfoWindow(mapboxMap, mapView)); + } + + // only add to selected markers if user didn't handle the click event themselves #3176 + selectedMarkers.add(marker); + } + + void deselectMarkers() { + if (selectedMarkers.isEmpty()) { + return; + } + + for (Marker marker : selectedMarkers) { + if (marker != null) { + if (marker.isInfoWindowShown()) { + marker.hideInfoWindow(); + } + } + } + + // Removes all selected markers from the list + selectedMarkers.clear(); + } + + void deselectMarker(@NonNull Marker marker) { + if (!selectedMarkers.contains(marker)) { + return; + } + + if (marker.isInfoWindowShown()) { + marker.hideInfoWindow(); + } + selectedMarkers.remove(marker); + } + + @NonNull + List getSelectedMarkers() { + return selectedMarkers; + } + + @NonNull + InfoWindowManager getInfoWindowManager() { + return infoWindowManager; + } + + void adjustTopOffsetPixels(@NonNull MapboxMap mapboxMap) { + int count = annotationsArray.size(); + for (int i = 0; i < count; i++) { + Annotation annotation = annotationsArray.get(i); + if (annotation instanceof Marker) { + Marker marker = (Marker) annotation; + marker.setTopOffsetPixels( + iconManager.getTopOffsetPixelsForIcon(marker.getIcon())); + } + } + + for (Marker marker : selectedMarkers) { + if (marker.isInfoWindowShown()) { + marker.hideInfoWindow(); + marker.showInfoWindow(mapboxMap, mapView); + } + } + } + + private boolean isAddedToMap(@Nullable Annotation annotation) { + return annotation != null && annotation.getId() != -1 && annotationsArray.indexOfKey(annotation.getId()) > -1; + } + + private void logNonAdded(@NonNull Annotation annotation) { + Logger.w(TAG, String.format( + "Attempting to update non-added %s with value %s", annotation.getClass().getCanonicalName(), annotation) + ); + } + + // + // Click event + // + + boolean onTap(@NonNull PointF tapPoint) { + MarkerHit markerHit = getMarkerHitFromTouchArea(tapPoint); + long markerId = new MarkerHitResolver(mapboxMap).execute(markerHit); + if (markerId != NO_ANNOTATION_ID) { + if (isClickHandledForMarker(markerId)) { + return true; + } + } + + ShapeAnnotationHit shapeAnnotationHit = getShapeAnnotationHitFromTap(tapPoint); + Annotation annotation = new ShapeAnnotationHitResolver(shapeAnnotations).execute(shapeAnnotationHit); + return annotation != null && handleClickForShapeAnnotation(annotation); + } + + private ShapeAnnotationHit getShapeAnnotationHitFromTap(PointF tapPoint) { + float touchTargetSide = Mapbox.getApplicationContext().getResources().getDimension(R.dimen.mapbox_eight_dp); + RectF tapRect = new RectF( + tapPoint.x - touchTargetSide, + tapPoint.y - touchTargetSide, + tapPoint.x + touchTargetSide, + tapPoint.y + touchTargetSide + ); + return new ShapeAnnotationHit(tapRect); + } + + private boolean handleClickForShapeAnnotation(Annotation annotation) { + if (annotation instanceof Polygon && onPolygonClickListener != null) { + onPolygonClickListener.onPolygonClick((Polygon) annotation); + return true; + } else if (annotation instanceof Polyline && onPolylineClickListener != null) { + onPolylineClickListener.onPolylineClick((Polyline) annotation); + return true; + } + return false; + } + + private MarkerHit getMarkerHitFromTouchArea(PointF tapPoint) { + int touchSurfaceWidth = (int) (iconManager.getHighestIconHeight() * 1.5); + int touchSurfaceHeight = (int) (iconManager.getHighestIconWidth() * 1.5); + final RectF tapRect = new RectF(tapPoint.x - touchSurfaceWidth, + tapPoint.y - touchSurfaceHeight, + tapPoint.x + touchSurfaceWidth, + tapPoint.y + touchSurfaceHeight + ); + return new MarkerHit(tapRect, getMarkersInRect(tapRect)); + } + + private boolean isClickHandledForMarker(long markerId) { + Marker marker = (Marker) getAnnotation(markerId); + boolean handledDefaultClick = onClickMarker(marker); + if (!handledDefaultClick) { + toggleMarkerSelectionState(marker); + } + return true; + } + + private boolean onClickMarker(@NonNull Marker marker) { + return onMarkerClickListener != null && onMarkerClickListener.onMarkerClick(marker); + } + + private void toggleMarkerSelectionState(@NonNull Marker marker) { + if (!selectedMarkers.contains(marker)) { + selectMarker(marker); + } else { + deselectMarker(marker); + } + } + + private static class ShapeAnnotationHitResolver { + + private ShapeAnnotations shapeAnnotations; + + ShapeAnnotationHitResolver(ShapeAnnotations shapeAnnotations) { + this.shapeAnnotations = shapeAnnotations; + } + + @Nullable + public Annotation execute(@NonNull ShapeAnnotationHit shapeHit) { + Annotation foundAnnotation = null; + List annotations = shapeAnnotations.obtainAllIn(shapeHit.tapPoint); + if (annotations.size() > 0) { + foundAnnotation = annotations.get(0); + } + return foundAnnotation; + } + } + + private static class MarkerHitResolver { + + @NonNull + private final Projection projection; + private final int minimalTouchSize; + + @Nullable + private View view; + + private Bitmap bitmap; + private int bitmapWidth; + private int bitmapHeight; + private PointF markerLocation; + + @NonNull + private Rect hitRectView = new Rect(); + @NonNull + private RectF hitRectMarker = new RectF(); + @NonNull + private RectF highestSurfaceIntersection = new RectF(); + + private long closestMarkerId = NO_ANNOTATION_ID; + + MarkerHitResolver(@NonNull MapboxMap mapboxMap) { + this.projection = mapboxMap.getProjection(); + this.minimalTouchSize = (int) (32 * Mapbox.getApplicationContext().getResources().getDisplayMetrics().density); + } + + public long execute(@NonNull MarkerHit markerHit) { + resolveForMarkers(markerHit); + return closestMarkerId; + } + + private void resolveForMarkers(MarkerHit markerHit) { + for (Marker marker : markerHit.markers) { + resolveForMarker(markerHit, marker); + } + } + + private void resolveForMarker(@NonNull MarkerHit markerHit, Marker marker) { + markerLocation = projection.toScreenLocation(marker.getPosition()); + bitmap = marker.getIcon().getBitmap(); + + bitmapHeight = bitmap.getHeight(); + if (bitmapHeight < minimalTouchSize) { + bitmapHeight = minimalTouchSize; + } + + bitmapWidth = bitmap.getWidth(); + if (bitmapWidth < minimalTouchSize) { + bitmapWidth = minimalTouchSize; + } + + hitRectMarker.set(0, 0, bitmapWidth, bitmapHeight); + hitRectMarker.offsetTo( + markerLocation.x - bitmapWidth / 2, + markerLocation.y - bitmapHeight / 2 + ); + hitTestMarker(markerHit, marker, hitRectMarker); + } + + private void hitTestMarker(MarkerHit markerHit, @NonNull Marker marker, RectF hitRectMarker) { + if (hitRectMarker.contains(markerHit.getTapPointX(), markerHit.getTapPointY())) { + hitRectMarker.intersect(markerHit.tapRect); + if (isRectangleHighestSurfaceIntersection(hitRectMarker)) { + highestSurfaceIntersection = new RectF(hitRectMarker); + closestMarkerId = marker.getId(); + } + } + } + + private boolean isRectangleHighestSurfaceIntersection(RectF rectF) { + return rectF.width() * rectF.height() > highestSurfaceIntersection.width() * highestSurfaceIntersection.height(); + } + } + + private static class ShapeAnnotationHit { + private final RectF tapPoint; + + ShapeAnnotationHit(RectF tapPoint) { + this.tapPoint = tapPoint; + } + } + + private static class MarkerHit { + private final RectF tapRect; + private final List markers; + + MarkerHit(RectF tapRect, List markers) { + this.tapRect = tapRect; + this.markers = markers; + } + + float getTapPointX() { + return tapRect.centerX(); + } + + float getTapPointY() { + return tapRect.centerY(); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Annotations.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Annotations.java new file mode 100644 index 000000000..ae41cbb0c --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Annotations.java @@ -0,0 +1,25 @@ +package com.mapbox.mapboxsdk.maps; + + +import android.support.annotation.NonNull; + +import com.mapbox.mapboxsdk.annotations.Annotation; + +import java.util.List; + +/** + * Interface that defines convenient methods for working with a {@link Annotation}'s collection. + */ +interface Annotations { + Annotation obtainBy(long id); + + List obtainAll(); + + void removeBy(long id); + + void removeBy(@NonNull Annotation annotation); + + void removeBy(@NonNull List annotationList); + + void removeAll(); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AttributionDialogManager.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AttributionDialogManager.java new file mode 100644 index 000000000..4f523948d --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AttributionDialogManager.java @@ -0,0 +1,208 @@ +package com.mapbox.mapboxsdk.maps; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.Toast; +import com.mapbox.mapboxsdk.MapStrictMode; +import com.mapbox.mapboxsdk.Mapbox; +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.attribution.Attribution; +import com.mapbox.mapboxsdk.attribution.AttributionParser; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.style.sources.Source; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Responsible for managing attribution interactions on the map. + *

+ * When the user clicks the attribution icon, {@link AttributionDialogManager#onClick(View)} will be invoked. + * An attribution dialog will be shown to the user with contents based on the attributions found in the map style. + * Additionally an telemetry option item is shown to configure telemetry settings. + *

+ */ +public class AttributionDialogManager implements View.OnClickListener, DialogInterface.OnClickListener { + + private static final String MAP_FEEDBACK_URL = "https://apps.mapbox.com/feedback"; + private static final String MAP_FEEDBACK_LOCATION_FORMAT = MAP_FEEDBACK_URL + "/#/%f/%f/%d"; + + @NonNull + private final Context context; + @NonNull + private final MapboxMap mapboxMap; + private Set attributionSet; + private AlertDialog dialog; + + public AttributionDialogManager(@NonNull Context context, @NonNull MapboxMap mapboxMap) { + this.context = context; + this.mapboxMap = mapboxMap; + } + + // Called when someone presses the attribution icon on the map + @Override + public void onClick(@NonNull View view) { + attributionSet = new AttributionBuilder(mapboxMap, view.getContext()).build(); + + boolean isActivityFinishing = false; + if (context instanceof Activity) { + isActivityFinishing = ((Activity) context).isFinishing(); + } + + // check is hosting activity isn't finishing + // https://github.com/mapbox/mapbox-gl-native/issues/11238 + if (!isActivityFinishing) { + showAttributionDialog(getAttributionTitles()); + } + } + + protected void showAttributionDialog(@NonNull String[] attributionTitles) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.mapbox_attributionsDialogTitle); + builder.setAdapter(new ArrayAdapter<>(context, R.layout.mapbox_attribution_list_item, attributionTitles), this); + dialog = builder.show(); + } + + private String[] getAttributionTitles() { + List titles = new ArrayList<>(); + for (Attribution attribution : attributionSet) { + titles.add(attribution.getTitle()); + } + return titles.toArray(new String[titles.size()]); + } + + // Called when someone selects an attribution or telemetry settings from the dialog + @Override + public void onClick(DialogInterface dialog, int which) { + if (isLatestEntry(which)) { + showTelemetryDialog(); + } else { + showMapFeedbackWebPage(which); + } + } + + public void onStop() { + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + } + + private boolean isLatestEntry(int attributionKeyIndex) { + return attributionKeyIndex == getAttributionTitles().length - 1; + } + + private void showTelemetryDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.mapbox_attributionTelemetryTitle); + builder.setMessage(R.string.mapbox_attributionTelemetryMessage); + builder.setPositiveButton(R.string.mapbox_attributionTelemetryPositive, new DialogInterface.OnClickListener() { + @Override + public void onClick(@NonNull DialogInterface dialog, int which) { + TelemetryDefinition telemetry = Mapbox.getTelemetry(); + if (telemetry != null) { + telemetry.setUserTelemetryRequestState(true); + } + dialog.cancel(); + } + }); + builder.setNeutralButton(R.string.mapbox_attributionTelemetryNeutral, new DialogInterface.OnClickListener() { + @Override + public void onClick(@NonNull DialogInterface dialog, int which) { + showWebPage(context.getResources().getString(R.string.mapbox_telemetryLink)); + dialog.cancel(); + } + }); + builder.setNegativeButton(R.string.mapbox_attributionTelemetryNegative, new DialogInterface.OnClickListener() { + @Override + public void onClick(@NonNull DialogInterface dialog, int which) { + TelemetryDefinition telemetry = Mapbox.getTelemetry(); + if (telemetry != null) { + telemetry.setUserTelemetryRequestState(false); + } + dialog.cancel(); + } + }); + builder.show(); + } + + private void showMapFeedbackWebPage(int which) { + Attribution[] attributions = attributionSet.toArray(new Attribution[attributionSet.size()]); + String url = attributions[which].getUrl(); + if (url.contains(MAP_FEEDBACK_URL)) { + url = buildMapFeedbackMapUrl(mapboxMap.getCameraPosition()); + } + showWebPage(url); + } + + @NonNull + private String buildMapFeedbackMapUrl(@Nullable CameraPosition cameraPosition) { + // appends current location to the map feedback url if available + return cameraPosition != null ? String.format(Locale.getDefault(), + MAP_FEEDBACK_LOCATION_FORMAT, cameraPosition.target.getLongitude(), cameraPosition.target.getLatitude(), + (int) cameraPosition.zoom) : MAP_FEEDBACK_URL; + } + + private void showWebPage(@NonNull String url) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + context.startActivity(intent); + } catch (ActivityNotFoundException exception) { + // explicitly handling if the device hasn't have a web browser installed. #8899 + Toast.makeText(context, R.string.mapbox_attributionErrorNoBrowser, Toast.LENGTH_LONG).show(); + MapStrictMode.strictModeViolation(exception); + } + } + + private static class AttributionBuilder { + + private final MapboxMap mapboxMap; + @NonNull + private final WeakReference context; + + AttributionBuilder(MapboxMap mapboxMap, Context context) { + this.mapboxMap = mapboxMap; + this.context = new WeakReference<>(context); + } + + private Set build() { + Context context = this.context.get(); + if (context == null) { + return Collections.emptySet(); + } + + List attributions = new ArrayList<>(); + String attribution; + + Style style = mapboxMap.getStyle(); + if (style != null) { + for (Source source : mapboxMap.getStyle().getSources()) { + attribution = source.getAttribution(); + if (!attribution.isEmpty()) { + attributions.add(source.getAttribution()); + } + } + } + + return new AttributionParser.Options(context) + .withCopyrightSign(true) + .withImproveMap(true) + .withTelemetryAttribution(true) + .withAttributionData(attributions.toArray(new String[attributions.size()])) + .build().getAttributions(); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/CameraChangeDispatcher.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/CameraChangeDispatcher.java new file mode 100644 index 000000000..a1fd4e7e3 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/CameraChangeDispatcher.java @@ -0,0 +1,204 @@ +package com.mapbox.mapboxsdk.maps; + +import android.os.Handler; +import android.os.Message; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; + +import java.lang.annotation.Retention; +import java.lang.ref.WeakReference; +import java.util.concurrent.CopyOnWriteArrayList; + +import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraIdleListener; +import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveCanceledListener; +import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveListener; +import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Class responsible for dispatching camera change events to registered listeners. + */ +class CameraChangeDispatcher implements MapboxMap.OnCameraMoveStartedListener, MapboxMap.OnCameraMoveListener, + MapboxMap.OnCameraMoveCanceledListener, OnCameraIdleListener { + + private final CameraChangeHandler handler = new CameraChangeHandler(this); + + private boolean idle = true; + private int moveStartedReason; + + private final CopyOnWriteArrayList onCameraMoveStarted = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList onCameraMoveCanceled = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList onCameraMove = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList onCameraIdle = new CopyOnWriteArrayList<>(); + + @Retention(SOURCE) + @IntDef( {MOVE_STARTED, MOVE, MOVE_CANCELED, IDLE}) + @interface CameraChange { + } + + private static final int MOVE_STARTED = 0; + private static final int MOVE = 1; + private static final int MOVE_CANCELED = 2; + private static final int IDLE = 3; + + @Override + public void onCameraMoveStarted(final int reason) { + moveStartedReason = reason; + handler.scheduleMessage(MOVE_STARTED); + } + + @Override + public void onCameraMove() { + handler.scheduleMessage(MOVE); + } + + @Override + public void onCameraMoveCanceled() { + handler.scheduleMessage(MOVE_CANCELED); + } + + @Override + public void onCameraIdle() { + handler.scheduleMessage(IDLE); + } + + void addOnCameraIdleListener(@NonNull OnCameraIdleListener listener) { + onCameraIdle.add(listener); + } + + void removeOnCameraIdleListener(@NonNull OnCameraIdleListener listener) { + if (onCameraIdle.contains(listener)) { + onCameraIdle.remove(listener); + } + } + + void addOnCameraMoveCancelListener(OnCameraMoveCanceledListener listener) { + onCameraMoveCanceled.add(listener); + } + + void removeOnCameraMoveCancelListener(OnCameraMoveCanceledListener listener) { + if (onCameraMoveCanceled.contains(listener)) { + onCameraMoveCanceled.remove(listener); + } + } + + void addOnCameraMoveStartedListener(OnCameraMoveStartedListener listener) { + onCameraMoveStarted.add(listener); + } + + void removeOnCameraMoveStartedListener(OnCameraMoveStartedListener listener) { + if (onCameraMoveStarted.contains(listener)) { + onCameraMoveStarted.remove(listener); + } + } + + void addOnCameraMoveListener(OnCameraMoveListener listener) { + onCameraMove.add(listener); + } + + void removeOnCameraMoveListener(OnCameraMoveListener listener) { + if (onCameraMove.contains(listener)) { + onCameraMove.remove(listener); + } + } + + private void executeOnCameraMoveStarted() { + if (!idle) { + return; + } + idle = false; + if (!onCameraMoveStarted.isEmpty()) { + for (OnCameraMoveStartedListener cameraMoveStartedListener : onCameraMoveStarted) { + cameraMoveStartedListener.onCameraMoveStarted(moveStartedReason); + } + } + } + + private void executeOnCameraMove() { + if (!onCameraMove.isEmpty() && !idle) { + for (OnCameraMoveListener cameraMoveListener : onCameraMove) { + cameraMoveListener.onCameraMove(); + } + } + } + + private void executeOnCameraMoveCancelled() { + if (!onCameraMoveCanceled.isEmpty() && !idle) { + for (OnCameraMoveCanceledListener cameraMoveCanceledListener : onCameraMoveCanceled) { + cameraMoveCanceledListener.onCameraMoveCanceled(); + } + } + } + + private void executeOnCameraIdle() { + if (idle) { + return; + } + idle = true; + if (!onCameraIdle.isEmpty()) { + for (OnCameraIdleListener cameraIdleListener : onCameraIdle) { + cameraIdleListener.onCameraIdle(); + } + } + } + + void onDestroy() { + handler.removeCallbacksAndMessages(null); + onCameraMoveStarted.clear(); + onCameraMoveCanceled.clear(); + onCameraMove.clear(); + onCameraIdle.clear(); + } + + private static class CameraChangeHandler extends Handler { + + private WeakReference dispatcherWeakReference; + + CameraChangeHandler(CameraChangeDispatcher dispatcher) { + super(); + this.dispatcherWeakReference = new WeakReference<>(dispatcher); + } + + @Override + public void handleMessage(@NonNull Message msg) { + CameraChangeDispatcher dispatcher = dispatcherWeakReference.get(); + if (dispatcher != null) { + switch (msg.what) { + case MOVE_STARTED: + dispatcher.executeOnCameraMoveStarted(); + break; + case MOVE: + dispatcher.executeOnCameraMove(); + break; + case MOVE_CANCELED: + dispatcher.executeOnCameraMoveCancelled(); + break; + case IDLE: + dispatcher.executeOnCameraIdle(); + break; + } + } + } + + void scheduleMessage(@CameraChange int change) { + CameraChangeDispatcher dispatcher = dispatcherWeakReference.get(); + if (dispatcher != null) { + // if there is a movement that is cancelled/stopped and restarted in the same code block + // we can safely assume that the movement will continue, no need for dispatching unnecessary callbacks sequence + if (change == MOVE_STARTED) { + boolean shouldReturn = !dispatcher.idle && (hasMessages(IDLE) || hasMessages(MOVE_CANCELED)); + removeMessages(IDLE); + removeMessages(MOVE_CANCELED); + + if (shouldReturn) { + return; + } + } + + Message message = new Message(); + message.what = change; + this.sendMessage(message); + } + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/FocalPointChangeListener.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/FocalPointChangeListener.java new file mode 100644 index 000000000..aec9a848b --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/FocalPointChangeListener.java @@ -0,0 +1,11 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.PointF; + +/** + * Interface definition of a callback that is invoked when the focal point will change. + */ +public interface FocalPointChangeListener { + + void onFocalPointChanged(PointF pointF); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/IconManager.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/IconManager.java new file mode 100644 index 000000000..3a0824ad1 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/IconManager.java @@ -0,0 +1,155 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.Bitmap; + +import android.support.annotation.NonNull; +import com.mapbox.mapboxsdk.Mapbox; +import com.mapbox.mapboxsdk.annotations.Icon; +import com.mapbox.mapboxsdk.annotations.IconFactory; +import com.mapbox.mapboxsdk.annotations.Marker; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Responsible for managing icons added to the Map. + *

+ * Maintains a {@link List} of {@link Icon} and is responsible for initialising default markers. + *

+ *

+ * Keep track of icons added and the resulting average icon size. This is used internally by our + * gestures detection to calculate the size of a touch target. + *

+ */ +class IconManager { + + private final Map iconMap = new HashMap<>(); + + private NativeMap nativeMap; + private int highestIconWidth; + private int highestIconHeight; + + IconManager(NativeMap nativeMap) { + this.nativeMap = nativeMap; + } + + Icon loadIconForMarker(@NonNull Marker marker) { + Icon icon = marker.getIcon(); + if (icon == null) { + // TODO replace with anchor implementation, we are faking an anchor by adding extra pixels and diving height by 2 + icon = loadDefaultIconForMarker(marker); + } else { + updateHighestIconSize(icon); + } + addIcon(icon); + return icon; + } + + int getTopOffsetPixelsForIcon(@NonNull Icon icon) { + return (int) (nativeMap.getTopOffsetPixelsForAnnotationSymbol(icon.getId()) * nativeMap.getPixelRatio()); + } + + int getHighestIconWidth() { + return highestIconWidth; + } + + int getHighestIconHeight() { + return highestIconHeight; + } + + private Icon loadDefaultIconForMarker(Marker marker) { + Icon icon = IconFactory.getInstance(Mapbox.getApplicationContext()).defaultMarker(); + Bitmap bitmap = icon.getBitmap(); + updateHighestIconSize(bitmap.getWidth(), bitmap.getHeight() / 2); + marker.setIcon(icon); + return icon; + } + + private void addIcon(@NonNull Icon icon) { + addIcon(icon, true); + } + + private void addIcon(@NonNull Icon icon, boolean addIconToMap) { + if (!iconMap.keySet().contains(icon)) { + iconMap.put(icon, 1); + if (addIconToMap) { + loadIcon(icon); + } + } else { + iconMap.put(icon, iconMap.get(icon) + 1); + } + } + + private void updateHighestIconSize(Icon icon) { + updateHighestIconSize(icon.getBitmap()); + } + + private void updateHighestIconSize(Bitmap bitmap) { + updateHighestIconSize(bitmap.getWidth(), bitmap.getHeight()); + } + + private void updateHighestIconSize(int width, int height) { + if (width > highestIconWidth) { + highestIconWidth = width; + } + + if (height > highestIconHeight) { + highestIconHeight = height; + } + } + + private void loadIcon(Icon icon) { + Bitmap bitmap = icon.getBitmap(); + nativeMap.addAnnotationIcon(icon.getId(), + bitmap.getWidth(), + bitmap.getHeight(), + icon.getScale(), + icon.toBytes()); + } + + void reloadIcons() { + for (Icon icon : iconMap.keySet()) { + loadIcon(icon); + } + } + + void ensureIconLoaded(@NonNull Marker marker, @NonNull MapboxMap mapboxMap) { + Icon icon = marker.getIcon(); + if (icon == null) { + icon = loadDefaultIconForMarker(marker); + } + addIcon(icon); + setTopOffsetPixels(marker, mapboxMap, icon); + } + + private void setTopOffsetPixels(Marker marker, @NonNull MapboxMap mapboxMap, @NonNull Icon icon) { + // this seems to be a costly operation according to the profiler so I'm trying to save some calls + Marker previousMarker = marker.getId() != -1 ? (Marker) mapboxMap.getAnnotation(marker.getId()) : null; + if (previousMarker == null || previousMarker.getIcon() == null || previousMarker.getIcon() != marker.getIcon()) { + marker.setTopOffsetPixels(getTopOffsetPixelsForIcon(icon)); + } + } + + void iconCleanup(@NonNull Icon icon) { + Integer refCounter = iconMap.get(icon); + if (refCounter != null) { + refCounter--; + if (refCounter == 0) { + remove(icon); + } else { + updateIconRefCounter(icon, refCounter); + } + } + } + + private void remove(Icon icon) { + nativeMap.removeAnnotationIcon(icon.getId()); + iconMap.remove(icon); + } + + private void updateIconRefCounter(Icon icon, int refCounter) { + iconMap.put(icon, refCounter); + } + +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Image.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Image.java new file mode 100644 index 000000000..a261c2f4b --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Image.java @@ -0,0 +1,22 @@ +package com.mapbox.mapboxsdk.maps; + +import android.support.annotation.Keep; + +@Keep +class Image { + private final byte[] buffer; + private final float pixelRatio; + private final String name; + private final int width; + private final int height; + private final boolean sdf; + + public Image(byte[] buffer, float pixelRatio, String name, int width, int height, boolean sdf) { + this.buffer = buffer; + this.pixelRatio = pixelRatio; + this.name = name; + this.width = width; + this.height = height; + this.sdf = sdf; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/InfoWindowManager.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/InfoWindowManager.java new file mode 100644 index 000000000..434244a7c --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/InfoWindowManager.java @@ -0,0 +1,96 @@ +package com.mapbox.mapboxsdk.maps; + +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.mapbox.mapboxsdk.annotations.InfoWindow; +import com.mapbox.mapboxsdk.annotations.Marker; + +import java.util.ArrayList; +import java.util.List; + +/** + * Responsible for managing InfoWindows shown on the Map. + *

+ * Maintains a {@link List} of opened {@link InfoWindow} and tracks configurations as + * allowConcurrentMultipleInfoWindows which allows to have multiple {@link InfoWindow} open at the + * same time. Responsible for managing listeners as + * {@link com.mapbox.mapboxsdk.maps.MapboxMap.OnInfoWindowClickListener} and + * {@link com.mapbox.mapboxsdk.maps.MapboxMap.OnInfoWindowLongClickListener}. + *

+ */ +class InfoWindowManager { + + private final List infoWindows = new ArrayList<>(); + + @Nullable + private MapboxMap.InfoWindowAdapter infoWindowAdapter; + private boolean allowConcurrentMultipleInfoWindows; + + @Nullable + private MapboxMap.OnInfoWindowClickListener onInfoWindowClickListener; + @Nullable + private MapboxMap.OnInfoWindowLongClickListener onInfoWindowLongClickListener; + @Nullable + private MapboxMap.OnInfoWindowCloseListener onInfoWindowCloseListener; + + void update() { + if (!infoWindows.isEmpty()) { + for (InfoWindow infoWindow : infoWindows) { + infoWindow.update(); + } + } + } + + void setInfoWindowAdapter(@Nullable MapboxMap.InfoWindowAdapter infoWindowAdapter) { + this.infoWindowAdapter = infoWindowAdapter; + } + + @Nullable + MapboxMap.InfoWindowAdapter getInfoWindowAdapter() { + return infoWindowAdapter; + } + + void setAllowConcurrentMultipleOpenInfoWindows(boolean allow) { + allowConcurrentMultipleInfoWindows = allow; + } + + boolean isAllowConcurrentMultipleOpenInfoWindows() { + return allowConcurrentMultipleInfoWindows; + } + + boolean isInfoWindowValidForMarker(@Nullable Marker marker) { + return marker != null && (!TextUtils.isEmpty(marker.getTitle()) || !TextUtils.isEmpty(marker.getSnippet())); + } + + void setOnInfoWindowClickListener(@Nullable MapboxMap.OnInfoWindowClickListener listener) { + onInfoWindowClickListener = listener; + } + + @Nullable + MapboxMap.OnInfoWindowClickListener getOnInfoWindowClickListener() { + return onInfoWindowClickListener; + } + + void setOnInfoWindowLongClickListener(@Nullable MapboxMap.OnInfoWindowLongClickListener listener) { + onInfoWindowLongClickListener = listener; + } + + @Nullable + MapboxMap.OnInfoWindowLongClickListener getOnInfoWindowLongClickListener() { + return onInfoWindowLongClickListener; + } + + void setOnInfoWindowCloseListener(@Nullable MapboxMap.OnInfoWindowCloseListener listener) { + onInfoWindowCloseListener = listener; + } + + @Nullable + MapboxMap.OnInfoWindowCloseListener getOnInfoWindowCloseListener() { + return onInfoWindowCloseListener; + } + + public void add(InfoWindow infoWindow) { + infoWindows.add(infoWindow); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapChangeReceiver.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapChangeReceiver.java new file mode 100644 index 000000000..5932f6fbf --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapChangeReceiver.java @@ -0,0 +1,395 @@ +package com.mapbox.mapboxsdk.maps; + +import com.mapbox.mapboxsdk.log.Logger; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +class MapChangeReceiver implements NativeMapView.StateCallback { + + private static final String TAG = "Mbgl-MapChangeReceiver"; + + private final List onCameraWillChangeListenerList = new CopyOnWriteArrayList<>(); + private final List onCameraIsChangingListenerList = new CopyOnWriteArrayList<>(); + private final List onCameraDidChangeListenerList = new CopyOnWriteArrayList<>(); + private final List onWillStartLoadingMapListenerList + = new CopyOnWriteArrayList<>(); + private final List onDidFinishLoadingMapListenerList + = new CopyOnWriteArrayList<>(); + private final List onDidFailLoadingMapListenerList + = new CopyOnWriteArrayList<>(); + private final List onWillStartRenderingFrameList + = new CopyOnWriteArrayList<>(); + private final List onDidFinishRenderingFrameList + = new CopyOnWriteArrayList<>(); + private final List onWillStartRenderingMapListenerList + = new CopyOnWriteArrayList<>(); + private final List onDidFinishRenderingMapListenerList + = new CopyOnWriteArrayList<>(); + private final List onDidBecomeIdleListenerList + = new CopyOnWriteArrayList<>(); + private final List onDidFinishLoadingStyleListenerList + = new CopyOnWriteArrayList<>(); + private final List onSourceChangedListenerList = new CopyOnWriteArrayList<>(); + private final List onStyleImageMissingListenerList + = new CopyOnWriteArrayList<>(); + private final List onCanRemoveUnusedStyleImageListenerList + = new CopyOnWriteArrayList<>(); + + @Override + public void onCameraWillChange(boolean animated) { + try { + if (!onCameraWillChangeListenerList.isEmpty()) { + for (MapView.OnCameraWillChangeListener onCameraWillChangeListener : onCameraWillChangeListenerList) { + onCameraWillChangeListener.onCameraWillChange(animated); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onCameraWillChange", err); + throw err; + } + } + + @Override + public void onCameraIsChanging() { + try { + if (!onCameraIsChangingListenerList.isEmpty()) { + for (MapView.OnCameraIsChangingListener onCameraIsChangingListener : onCameraIsChangingListenerList) { + onCameraIsChangingListener.onCameraIsChanging(); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onCameraIsChanging", err); + throw err; + } + } + + @Override + public void onCameraDidChange(boolean animated) { + try { + if (!onCameraDidChangeListenerList.isEmpty()) { + for (MapView.OnCameraDidChangeListener onCameraDidChangeListener : onCameraDidChangeListenerList) { + onCameraDidChangeListener.onCameraDidChange(animated); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onCameraDidChange", err); + throw err; + } + } + + @Override + public void onWillStartLoadingMap() { + try { + if (!onWillStartLoadingMapListenerList.isEmpty()) { + for (MapView.OnWillStartLoadingMapListener onWillStartLoadingMapListener : onWillStartLoadingMapListenerList) { + onWillStartLoadingMapListener.onWillStartLoadingMap(); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onWillStartLoadingMap", err); + throw err; + } + } + + @Override + public void onDidFinishLoadingMap() { + try { + if (!onDidFinishLoadingMapListenerList.isEmpty()) { + for (MapView.OnDidFinishLoadingMapListener onDidFinishLoadingMapListener : onDidFinishLoadingMapListenerList) { + onDidFinishLoadingMapListener.onDidFinishLoadingMap(); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onDidFinishLoadingMap", err); + throw err; + } + } + + @Override + public void onDidFailLoadingMap(String error) { + try { + if (!onDidFailLoadingMapListenerList.isEmpty()) { + for (MapView.OnDidFailLoadingMapListener onDidFailLoadingMapListener : onDidFailLoadingMapListenerList) { + onDidFailLoadingMapListener.onDidFailLoadingMap(error); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onDidFailLoadingMap", err); + throw err; + } + } + + @Override + public void onWillStartRenderingFrame() { + try { + if (!onWillStartRenderingFrameList.isEmpty()) { + for (MapView.OnWillStartRenderingFrameListener listener : onWillStartRenderingFrameList) { + listener.onWillStartRenderingFrame(); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onWillStartRenderingFrame", err); + throw err; + } + } + + @Override + public void onDidFinishRenderingFrame(boolean fully) { + try { + if (!onDidFinishRenderingFrameList.isEmpty()) { + for (MapView.OnDidFinishRenderingFrameListener listener : onDidFinishRenderingFrameList) { + listener.onDidFinishRenderingFrame(fully); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onDidFinishRenderingFrame", err); + throw err; + } + } + + @Override + public void onWillStartRenderingMap() { + try { + if (!onWillStartRenderingMapListenerList.isEmpty()) { + for (MapView.OnWillStartRenderingMapListener listener : onWillStartRenderingMapListenerList) { + listener.onWillStartRenderingMap(); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onWillStartRenderingMap", err); + throw err; + } + } + + @Override + public void onDidFinishRenderingMap(boolean fully) { + try { + if (!onDidFinishRenderingMapListenerList.isEmpty()) { + for (MapView.OnDidFinishRenderingMapListener listener : onDidFinishRenderingMapListenerList) { + listener.onDidFinishRenderingMap(fully); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onDidFinishRenderingMap", err); + throw err; + } + } + + @Override + public void onDidBecomeIdle() { + try { + if (!onDidBecomeIdleListenerList.isEmpty()) { + for (MapView.OnDidBecomeIdleListener listener : onDidBecomeIdleListenerList) { + listener.onDidBecomeIdle(); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onDidBecomeIdle", err); + throw err; + } + } + + @Override + public void onDidFinishLoadingStyle() { + try { + if (!onDidFinishLoadingStyleListenerList.isEmpty()) { + for (MapView.OnDidFinishLoadingStyleListener listener : onDidFinishLoadingStyleListenerList) { + listener.onDidFinishLoadingStyle(); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onDidFinishLoadingStyle", err); + throw err; + } + } + + @Override + public void onSourceChanged(String sourceId) { + try { + if (!onSourceChangedListenerList.isEmpty()) { + for (MapView.OnSourceChangedListener onSourceChangedListener : onSourceChangedListenerList) { + onSourceChangedListener.onSourceChangedListener(sourceId); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onSourceChanged", err); + throw err; + } + } + + @Override + public void onStyleImageMissing(String imageId) { + try { + if (!onStyleImageMissingListenerList.isEmpty()) { + for (MapView.OnStyleImageMissingListener listener : onStyleImageMissingListenerList) { + listener.onStyleImageMissing(imageId); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onStyleImageMissing", err); + throw err; + } + } + + @Override + public boolean onCanRemoveUnusedStyleImage(String imageId) { + if (onCanRemoveUnusedStyleImageListenerList.isEmpty()) { + return true; + } + + try { + if (!onCanRemoveUnusedStyleImageListenerList.isEmpty()) { + boolean canRemove = true; + for (MapView.OnCanRemoveUnusedStyleImageListener listener : onCanRemoveUnusedStyleImageListenerList) { + canRemove &= listener.onCanRemoveUnusedStyleImage(imageId); + } + + return canRemove; + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onCanRemoveUnusedStyleImage", err); + throw err; + } + + return true; + } + + void addOnCameraWillChangeListener(MapView.OnCameraWillChangeListener listener) { + onCameraWillChangeListenerList.add(listener); + } + + void removeOnCameraWillChangeListener(MapView.OnCameraWillChangeListener listener) { + onCameraWillChangeListenerList.remove(listener); + } + + void addOnCameraIsChangingListener(MapView.OnCameraIsChangingListener listener) { + onCameraIsChangingListenerList.add(listener); + } + + void removeOnCameraIsChangingListener(MapView.OnCameraIsChangingListener listener) { + onCameraIsChangingListenerList.remove(listener); + } + + void addOnCameraDidChangeListener(MapView.OnCameraDidChangeListener listener) { + onCameraDidChangeListenerList.add(listener); + } + + void removeOnCameraDidChangeListener(MapView.OnCameraDidChangeListener listener) { + onCameraDidChangeListenerList.remove(listener); + } + + void addOnWillStartLoadingMapListener(MapView.OnWillStartLoadingMapListener listener) { + onWillStartLoadingMapListenerList.add(listener); + } + + void removeOnWillStartLoadingMapListener(MapView.OnWillStartLoadingMapListener listener) { + onWillStartLoadingMapListenerList.remove(listener); + } + + void addOnDidFinishLoadingMapListener(MapView.OnDidFinishLoadingMapListener listener) { + onDidFinishLoadingMapListenerList.add(listener); + } + + void removeOnDidFinishLoadingMapListener(MapView.OnDidFinishLoadingMapListener listener) { + onDidFinishLoadingMapListenerList.remove(listener); + } + + void addOnDidFailLoadingMapListener(MapView.OnDidFailLoadingMapListener listener) { + onDidFailLoadingMapListenerList.add(listener); + } + + void removeOnDidFailLoadingMapListener(MapView.OnDidFailLoadingMapListener listener) { + onDidFailLoadingMapListenerList.remove(listener); + } + + void addOnWillStartRenderingFrameListener(MapView.OnWillStartRenderingFrameListener listener) { + onWillStartRenderingFrameList.add(listener); + } + + void removeOnWillStartRenderingFrameListener(MapView.OnWillStartRenderingFrameListener listener) { + onWillStartRenderingFrameList.remove(listener); + } + + void addOnDidFinishRenderingFrameListener(MapView.OnDidFinishRenderingFrameListener listener) { + onDidFinishRenderingFrameList.add(listener); + } + + void removeOnDidFinishRenderingFrameListener(MapView.OnDidFinishRenderingFrameListener listener) { + onDidFinishRenderingFrameList.remove(listener); + } + + void addOnWillStartRenderingMapListener(MapView.OnWillStartRenderingMapListener listener) { + onWillStartRenderingMapListenerList.add(listener); + } + + void removeOnWillStartRenderingMapListener(MapView.OnWillStartRenderingMapListener listener) { + onWillStartRenderingMapListenerList.remove(listener); + } + + void addOnDidFinishRenderingMapListener(MapView.OnDidFinishRenderingMapListener listener) { + onDidFinishRenderingMapListenerList.add(listener); + } + + void removeOnDidFinishRenderingMapListener(MapView.OnDidFinishRenderingMapListener listener) { + onDidFinishRenderingMapListenerList.remove(listener); + } + + void addOnDidBecomeIdleListener(MapView.OnDidBecomeIdleListener listener) { + onDidBecomeIdleListenerList.add(listener); + } + + void removeOnDidBecomeIdleListener(MapView.OnDidBecomeIdleListener listener) { + onDidBecomeIdleListenerList.remove(listener); + } + + void addOnDidFinishLoadingStyleListener(MapView.OnDidFinishLoadingStyleListener listener) { + onDidFinishLoadingStyleListenerList.add(listener); + } + + void removeOnDidFinishLoadingStyleListener(MapView.OnDidFinishLoadingStyleListener listener) { + onDidFinishLoadingStyleListenerList.remove(listener); + } + + void addOnSourceChangedListener(MapView.OnSourceChangedListener listener) { + onSourceChangedListenerList.add(listener); + } + + void removeOnSourceChangedListener(MapView.OnSourceChangedListener listener) { + onSourceChangedListenerList.remove(listener); + } + + void addOnStyleImageMissingListener(MapView.OnStyleImageMissingListener listener) { + onStyleImageMissingListenerList.add(listener); + } + + void removeOnStyleImageMissingListener(MapView.OnStyleImageMissingListener listener) { + onStyleImageMissingListenerList.remove(listener); + } + + void addOnCanRemoveUnusedStyleImageListener(MapView.OnCanRemoveUnusedStyleImageListener listener) { + onCanRemoveUnusedStyleImageListenerList.add(listener); + } + + void removeOnCanRemoveUnusedStyleImageListener(MapView.OnCanRemoveUnusedStyleImageListener listener) { + onCanRemoveUnusedStyleImageListenerList.remove(listener); + } + + void clear() { + onCameraWillChangeListenerList.clear(); + onCameraIsChangingListenerList.clear(); + onCameraDidChangeListenerList.clear(); + onWillStartLoadingMapListenerList.clear(); + onDidFinishLoadingMapListenerList.clear(); + onDidFailLoadingMapListenerList.clear(); + onWillStartRenderingFrameList.clear(); + onDidFinishRenderingFrameList.clear(); + onWillStartRenderingMapListenerList.clear(); + onDidFinishRenderingMapListenerList.clear(); + onDidBecomeIdleListenerList.clear(); + onDidFinishLoadingStyleListenerList.clear(); + onSourceChangedListenerList.clear(); + onStyleImageMissingListenerList.clear(); + onCanRemoveUnusedStyleImageListenerList.clear(); + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapFragment.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapFragment.java new file mode 100644 index 000000000..8af9f2c6a --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapFragment.java @@ -0,0 +1,241 @@ +package com.mapbox.mapboxsdk.maps; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.mapbox.mapboxsdk.utils.MapFragmentUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Fragment wrapper around a map view. + *

+ * A Map component in an app. This fragment is the simplest way to place a map in an application. + * It's a wrapper around a view of a map to automatically handle the necessary life cycle needs. + * Being a fragment, this component can be added to an activity's layout or can dynamically be added + * using a FragmentManager. + *

+ *

+ * To get a reference to the MapView, use {@link #getMapAsync(OnMapReadyCallback)}} + *

+ * + * @see #getMapAsync(OnMapReadyCallback) + */ +public final class MapFragment extends Fragment implements OnMapReadyCallback { + + private final List mapReadyCallbackList = new ArrayList<>(); + private OnMapViewReadyCallback mapViewReadyCallback; + private MapboxMap mapboxMap; + private MapView map; + + /** + * Creates a default MapFragment instance + * + * @return MapFragment instantiated + */ + public static MapFragment newInstance() { + return new MapFragment(); + } + + /** + * Creates a MapFragment instance + * + * @param mapboxMapOptions The configuration options to be used. + * @return MapFragment instantiated. + */ + @NonNull + public static MapFragment newInstance(@Nullable MapboxMapOptions mapboxMapOptions) { + MapFragment mapFragment = new MapFragment(); + mapFragment.setArguments(MapFragmentUtils.createFragmentArgs(mapboxMapOptions)); + return mapFragment; + } + + /** + * Called when this fragment is inflated, parses XML tag attributes. + * + * @param context The context inflating this fragment. + * @param attrs The XML tag attributes. + * @param savedInstanceState The saved instance state for the map fragment. + */ + @Override + public void onInflate(@NonNull Context context, AttributeSet attrs, Bundle savedInstanceState) { + super.onInflate(context, attrs, savedInstanceState); + setArguments(MapFragmentUtils.createFragmentArgs(MapboxMapOptions.createFromAttributes(context, attrs))); + } + + /** + * Called when the context attaches to this fragment. + * + * @param context the context attaching + */ + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof OnMapViewReadyCallback) { + mapViewReadyCallback = (OnMapViewReadyCallback) context; + } + } + + /** + * Creates the fragment view hierarchy. + * + * @param inflater Inflater used to inflate content. + * @param container The parent layout for the map fragment. + * @param savedInstanceState The saved instance state for the map fragment. + * @return The view created + */ + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + Context context = inflater.getContext(); + map = new MapView(context, MapFragmentUtils.resolveArgs(context, getArguments())); + return map; + } + + /** + * Called when the fragment view hierarchy is created. + * + * @param view The content view of the fragment + * @param savedInstanceState The saved instance state of the fragment + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + map.onCreate(savedInstanceState); + map.getMapAsync(this); + + // notify listeners about mapview creation + if (mapViewReadyCallback != null) { + mapViewReadyCallback.onMapViewReady(map); + } + } + + /** + * Called when the style of the map has successfully loaded. + * + * @param mapboxMap The public api controller of the map + */ + @Override + public void onMapReady(@NonNull MapboxMap mapboxMap) { + this.mapboxMap = mapboxMap; + for (OnMapReadyCallback onMapReadyCallback : mapReadyCallbackList) { + onMapReadyCallback.onMapReady(mapboxMap); + } + } + + /** + * Called when the fragment is visible for the users. + */ + @Override + public void onStart() { + super.onStart(); + map.onStart(); + } + + /** + * Called when the fragment is ready to be interacted with. + */ + @Override + public void onResume() { + super.onResume(); + map.onResume(); + } + + /** + * Called when the fragment is pausing. + */ + @Override + public void onPause() { + super.onPause(); + map.onPause(); + } + + /** + * Called when the fragment state needs to be saved. + * + * @param outState The saved state + */ + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (map != null && !map.isDestroyed()) { + map.onSaveInstanceState(outState); + } + } + + /** + * Called when the fragment is no longer visible for the user. + */ + @Override + public void onStop() { + super.onStop(); + map.onStop(); + } + + /** + * Called when the fragment receives onLowMemory call from the hosting Activity. + */ + @Override + public void onLowMemory() { + super.onLowMemory(); + if (map != null && !map.isDestroyed()) { + map.onLowMemory(); + } + } + + /** + * Called when the fragment is view hiearchy is being destroyed. + */ + @Override + public void onDestroyView() { + super.onDestroyView(); + map.onDestroy(); + } + + /** + * Called when the fragment is destroyed. + */ + @Override + public void onDestroy() { + super.onDestroy(); + mapReadyCallbackList.clear(); + } + + /** + * Sets a callback object which will be triggered when the MapboxMap instance is ready to be used. + * + * @param onMapReadyCallback The callback to be invoked. + */ + public void getMapAsync(@NonNull final OnMapReadyCallback onMapReadyCallback) { + if (mapboxMap == null) { + mapReadyCallbackList.add(onMapReadyCallback); + } else { + onMapReadyCallback.onMapReady(mapboxMap); + } + } + + /** + * Callback to be invoked when the map fragment has inflated its MapView. + *

+ * To use this interface the context hosting the fragment must implement this interface. + * That instance will be set as part of Fragment#onAttach(Context context). + *

+ */ + public interface OnMapViewReadyCallback { + + /** + * Called when the map has been created. + * + * @param mapView The created mapview + */ + void onMapViewReady(MapView mapView); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java new file mode 100644 index 000000000..90e3934f7 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java @@ -0,0 +1,1140 @@ +package com.mapbox.mapboxsdk.maps; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.PointF; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.animation.DecelerateInterpolator; + +import com.mapbox.android.gestures.AndroidGesturesManager; +import com.mapbox.android.gestures.MoveGestureDetector; +import com.mapbox.android.gestures.MultiFingerTapGestureDetector; +import com.mapbox.android.gestures.RotateGestureDetector; +import com.mapbox.android.gestures.ShoveGestureDetector; +import com.mapbox.android.gestures.StandardGestureDetector; +import com.mapbox.android.gestures.StandardScaleGestureDetector; +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.constants.MapboxConstants; +import com.mapbox.mapboxsdk.utils.MathUtils; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +import static com.mapbox.mapboxsdk.constants.MapboxConstants.MAXIMUM_ANGULAR_VELOCITY; +import static com.mapbox.mapboxsdk.constants.MapboxConstants.MAX_ABSOLUTE_SCALE_VELOCITY_CHANGE; +import static com.mapbox.mapboxsdk.constants.MapboxConstants.QUICK_ZOOM_MAX_ZOOM_CHANGE; +import static com.mapbox.mapboxsdk.constants.MapboxConstants.ROTATE_VELOCITY_RATIO_THRESHOLD; +import static com.mapbox.mapboxsdk.constants.MapboxConstants.SCALE_VELOCITY_ANIMATION_DURATION_MULTIPLIER; +import static com.mapbox.mapboxsdk.constants.MapboxConstants.SCALE_VELOCITY_RATIO_THRESHOLD; +import static com.mapbox.mapboxsdk.constants.MapboxConstants.ZOOM_RATE; +import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE; +import static com.mapbox.mapboxsdk.utils.MathUtils.normalize; + +/** + * Manages gestures events on a MapView. + */ +final class MapGestureDetector { + + private final Transform transform; + private final Projection projection; + private final UiSettings uiSettings; + private final AnnotationManager annotationManager; + private final CameraChangeDispatcher cameraChangeDispatcher; + + // new map touch API + private final CopyOnWriteArrayList onMapClickListenerList + = new CopyOnWriteArrayList<>(); + + private final CopyOnWriteArrayList onMapLongClickListenerList + = new CopyOnWriteArrayList<>(); + + private final CopyOnWriteArrayList onFlingListenerList + = new CopyOnWriteArrayList<>(); + + private final CopyOnWriteArrayList onMoveListenerList + = new CopyOnWriteArrayList<>(); + + private final CopyOnWriteArrayList onRotateListenerList + = new CopyOnWriteArrayList<>(); + + private final CopyOnWriteArrayList onScaleListenerList + = new CopyOnWriteArrayList<>(); + + private final CopyOnWriteArrayList onShoveListenerList + = new CopyOnWriteArrayList<>(); + + /** + * User-set focal point. + */ + @Nullable + private PointF constantFocalPoint; + + @NonNull + private PointF doubleTapFocalPoint = new PointF(); + + private AndroidGesturesManager gesturesManager; + + private Animator scaleAnimator; + private Animator rotateAnimator; + private final List scheduledAnimators = new ArrayList<>(); + + /** + * Cancels scheduled velocity animations if user doesn't lift fingers within + * {@link MapboxConstants#SCHEDULED_ANIMATION_TIMEOUT} + */ + @NonNull + private Handler animationsTimeoutHandler = new Handler(); + + private boolean doubleTapRegistered; + + MapGestureDetector(@Nullable Context context, Transform transform, Projection projection, UiSettings uiSettings, + AnnotationManager annotationManager, CameraChangeDispatcher cameraChangeDispatcher) { + this.annotationManager = annotationManager; + this.transform = transform; + this.projection = projection; + this.uiSettings = uiSettings; + this.cameraChangeDispatcher = cameraChangeDispatcher; + + // Checking for context != null for testing purposes + if (context != null) { + // Initialize gestures manager + AndroidGesturesManager androidGesturesManager = new AndroidGesturesManager(context); + initializeGesturesManager(androidGesturesManager, true); + + // Initialize gesture listeners + initializeGestureListeners(context, true); + } + } + + private void initializeGestureListeners(@NonNull Context context, boolean attachDefaultListeners) { + if (attachDefaultListeners) { + StandardGestureListener standardGestureListener = new StandardGestureListener( + context.getResources().getDimension( + com.mapbox.android.gestures.R.dimen.mapbox_defaultScaleSpanSinceStartThreshold)); + MoveGestureListener moveGestureListener = new MoveGestureListener(); + ScaleGestureListener scaleGestureListener = new ScaleGestureListener( + context.getResources().getDimension(R.dimen.mapbox_density_constant), + context.getResources().getDimension(R.dimen.mapbox_minimum_scale_speed), + context.getResources().getDimension(R.dimen.mapbox_minimum_angled_scale_speed), + context.getResources().getDimension(R.dimen.mapbox_minimum_scale_velocity) + ); + RotateGestureListener rotateGestureListener = new RotateGestureListener( + context.getResources().getDimension(R.dimen.mapbox_minimum_scale_span_when_rotating), + context.getResources().getDimension(R.dimen.mapbox_density_constant), + context.getResources().getDimension(R.dimen.mapbox_angular_velocity_multiplier), + context.getResources().getDimension(R.dimen.mapbox_minimum_angular_velocity), + context.getResources().getDimension( + com.mapbox.android.gestures.R.dimen.mapbox_defaultScaleSpanSinceStartThreshold)); + ShoveGestureListener shoveGestureListener = new ShoveGestureListener(); + TapGestureListener tapGestureListener = new TapGestureListener(); + + gesturesManager.setStandardGestureListener(standardGestureListener); + gesturesManager.setMoveGestureListener(moveGestureListener); + gesturesManager.setStandardScaleGestureListener(scaleGestureListener); + gesturesManager.setRotateGestureListener(rotateGestureListener); + gesturesManager.setShoveGestureListener(shoveGestureListener); + gesturesManager.setMultiFingerTapGestureListener(tapGestureListener); + } + } + + private void initializeGesturesManager(@NonNull AndroidGesturesManager androidGesturesManager, + boolean setDefaultMutuallyExclusives) { + if (setDefaultMutuallyExclusives) { + Set shoveScaleSet = new HashSet<>(); + shoveScaleSet.add(AndroidGesturesManager.GESTURE_TYPE_SHOVE); + shoveScaleSet.add(AndroidGesturesManager.GESTURE_TYPE_SCALE); + + Set shoveRotateSet = new HashSet<>(); + shoveRotateSet.add(AndroidGesturesManager.GESTURE_TYPE_SHOVE); + shoveRotateSet.add(AndroidGesturesManager.GESTURE_TYPE_ROTATE); + + Set ScaleLongPressSet = new HashSet<>(); + ScaleLongPressSet.add(AndroidGesturesManager.GESTURE_TYPE_SCALE); + ScaleLongPressSet.add(AndroidGesturesManager.GESTURE_TYPE_LONG_PRESS); + + androidGesturesManager.setMutuallyExclusiveGestures(shoveScaleSet, shoveRotateSet, ScaleLongPressSet); + } + + gesturesManager = androidGesturesManager; + gesturesManager.getRotateGestureDetector().setAngleThreshold(3f); + } + + /** + * Set the gesture focal point. + *

+ * This is the center point used for calculate transformations from gestures, value is + * overridden if end user provides his own through {@link UiSettings#setFocalPoint(PointF)}. + *

+ * + * @param focalPoint the center point for gestures + */ + void setFocalPoint(@Nullable PointF focalPoint) { + if (focalPoint == null) { + // resetting focal point, + if (uiSettings.getFocalPoint() != null) { + // using user provided one to reset + focalPoint = uiSettings.getFocalPoint(); + } + } + this.constantFocalPoint = focalPoint; + } + + /** + * Called when user touches the screen, all positions are absolute. + *

+ * Forwards event to the related gesture detectors. + *

+ * + * @param motionEvent the MotionEvent + * @return True if touch event is handled + */ + boolean onTouchEvent(@Nullable MotionEvent motionEvent) { + // Framework can return null motion events in edge cases #9432 + if (motionEvent == null) { + return false; + } + + // Check and ignore non touch or left clicks + if ((motionEvent.getButtonState() != 0) && (motionEvent.getButtonState() != MotionEvent.BUTTON_PRIMARY)) { + return false; + } + + if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + cancelAnimators(); + transform.setGestureInProgress(true); + } + + boolean result = gesturesManager.onTouchEvent(motionEvent); + + switch (motionEvent.getActionMasked()) { + case MotionEvent.ACTION_POINTER_DOWN: + doubleTapFinished(); + break; + + case MotionEvent.ACTION_UP: + doubleTapFinished(); + transform.setGestureInProgress(false); + + if (!scheduledAnimators.isEmpty()) { + // Start all awaiting velocity animations + animationsTimeoutHandler.removeCallbacksAndMessages(null); + for (Animator animator : scheduledAnimators) { + animator.start(); + } + scheduledAnimators.clear(); + } + break; + + case MotionEvent.ACTION_CANCEL: + scheduledAnimators.clear(); + transform.setGestureInProgress(false); + doubleTapFinished(); + break; + } + + return result; + } + + void cancelAnimators() { + animationsTimeoutHandler.removeCallbacksAndMessages(null); + scheduledAnimators.clear(); + + cancelAnimator(scaleAnimator); + cancelAnimator(rotateAnimator); + + dispatchCameraIdle(); + } + + private void cancelAnimator(@Nullable Animator animator) { + if (animator != null && animator.isStarted()) { + animator.cancel(); + } + } + + /** + * Posted on main thread with {@link #animationsTimeoutHandler}. Cancels all scheduled animators if needed. + */ + @NonNull + private final Runnable cancelAnimatorsRunnable = new Runnable() { + @Override + public void run() { + cancelAnimators(); + } + }; + + /** + * Schedules a velocity animator to be executed when user lifts fingers, + * unless canceled by the {@link #cancelAnimatorsRunnable}. + * + * @param animator animator ot be scheduled + */ + private void scheduleAnimator(Animator animator) { + scheduledAnimators.add(animator); + animationsTimeoutHandler.removeCallbacksAndMessages(null); + animationsTimeoutHandler.postDelayed(cancelAnimatorsRunnable, MapboxConstants.SCHEDULED_ANIMATION_TIMEOUT); + } + + /** + * Called for events that don't fit the other handlers. + *

+ * Examples of such events are mouse scroll events, mouse moves, joystick & trackpad. + *

+ * + * @param event The MotionEvent occurred + * @return True is the event is handled + */ + boolean onGenericMotionEvent(MotionEvent event) { + // Mouse events + // if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) { // this is not available before API 18 + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == InputDevice.SOURCE_CLASS_POINTER) { + // Choose the action + switch (event.getActionMasked()) { + // Mouse scrolls + case MotionEvent.ACTION_SCROLL: + if (!uiSettings.isZoomGesturesEnabled()) { + return false; + } + + // Cancel any animation + transform.cancelTransitions(); + + // Get the vertical scroll amount, one click = 1 + float scrollDist = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + + // Scale the map by the appropriate power of two factor + transform.zoomBy(scrollDist, new PointF(event.getX(), event.getY())); + + return true; + + default: + // We are not interested in this event + return false; + } + } + + // We are not interested in this event + return false; + } + + private final class StandardGestureListener extends StandardGestureDetector.SimpleStandardOnGestureListener { + private final float doubleTapMovementThreshold; + + StandardGestureListener(float doubleTapMovementThreshold) { + this.doubleTapMovementThreshold = doubleTapMovementThreshold; + } + + @Override + public boolean onDown(MotionEvent motionEvent) { + return true; + } + + @Override + public boolean onSingleTapUp(MotionEvent motionEvent) { + transform.cancelTransitions(); + return true; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent motionEvent) { + PointF tapPoint = new PointF(motionEvent.getX(), motionEvent.getY()); + boolean tapHandled = annotationManager.onTap(tapPoint); + + if (!tapHandled) { + if (uiSettings.isDeselectMarkersOnTap()) { + // deselect any selected marker + annotationManager.deselectMarkers(); + } + + notifyOnMapClickListeners(tapPoint); + } + + return true; + } + + @Override + public boolean onDoubleTapEvent(MotionEvent motionEvent) { + int action = motionEvent.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN) { + doubleTapFocalPoint = new PointF(motionEvent.getX(), motionEvent.getY()); + doubleTapStarted(); + } + + if (motionEvent.getActionMasked() == MotionEvent.ACTION_UP) { + float diffX = Math.abs(motionEvent.getX() - doubleTapFocalPoint.x); + float diffY = Math.abs(motionEvent.getY() - doubleTapFocalPoint.y); + if (diffX > doubleTapMovementThreshold || diffY > doubleTapMovementThreshold) { + // Ignore double-tap event because we've started the quick-zoom. See #14013. + return false; + } + + if (!uiSettings.isZoomGesturesEnabled() || !uiSettings.isDoubleTapGesturesEnabled()) { + return false; + } + + // Single finger double tap + if (constantFocalPoint != null) { + // User provided focal point + doubleTapFocalPoint = constantFocalPoint; + } + + zoomInAnimated(doubleTapFocalPoint, false); + + return true; + } + + return super.onDoubleTapEvent(motionEvent); + } + + @Override + public void onLongPress(MotionEvent motionEvent) { + PointF longClickPoint = new PointF(motionEvent.getX(), motionEvent.getY()); + notifyOnMapLongClickListeners(longClickPoint); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (!uiSettings.isScrollGesturesEnabled()) { + // don't allow a fling if scroll is disabled + return false; + } + + notifyOnFlingListeners(); + + if (!uiSettings.isFlingVelocityAnimationEnabled()) { + return false; + } + + float screenDensity = uiSettings.getPixelRatio(); + + // calculate velocity vector for xy dimensions, independent from screen size + double velocityXY = Math.hypot(velocityX / screenDensity, velocityY / screenDensity); + if (velocityXY < MapboxConstants.VELOCITY_THRESHOLD_IGNORE_FLING) { + // ignore short flings, these can occur when other gestures just have finished executing + return false; + } + + transform.cancelTransitions(); + cameraChangeDispatcher.onCameraMoveStarted(REASON_API_GESTURE); + + // tilt results in a bigger translation, limiting input for #5281 + double tilt = transform.getTilt(); + double tiltFactor = 1.5 + ((tilt != 0) ? (tilt / 10) : 0); + double offsetX = velocityX / tiltFactor / screenDensity; + double offsetY = velocityY / tiltFactor / screenDensity; + + // calculate animation time based on displacement + long animationTime = (long) (velocityXY / 7 / tiltFactor + MapboxConstants.ANIMATION_DURATION_FLING_BASE); + + // update transformation + transform.moveBy(offsetX, offsetY, animationTime); + + return true; + } + } + + private void doubleTapStarted() { + // disable the move detector in preparation for the quickzoom, + // so that we don't move the map's center slightly before the quickzoom is started (see #14227) + gesturesManager.getMoveGestureDetector().setEnabled(false); + doubleTapRegistered = true; + } + + private void doubleTapFinished() { + if (doubleTapRegistered) { + // re-enable the move detector in case of double tap + gesturesManager.getMoveGestureDetector().setEnabled(true); + doubleTapRegistered = false; + } + } + + private final class MoveGestureListener extends MoveGestureDetector.SimpleOnMoveGestureListener { + @Override + public boolean onMoveBegin(@NonNull MoveGestureDetector detector) { + if (!uiSettings.isScrollGesturesEnabled()) { + return false; + } + + cancelTransitionsIfRequired(); + notifyOnMoveBeginListeners(detector); + return true; + } + + @Override + public boolean onMove(@NonNull MoveGestureDetector detector, float distanceX, float distanceY) { + // first move event is often delivered with no displacement + if (distanceX != 0 || distanceY != 0) { + // dispatching camera start event only when the movement actually occurred + cameraChangeDispatcher.onCameraMoveStarted(CameraChangeDispatcher.REASON_API_GESTURE); + + // Scroll the map + transform.moveBy(-distanceX, -distanceY, 0 /*no duration*/); + + notifyOnMoveListeners(detector); + } + return true; + } + + @Override + public void onMoveEnd(@NonNull MoveGestureDetector detector, float velocityX, float velocityY) { + dispatchCameraIdle(); + notifyOnMoveEndListeners(detector); + } + } + + private final class ScaleGestureListener extends StandardScaleGestureDetector.SimpleStandardOnScaleGestureListener { + + private final float minimumGestureSpeed; + private final float minimumAngledGestureSpeed; + private final float minimumVelocity; + private final double scaleVelocityRatioThreshold; + private boolean quickZoom; + private float spanSinceLast; + private double screenHeight; + private double startZoom; + + ScaleGestureListener(double densityMultiplier, float minimumGestureSpeed, float minimumAngledGestureSpeed, + float minimumVelocity) { + this.minimumGestureSpeed = minimumGestureSpeed; + this.minimumAngledGestureSpeed = minimumAngledGestureSpeed; + this.minimumVelocity = minimumVelocity; + this.scaleVelocityRatioThreshold = SCALE_VELOCITY_RATIO_THRESHOLD * densityMultiplier; + } + + @Override + public boolean onScaleBegin(@NonNull StandardScaleGestureDetector detector) { + quickZoom = detector.getPointersCount() == 1; + + if (!uiSettings.isZoomGesturesEnabled()) { + return false; + } + + if (quickZoom) { + if (!uiSettings.isQuickZoomGesturesEnabled()) { + return false; + } + // re-try disabling the move detector in case double tap has been interrupted before quickzoom started + gesturesManager.getMoveGestureDetector().setEnabled(false); + } else { + if (detector.getPreviousSpan() > 0) { + float currSpan = detector.getCurrentSpan(); + float prevSpan = detector.getPreviousSpan(); + double currTime = detector.getCurrentEvent().getEventTime(); + double prevTime = detector.getPreviousEvent().getEventTime(); + if (currTime == prevTime) { + return false; + } + double speed = Math.abs(currSpan - prevSpan) / (currTime - prevTime); + if (speed < minimumGestureSpeed) { + // do not scale if the minimal gesture speed is not met + return false; + } else if (!gesturesManager.getRotateGestureDetector().isInProgress()) { + float rotationDeltaSinceLast = gesturesManager.getRotateGestureDetector().getDeltaSinceLast(); + if (Math.abs(rotationDeltaSinceLast) > 0.4 && speed < minimumAngledGestureSpeed) { + // do not scale in case we're preferring to start rotation + return false; + } + + if (uiSettings.isDisableRotateWhenScaling()) { + // disable rotate gesture when scale is detected first + gesturesManager.getRotateGestureDetector().setEnabled(false); + } + } + } else { + return false; + } + } + + screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels; + startZoom = transform.getRawZoom(); + + cancelTransitionsIfRequired(); + + notifyOnScaleBeginListeners(detector); + + spanSinceLast = Math.abs(detector.getCurrentSpan() - detector.getPreviousSpan()); + + return true; + } + + @Override + public boolean onScale(@NonNull StandardScaleGestureDetector detector) { + // dispatching camera start event only when the movement actually occurred + cameraChangeDispatcher.onCameraMoveStarted(CameraChangeDispatcher.REASON_API_GESTURE); + + PointF focalPoint = getScaleFocalPoint(detector); + if (quickZoom) { + double pixelDeltaChange = Math.abs(detector.getCurrentEvent().getY() - doubleTapFocalPoint.y); + boolean zoomedOut = detector.getCurrentEvent().getY() < doubleTapFocalPoint.y; + + // normalize the pixel delta change, ranging from 0 to screen height, to a constant zoom change range + double normalizedDeltaChange = normalize(pixelDeltaChange, 0, screenHeight, 0, QUICK_ZOOM_MAX_ZOOM_CHANGE); + + // calculate target zoom and adjust for a multiplier + double targetZoom = (zoomedOut ? startZoom - normalizedDeltaChange : startZoom + normalizedDeltaChange); + targetZoom *= uiSettings.getZoomRate(); + + transform.setZoom(targetZoom, focalPoint); + } else { + double zoomBy = + (Math.log(detector.getScaleFactor()) / Math.log(Math.PI / 2)) * ZOOM_RATE * uiSettings.getZoomRate(); + transform.zoomBy(zoomBy, focalPoint); + } + + notifyOnScaleListeners(detector); + + spanSinceLast = Math.abs(detector.getCurrentSpan() - detector.getPreviousSpan()); + + return true; + } + + @Override + public void onScaleEnd(@NonNull StandardScaleGestureDetector detector, float velocityX, float velocityY) { + if (quickZoom) { + // re-enabled the move detector if the quickzoom happened + gesturesManager.getMoveGestureDetector().setEnabled(true); + } else { + // re-enable rotation in case it's been disabled + gesturesManager.getRotateGestureDetector().setEnabled(true); + } + + notifyOnScaleEndListeners(detector); + + float velocityXY = Math.abs(velocityX) + Math.abs(velocityY); + + if (!uiSettings.isScaleVelocityAnimationEnabled() + || velocityXY < minimumVelocity + || spanSinceLast / velocityXY < scaleVelocityRatioThreshold) { + // notifying listeners that camera is idle only if there is no follow-up animation + dispatchCameraIdle(); + return; + } + + double zoomAddition = calculateScale(velocityXY, detector.isScalingOut()); + double currentZoom = transform.getRawZoom(); + PointF focalPoint = getScaleFocalPoint(detector); + // (log(x + 1 / e^2) + 2) * 150, x=0 to 2.5 (MapboxConstants#MAX_ABSOLUTE_SCALE_VELOCITY_CHANGE) + long animationTime = (long) ((Math.log((Math.abs(zoomAddition)) + 1 / Math.pow(Math.E, 2)) + 2) + * SCALE_VELOCITY_ANIMATION_DURATION_MULTIPLIER); + scaleAnimator = createScaleAnimator(currentZoom, zoomAddition, focalPoint, animationTime); + scheduleAnimator(scaleAnimator); + } + + @NonNull + private PointF getScaleFocalPoint(@NonNull StandardScaleGestureDetector detector) { + if (constantFocalPoint != null) { + // around user provided focal point + return constantFocalPoint; + } else if (quickZoom) { + // around center + return new PointF(uiSettings.getWidth() / 2, uiSettings.getHeight() / 2); + } else { + // around gesture + return detector.getFocalPoint(); + } + } + + private double calculateScale(double velocityXY, boolean isScalingOut) { + double zoomAddition = velocityXY * MAX_ABSOLUTE_SCALE_VELOCITY_CHANGE * 1e-4; + zoomAddition = MathUtils.clamp(zoomAddition, 0, MAX_ABSOLUTE_SCALE_VELOCITY_CHANGE); + if (isScalingOut) { + zoomAddition = -zoomAddition; + } + return zoomAddition; + } + } + + private final class RotateGestureListener extends RotateGestureDetector.SimpleOnRotateGestureListener { + private final float minimumScaleSpanWhenRotating; + private final float angularVelocityMultiplier; + private final float minimumAngularVelocity; + private final double rotateVelocityRatioThreshold; + private final float defaultSpanSinceStartThreshold; + + RotateGestureListener(float minimumScaleSpanWhenRotating, double densityMultiplier, float angularVelocityMultiplier, + float minimumAngularVelocity, float defaultSpanSinceStartThreshold) { + this.minimumScaleSpanWhenRotating = minimumScaleSpanWhenRotating; + this.angularVelocityMultiplier = angularVelocityMultiplier; + this.minimumAngularVelocity = minimumAngularVelocity; + this.rotateVelocityRatioThreshold = ROTATE_VELOCITY_RATIO_THRESHOLD * densityMultiplier; + this.defaultSpanSinceStartThreshold = defaultSpanSinceStartThreshold; + } + + @Override + public boolean onRotateBegin(@NonNull RotateGestureDetector detector) { + if (!uiSettings.isRotateGesturesEnabled()) { + return false; + } + + float deltaSinceLast = Math.abs(detector.getDeltaSinceLast()); + double currTime = detector.getCurrentEvent().getEventTime(); + double prevTime = detector.getPreviousEvent().getEventTime(); + if (currTime == prevTime) { + return false; + } + double speed = deltaSinceLast / (currTime - prevTime); + float deltaSinceStart = Math.abs(detector.getDeltaSinceStart()); + + // adjust the responsiveness of a rotation gesture - the higher the speed, the bigger the threshold + if (speed < 0.04 + || (speed > 0.07 && deltaSinceStart < 5) + || (speed > 0.15 && deltaSinceStart < 7) + || (speed > 0.5 && deltaSinceStart < 15) + ) { + return false; + } + + if (uiSettings.isIncreaseScaleThresholdWhenRotating()) { + // when rotation starts, interrupting scale and increasing the threshold + // to make rotation without scaling easier + gesturesManager.getStandardScaleGestureDetector().setSpanSinceStartThreshold(minimumScaleSpanWhenRotating); + gesturesManager.getStandardScaleGestureDetector().interrupt(); + } + + cancelTransitionsIfRequired(); + + notifyOnRotateBeginListeners(detector); + + return true; + } + + @Override + public boolean onRotate(@NonNull RotateGestureDetector detector, float rotationDegreesSinceLast, + float rotationDegreesSinceFirst) { + // dispatching camera start event only when the movement actually occurred + cameraChangeDispatcher.onCameraMoveStarted(CameraChangeDispatcher.REASON_API_GESTURE); + + // Calculate map bearing value + double bearing = transform.getRawBearing() + rotationDegreesSinceLast; + + // Rotate the map + PointF focalPoint = getRotateFocalPoint(detector); + transform.setBearing(bearing, focalPoint.x, focalPoint.y); + + notifyOnRotateListeners(detector); + + return true; + } + + @Override + public void onRotateEnd(@NonNull RotateGestureDetector detector, float velocityX, + float velocityY, float angularVelocity) { + if (uiSettings.isIncreaseScaleThresholdWhenRotating()) { + // resetting default scale threshold values + gesturesManager.getStandardScaleGestureDetector().setSpanSinceStartThreshold(defaultSpanSinceStartThreshold); + } + + notifyOnRotateEndListeners(detector); + + angularVelocity = angularVelocity * angularVelocityMultiplier; + angularVelocity = MathUtils.clamp(angularVelocity, -MAXIMUM_ANGULAR_VELOCITY, MAXIMUM_ANGULAR_VELOCITY); + + float velocityXY = Math.abs(velocityX) + Math.abs(velocityY); + float delta = Math.abs(detector.getDeltaSinceLast()); + double ratio = delta / velocityXY; + + if (!uiSettings.isRotateVelocityAnimationEnabled() + || Math.abs(angularVelocity) < minimumAngularVelocity + || (gesturesManager.getStandardScaleGestureDetector().isInProgress() && ratio < rotateVelocityRatioThreshold)) { + // notifying listeners that camera is idle only if there is no follow-up animation + dispatchCameraIdle(); + return; + } + + long animationTime = (long) ((Math.log((Math.abs(angularVelocity)) + 1 / Math.pow(Math.E, 2)) + 2) + * SCALE_VELOCITY_ANIMATION_DURATION_MULTIPLIER); + + PointF focalPoint = getRotateFocalPoint(detector); + rotateAnimator = createRotateAnimator(angularVelocity, animationTime, focalPoint); + scheduleAnimator(rotateAnimator); + } + + @NonNull + private PointF getRotateFocalPoint(@NonNull RotateGestureDetector detector) { + if (constantFocalPoint != null) { + // User provided focal point + return constantFocalPoint; + } else { + // around gesture + return detector.getFocalPoint(); + } + } + + private Animator createRotateAnimator(float angularVelocity, long animationTime, + @NonNull final PointF animationFocalPoint) { + ValueAnimator animator = ValueAnimator.ofFloat(angularVelocity, 0f); + animator.setDuration(animationTime); + animator.setInterpolator(new DecelerateInterpolator()); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + transform.setBearing( + transform.getRawBearing() + (float) animation.getAnimatedValue(), + animationFocalPoint.x, animationFocalPoint.y, + 0L + ); + } + }); + + animator.addListener(new AnimatorListenerAdapter() { + + @Override + public void onAnimationStart(Animator animation) { + transform.cancelTransitions(); + cameraChangeDispatcher.onCameraMoveStarted(REASON_API_GESTURE); + } + + @Override + public void onAnimationCancel(Animator animation) { + transform.cancelTransitions(); + } + + @Override + public void onAnimationEnd(Animator animation) { + dispatchCameraIdle(); + } + }); + + return animator; + } + } + + private final class ShoveGestureListener extends ShoveGestureDetector.SimpleOnShoveGestureListener { + @Override + public boolean onShoveBegin(@NonNull ShoveGestureDetector detector) { + if (!uiSettings.isTiltGesturesEnabled()) { + return false; + } + + cancelTransitionsIfRequired(); + + // disabling move gesture during shove + gesturesManager.getMoveGestureDetector().setEnabled(false); + + notifyOnShoveBeginListeners(detector); + + return true; + } + + @Override + public boolean onShove(@NonNull ShoveGestureDetector detector, + float deltaPixelsSinceLast, float deltaPixelsSinceStart) { + // dispatching camera start event only when the movement actually occurred + cameraChangeDispatcher.onCameraMoveStarted(CameraChangeDispatcher.REASON_API_GESTURE); + + // Get tilt value (scale and clamp) + double pitch = transform.getTilt(); + pitch -= MapboxConstants.SHOVE_PIXEL_CHANGE_FACTOR * deltaPixelsSinceLast; + pitch = MathUtils.clamp(pitch, MapboxConstants.MINIMUM_TILT, MapboxConstants.MAXIMUM_TILT); + + // Tilt the map + transform.setTilt(pitch); + + notifyOnShoveListeners(detector); + + return true; + } + + @Override + public void onShoveEnd(@NonNull ShoveGestureDetector detector, float velocityX, float velocityY) { + dispatchCameraIdle(); + + // re-enabling move gesture + gesturesManager.getMoveGestureDetector().setEnabled(true); + + notifyOnShoveEndListeners(detector); + } + } + + private final class TapGestureListener implements MultiFingerTapGestureDetector.OnMultiFingerTapGestureListener { + @Override + public boolean onMultiFingerTap(@NonNull MultiFingerTapGestureDetector detector, int pointersCount) { + if (!uiSettings.isZoomGesturesEnabled() || pointersCount != 2) { + return false; + } + + transform.cancelTransitions(); + cameraChangeDispatcher.onCameraMoveStarted(REASON_API_GESTURE); + + PointF zoomFocalPoint; + // Single finger double tap + if (constantFocalPoint != null) { + // User provided focal point + zoomFocalPoint = constantFocalPoint; + } else { + // Zoom in on gesture + zoomFocalPoint = detector.getFocalPoint(); + } + + zoomOutAnimated(zoomFocalPoint, false); + + return true; + } + } + + private Animator createScaleAnimator(double currentZoom, double zoomAddition, + @NonNull final PointF animationFocalPoint, long animationTime) { + ValueAnimator animator = ValueAnimator.ofFloat((float) currentZoom, (float) (currentZoom + zoomAddition)); + animator.setDuration(animationTime); + animator.setInterpolator(new DecelerateInterpolator()); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + transform.setZoom((Float) animation.getAnimatedValue(), animationFocalPoint); + } + }); + + animator.addListener(new AnimatorListenerAdapter() { + + @Override + public void onAnimationStart(Animator animation) { + transform.cancelTransitions(); + cameraChangeDispatcher.onCameraMoveStarted(REASON_API_GESTURE); + } + + @Override + public void onAnimationCancel(Animator animation) { + transform.cancelTransitions(); + } + + @Override + public void onAnimationEnd(Animator animation) { + dispatchCameraIdle(); + } + }); + return animator; + } + + /** + * Zoom in by 1. + * + * @param zoomFocalPoint focal point of zoom animation + * @param runImmediately if true, animation will be started right away, otherwise it will wait until + * {@link MotionEvent#ACTION_UP} is registered. + */ + void zoomInAnimated(@NonNull PointF zoomFocalPoint, boolean runImmediately) { + zoomAnimated(true, zoomFocalPoint, runImmediately); + } + + /** + * Zoom out by 1. + * + * @param zoomFocalPoint focal point of zoom animation + * @param runImmediately if true, animation will be started right away, otherwise it will wait until + * {@link MotionEvent#ACTION_UP} is registered. + */ + void zoomOutAnimated(@NonNull PointF zoomFocalPoint, boolean runImmediately) { + zoomAnimated(false, zoomFocalPoint, runImmediately); + } + + private void zoomAnimated(boolean zoomIn, @NonNull PointF zoomFocalPoint, boolean runImmediately) { + //canceling here as well, because when using a button it will not be canceled automatically by onDown() + cancelAnimator(scaleAnimator); + + double currentZoom = transform.getRawZoom(); + scaleAnimator = createScaleAnimator( + currentZoom, + zoomIn ? 1 : -1, + zoomFocalPoint, + MapboxConstants.ANIMATION_DURATION); + if (runImmediately) { + scaleAnimator.start(); + } else { + scheduleAnimator(scaleAnimator); + } + } + + private void dispatchCameraIdle() { + // we need to dispatch camera idle callback only if there is no other gestures in progress + if (noGesturesInProgress()) { + // invalidate the camera position, so that it's valid when fetched from the #onIdle event + // and doesn't rely on the last frame being rendered + transform.invalidateCameraPosition(); + cameraChangeDispatcher.onCameraIdle(); + } + } + + private void cancelTransitionsIfRequired() { + // we need to cancel core transitions only if there is no started gesture yet + if (noGesturesInProgress()) { + transform.cancelTransitions(); + } + } + + private boolean noGesturesInProgress() { + return (!uiSettings.isScrollGesturesEnabled() || !gesturesManager.getMoveGestureDetector().isInProgress()) + && (!uiSettings.isZoomGesturesEnabled() || !gesturesManager.getStandardScaleGestureDetector().isInProgress()) + && (!uiSettings.isRotateGesturesEnabled() || !gesturesManager.getRotateGestureDetector().isInProgress()) + && (!uiSettings.isTiltGesturesEnabled() || !gesturesManager.getShoveGestureDetector().isInProgress()); + } + + void notifyOnMapClickListeners(@NonNull PointF tapPoint) { + for (MapboxMap.OnMapClickListener listener : onMapClickListenerList) { + if (listener.onMapClick(projection.fromScreenLocation(tapPoint))) { + return; + } + } + } + + void notifyOnMapLongClickListeners(@NonNull PointF longClickPoint) { + for (MapboxMap.OnMapLongClickListener listener : onMapLongClickListenerList) { + if (listener.onMapLongClick(projection.fromScreenLocation(longClickPoint))) { + return; + } + } + } + + void notifyOnFlingListeners() { + for (MapboxMap.OnFlingListener listener : onFlingListenerList) { + listener.onFling(); + } + } + + void notifyOnMoveBeginListeners(@NonNull MoveGestureDetector detector) { + for (MapboxMap.OnMoveListener listener : onMoveListenerList) { + listener.onMoveBegin(detector); + } + } + + void notifyOnMoveListeners(@NonNull MoveGestureDetector detector) { + for (MapboxMap.OnMoveListener listener : onMoveListenerList) { + listener.onMove(detector); + } + } + + void notifyOnMoveEndListeners(@NonNull MoveGestureDetector detector) { + for (MapboxMap.OnMoveListener listener : onMoveListenerList) { + listener.onMoveEnd(detector); + } + } + + void notifyOnRotateBeginListeners(@NonNull RotateGestureDetector detector) { + for (MapboxMap.OnRotateListener listener : onRotateListenerList) { + listener.onRotateBegin(detector); + } + } + + void notifyOnRotateListeners(@NonNull RotateGestureDetector detector) { + for (MapboxMap.OnRotateListener listener : onRotateListenerList) { + listener.onRotate(detector); + } + } + + void notifyOnRotateEndListeners(@NonNull RotateGestureDetector detector) { + for (MapboxMap.OnRotateListener listener : onRotateListenerList) { + listener.onRotateEnd(detector); + } + } + + void notifyOnScaleBeginListeners(@NonNull StandardScaleGestureDetector detector) { + for (MapboxMap.OnScaleListener listener : onScaleListenerList) { + listener.onScaleBegin(detector); + } + } + + void notifyOnScaleListeners(@NonNull StandardScaleGestureDetector detector) { + for (MapboxMap.OnScaleListener listener : onScaleListenerList) { + listener.onScale(detector); + } + } + + void notifyOnScaleEndListeners(@NonNull StandardScaleGestureDetector detector) { + for (MapboxMap.OnScaleListener listener : onScaleListenerList) { + listener.onScaleEnd(detector); + } + } + + void notifyOnShoveBeginListeners(@NonNull ShoveGestureDetector detector) { + for (MapboxMap.OnShoveListener listener : onShoveListenerList) { + listener.onShoveBegin(detector); + } + } + + void notifyOnShoveListeners(@NonNull ShoveGestureDetector detector) { + for (MapboxMap.OnShoveListener listener : onShoveListenerList) { + listener.onShove(detector); + } + } + + void notifyOnShoveEndListeners(@NonNull ShoveGestureDetector detector) { + for (MapboxMap.OnShoveListener listener : onShoveListenerList) { + listener.onShoveEnd(detector); + } + } + + void addOnMapClickListener(MapboxMap.OnMapClickListener onMapClickListener) { + onMapClickListenerList.add(onMapClickListener); + } + + void removeOnMapClickListener(MapboxMap.OnMapClickListener onMapClickListener) { + onMapClickListenerList.remove(onMapClickListener); + } + + void addOnMapLongClickListener(MapboxMap.OnMapLongClickListener onMapLongClickListener) { + onMapLongClickListenerList.add(onMapLongClickListener); + } + + void removeOnMapLongClickListener(MapboxMap.OnMapLongClickListener onMapLongClickListener) { + onMapLongClickListenerList.remove(onMapLongClickListener); + } + + void addOnFlingListener(MapboxMap.OnFlingListener onFlingListener) { + onFlingListenerList.add(onFlingListener); + } + + void removeOnFlingListener(MapboxMap.OnFlingListener onFlingListener) { + onFlingListenerList.remove(onFlingListener); + } + + void addOnMoveListener(MapboxMap.OnMoveListener listener) { + onMoveListenerList.add(listener); + } + + void removeOnMoveListener(MapboxMap.OnMoveListener listener) { + onMoveListenerList.remove(listener); + } + + void addOnRotateListener(MapboxMap.OnRotateListener listener) { + onRotateListenerList.add(listener); + } + + void removeOnRotateListener(MapboxMap.OnRotateListener listener) { + onRotateListenerList.remove(listener); + } + + void addOnScaleListener(MapboxMap.OnScaleListener listener) { + onScaleListenerList.add(listener); + } + + void removeOnScaleListener(MapboxMap.OnScaleListener listener) { + onScaleListenerList.remove(listener); + } + + void addShoveListener(MapboxMap.OnShoveListener listener) { + onShoveListenerList.add(listener); + } + + void removeShoveListener(MapboxMap.OnShoveListener listener) { + onShoveListenerList.remove(listener); + } + + AndroidGesturesManager getGesturesManager() { + return gesturesManager; + } + + void setGesturesManager(@NonNull Context context, @NonNull AndroidGesturesManager gesturesManager, + boolean attachDefaultListeners, boolean setDefaultMutuallyExclusives) { + initializeGesturesManager(gesturesManager, setDefaultMutuallyExclusives); + initializeGestureListeners(context, attachDefaultListeners); + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapKeyListener.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapKeyListener.java new file mode 100644 index 000000000..afb80027d --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapKeyListener.java @@ -0,0 +1,271 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.PointF; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +/** + * Manages key events on a MapView. + *

+ *

    + *
  • Uses {@link Transform} to change the map state
  • + *
  • Uses {@link UiSettings} to verify validity of user restricted movement.
  • + *
+ *

+ */ +final class MapKeyListener { + + private final Transform transform; + private final UiSettings uiSettings; + private final MapGestureDetector mapGestureDetector; + + @Nullable + private TrackballLongPressTimeOut currentTrackballLongPressTimeOut; + + MapKeyListener(Transform transform, UiSettings uiSettings, MapGestureDetector mapGestureDetector) { + this.transform = transform; + this.uiSettings = uiSettings; + this.mapGestureDetector = mapGestureDetector; + } + + /** + * Called when the user presses a key, alse called for repeated keys held down. + * + * @param keyCode the id of the pressed key + * @param event the related key event + * @return true if the wevent is handled + */ + boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { + // If the user has held the scroll key down for a while then accelerate + // the scroll speed + double scrollDist = event.getRepeatCount() >= 5 ? 50.0 : 10.0; + + // Check which key was pressed via hardware/real key code + switch (keyCode) { + // Tell the system to track these keys for long presses on + // onKeyLongPress is fired + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + event.startTracking(); + return true; + + case KeyEvent.KEYCODE_DPAD_LEFT: + if (!uiSettings.isScrollGesturesEnabled()) { + return false; + } + + // Cancel any animation + transform.cancelTransitions(); + + // Move left + transform.moveBy(scrollDist, 0.0, 0 /*no animation*/); + return true; + + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (!uiSettings.isScrollGesturesEnabled()) { + return false; + } + + // Cancel any animation + transform.cancelTransitions(); + + // Move right + transform.moveBy(-scrollDist, 0.0, 0 /*no animation*/); + return true; + + case KeyEvent.KEYCODE_DPAD_UP: + if (!uiSettings.isScrollGesturesEnabled()) { + return false; + } + + // Cancel any animation + transform.cancelTransitions(); + + // Move up + transform.moveBy(0.0, scrollDist, 0 /*no animation*/); + return true; + + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!uiSettings.isScrollGesturesEnabled()) { + return false; + } + + // Cancel any animation + transform.cancelTransitions(); + + // Move down + transform.moveBy(0.0, -scrollDist, 0 /*no animation*/); + return true; + + default: + // We are not interested in this key + return false; + } + } + + /** + * Called when the user long presses a key that is being tracked. + * + * @param keyCode the id of the long pressed key + * @param event the related key event + * @return true if event is handled + */ + boolean onKeyLongPress(int keyCode, KeyEvent event) { + // Check which key was pressed via hardware/real key code + switch (keyCode) { + // Tell the system to track these keys for long presses on + // onKeyLongPress is fired + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (!uiSettings.isZoomGesturesEnabled()) { + return false; + } + + // Zoom out + PointF focalPoint = new PointF(uiSettings.getWidth() / 2, uiSettings.getHeight() / 2); + mapGestureDetector.zoomOutAnimated(focalPoint, true); + return true; + + default: + // We are not interested in this key + return false; + } + } + + /** + * Called when the user releases a key. + * + * @param keyCode the id of the released key + * @param event the related key event + * @return true if the event is handled + */ + boolean onKeyUp(int keyCode, KeyEvent event) { + // Check if the key action was canceled (used for virtual keyboards) + if (event.isCanceled()) { + return false; + } + + // Check which key was pressed via hardware/real key code + // Note if keyboard does not have physical key (ie primary non-shifted + // key) then it will not appear here + // Must use the key character map as physical to character is not + // fixed/guaranteed + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + if (!uiSettings.isZoomGesturesEnabled()) { + return false; + } + + // Zoom in + PointF focalPoint = new PointF(uiSettings.getWidth() / 2, uiSettings.getHeight() / 2); + mapGestureDetector.zoomInAnimated(focalPoint, true); + return true; + } + + // We are not interested in this key + return false; + } + + /** + * Called for trackball events, all motions are relative in device specific units. + * + * @param event the related motion event + * @return true if the event is handled + */ + boolean onTrackballEvent(MotionEvent event) { + // Choose the action + switch (event.getActionMasked()) { + // The trackball was rotated + case MotionEvent.ACTION_MOVE: + if (!uiSettings.isScrollGesturesEnabled()) { + return false; + } + + // Cancel any animation + transform.cancelTransitions(); + + // Scroll the map + transform.moveBy(-10.0 * event.getX(), -10.0 * event.getY(), 0 /*no animation*/); + return true; + + // Trackball was pushed in so start tracking and tell system we are + // interested + // We will then get the up action + case MotionEvent.ACTION_DOWN: + // Set up a delayed callback to check if trackball is still + // After waiting the system long press time out + if (currentTrackballLongPressTimeOut != null) { + currentTrackballLongPressTimeOut.cancel(); + currentTrackballLongPressTimeOut = null; + } + currentTrackballLongPressTimeOut = new TrackballLongPressTimeOut(); + new Handler(Looper.getMainLooper()).postDelayed(currentTrackballLongPressTimeOut, + ViewConfiguration.getLongPressTimeout()); + return true; + + // Trackball was released + case MotionEvent.ACTION_UP: + if (!uiSettings.isZoomGesturesEnabled()) { + return false; + } + + // Only handle if we have not already long pressed + if (currentTrackballLongPressTimeOut != null) { + // Zoom in + PointF focalPoint = new PointF(uiSettings.getWidth() / 2, uiSettings.getHeight() / 2); + mapGestureDetector.zoomInAnimated(focalPoint, true); + } + return true; + + // Trackball was cancelled + case MotionEvent.ACTION_CANCEL: + if (currentTrackballLongPressTimeOut != null) { + currentTrackballLongPressTimeOut.cancel(); + currentTrackballLongPressTimeOut = null; + } + return true; + + default: + // We are not interested in this event + return false; + } + } + + /** + * This class implements the trackball long press time out callback + */ + private class TrackballLongPressTimeOut implements Runnable { + + // Track if we have been cancelled + private boolean cancelled; + + TrackballLongPressTimeOut() { + cancelled = false; + } + + // Cancel the timeout + public void cancel() { + cancelled = true; + } + + // Called when long press time out expires + @Override + public void run() { + // Check if the trackball is still pressed + if (!cancelled) { + // Zoom out + PointF pointF = new PointF(uiSettings.getWidth() / 2, uiSettings.getHeight() / 2); + mapGestureDetector.zoomOutAnimated(pointF, true); + + // Ensure the up action is not run + currentTrackballLongPressTimeOut = null; + } + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java new file mode 100644 index 000000000..4521d2ae6 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java @@ -0,0 +1,1350 @@ +package com.mapbox.mapboxsdk.maps; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.PointF; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v4.util.LongSparseArray; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.TextureView; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.mapbox.android.gestures.AndroidGesturesManager; +import com.mapbox.mapboxsdk.MapStrictMode; +import com.mapbox.mapboxsdk.Mapbox; +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.annotations.Annotation; +import com.mapbox.mapboxsdk.constants.MapboxConstants; +import com.mapbox.mapboxsdk.exceptions.MapboxConfigurationException; +import com.mapbox.mapboxsdk.location.LocationComponent; +import com.mapbox.mapboxsdk.maps.renderer.MapRenderer; +import com.mapbox.mapboxsdk.maps.renderer.glsurfaceview.GLSurfaceViewMapRenderer; +import com.mapbox.mapboxsdk.maps.renderer.glsurfaceview.MapboxGLSurfaceView; +import com.mapbox.mapboxsdk.maps.renderer.textureview.TextureViewMapRenderer; +import com.mapbox.mapboxsdk.maps.widgets.CompassView; +import com.mapbox.mapboxsdk.net.ConnectivityReceiver; +import com.mapbox.mapboxsdk.storage.FileSource; +import com.mapbox.mapboxsdk.utils.BitmapUtils; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import static com.mapbox.mapboxsdk.maps.widgets.CompassView.TIME_MAP_NORTH_ANIMATION; +import static com.mapbox.mapboxsdk.maps.widgets.CompassView.TIME_WAIT_IDLE; + +/** + *

+ * A {@code MapView} provides an embeddable map interface. + * You use this class to display map information and to manipulate the map contents from your application. + * You can center the map on a given coordinate, specify the size of the area you want to display, + * and style the features of the map to fit your application's use case. + *

+ *

+ * Use of {@code MapView} requires a Mapbox API access token. + * Obtain an access token on the Mapbox account page. + *

+ * Warning: Please note that you are responsible for getting permission to use the map data, + * and for ensuring your use adheres to the relevant terms of use. + */ +public class MapView extends FrameLayout implements NativeMapView.ViewCallback { + + private final MapChangeReceiver mapChangeReceiver = new MapChangeReceiver(); + private final MapCallback mapCallback = new MapCallback(); + private final InitialRenderCallback initialRenderCallback = new InitialRenderCallback(); + + @Nullable + private NativeMap nativeMapView; + @Nullable + private MapboxMap mapboxMap; + private AttributionClickListener attributionClickListener; + private MapboxMapOptions mapboxMapOptions; + private MapRenderer mapRenderer; + private boolean destroyed; + + private CompassView compassView; + private PointF focalPoint; + private ImageView attrView; + private ImageView logoView; + + @Nullable + private MapGestureDetector mapGestureDetector; + @Nullable + private MapKeyListener mapKeyListener; + @Nullable + private Bundle savedInstanceState; + private boolean isStarted; + + @UiThread + public MapView(@NonNull Context context) { + super(context); + initialize(context, MapboxMapOptions.createFromAttributes(context)); + } + + @UiThread + public MapView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initialize(context, MapboxMapOptions.createFromAttributes(context, attrs)); + } + + @UiThread + public MapView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initialize(context, MapboxMapOptions.createFromAttributes(context, attrs)); + } + + @UiThread + public MapView(@NonNull Context context, @Nullable MapboxMapOptions options) { + super(context); + initialize(context, options == null ? MapboxMapOptions.createFromAttributes(context) : options); + } + + @CallSuper + @UiThread + protected void initialize(@NonNull final Context context, @NonNull final MapboxMapOptions options) { + if (isInEditMode()) { + // in IDE layout editor, just return + return; + } + + if (!Mapbox.hasInstance()) { + throw new MapboxConfigurationException(); + } + + // hide surface until map is fully loaded #10990 + setForeground(new ColorDrawable(options.getForegroundLoadColor())); + + mapboxMapOptions = options; + + // inflate view + View view = LayoutInflater.from(context).inflate(R.layout.mapbox_mapview_internal, this); + compassView = view.findViewById(R.id.compassView); + attrView = view.findViewById(R.id.attributionView); + attrView.setImageDrawable(BitmapUtils.getDrawableFromRes(getContext(), R.drawable.mapbox_info_bg_selector)); + logoView = view.findViewById(R.id.logoView); + logoView.setImageDrawable(BitmapUtils.getDrawableFromRes(getContext(), R.drawable.mapbox_logo_icon)); + + // add accessibility support + setContentDescription(context.getString(R.string.mapbox_mapActionDescription)); + setWillNotDraw(false); + initialiseDrawingSurface(options); + } + + private void initialiseMap() { + Context context = getContext(); + + // callback for focal point invalidation + final FocalPointInvalidator focalInvalidator = new FocalPointInvalidator(); + focalInvalidator.addListener(createFocalPointChangeListener()); + + // callback for registering touch listeners + GesturesManagerInteractionListener registerTouchListener = new GesturesManagerInteractionListener(); + + // callback for camera change events + final CameraChangeDispatcher cameraDispatcher = new CameraChangeDispatcher(); + + // setup components for MapboxMap creation + Projection proj = new Projection(nativeMapView, this); + UiSettings uiSettings = new UiSettings(proj, focalInvalidator, compassView, attrView, logoView, getPixelRatio()); + LongSparseArray annotationsArray = new LongSparseArray<>(); + IconManager iconManager = new IconManager(nativeMapView); + Annotations annotations = new AnnotationContainer(nativeMapView, annotationsArray); + Markers markers = new MarkerContainer(nativeMapView, annotationsArray, iconManager); + Polygons polygons = new PolygonContainer(nativeMapView, annotationsArray); + Polylines polylines = new PolylineContainer(nativeMapView, annotationsArray); + ShapeAnnotations shapeAnnotations = new ShapeAnnotationContainer(nativeMapView, annotationsArray); + AnnotationManager annotationManager = new AnnotationManager(this, annotationsArray, iconManager, + annotations, markers, polygons, polylines, shapeAnnotations); + Transform transform = new Transform(this, nativeMapView, cameraDispatcher); + + // MapboxMap + List developerAnimationListeners = new ArrayList<>(); + mapboxMap = new MapboxMap(nativeMapView, transform, uiSettings, proj, registerTouchListener, cameraDispatcher, + developerAnimationListeners); + mapboxMap.injectAnnotationManager(annotationManager); + + // user input + mapGestureDetector = new MapGestureDetector(context, transform, proj, uiSettings, + annotationManager, cameraDispatcher); + mapKeyListener = new MapKeyListener(transform, uiSettings, mapGestureDetector); + + // compass + compassView.injectCompassAnimationListener(createCompassAnimationListener(cameraDispatcher)); + compassView.setOnClickListener(createCompassClickListener(cameraDispatcher)); + + // LocationComponent + mapboxMap.injectLocationComponent(new LocationComponent(mapboxMap, transform, developerAnimationListeners)); + + // inject widgets with MapboxMap + attrView.setOnClickListener(attributionClickListener = new AttributionClickListener(context, mapboxMap)); + + // Ensure this view is interactable + setClickable(true); + setLongClickable(true); + setFocusable(true); + setFocusableInTouchMode(true); + requestDisallowInterceptTouchEvent(true); + + // notify Map object about current connectivity state + nativeMapView.setReachability(Mapbox.isConnected()); + + // initialise MapboxMap + if (savedInstanceState == null) { + mapboxMap.initialise(context, mapboxMapOptions); + } else { + mapboxMap.onRestoreInstanceState(savedInstanceState); + } + + mapCallback.initialised(); + } + + private FocalPointChangeListener createFocalPointChangeListener() { + return new FocalPointChangeListener() { + @Override + public void onFocalPointChanged(PointF pointF) { + focalPoint = pointF; + } + }; + } + + private MapboxMap.OnCompassAnimationListener createCompassAnimationListener(@NonNull final CameraChangeDispatcher + cameraChangeDispatcher) { + return new MapboxMap.OnCompassAnimationListener() { + @Override + public void onCompassAnimation() { + cameraChangeDispatcher.onCameraMove(); + } + + @Override + public void onCompassAnimationFinished() { + compassView.isAnimating(false); + cameraChangeDispatcher.onCameraIdle(); + } + }; + } + + private OnClickListener createCompassClickListener(@NonNull final CameraChangeDispatcher cameraChangeDispatcher) { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (mapboxMap != null && compassView != null) { + if (focalPoint != null) { + mapboxMap.setFocalBearing(0, focalPoint.x, focalPoint.y, TIME_MAP_NORTH_ANIMATION); + } else { + mapboxMap.setFocalBearing(0, mapboxMap.getWidth() / 2, mapboxMap.getHeight() / 2, TIME_MAP_NORTH_ANIMATION); + } + cameraChangeDispatcher.onCameraMoveStarted(MapboxMap.OnCameraMoveStartedListener.REASON_API_ANIMATION); + compassView.isAnimating(true); + compassView.postDelayed(compassView, TIME_WAIT_IDLE + TIME_MAP_NORTH_ANIMATION); + } + } + }; + } + + // + // Lifecycle events + // + + /** + *

+ * You must call this method from the parent's Activity#onCreate(Bundle)} or + * Fragment#onViewCreated(View, Bundle). + *

+ * You must set a valid access token with {@link com.mapbox.mapboxsdk.Mapbox#getInstance(Context, String)} + * before you call this method or an exception will be thrown. + * + * @param savedInstanceState Pass in the parent's savedInstanceState. + * @see com.mapbox.mapboxsdk.Mapbox#getInstance(Context, String) + */ + @UiThread + public void onCreate(@Nullable Bundle savedInstanceState) { + if (savedInstanceState == null) { + TelemetryDefinition telemetry = Mapbox.getTelemetry(); + if (telemetry != null) { + telemetry.onAppUserTurnstileEvent(); + } + } else if (savedInstanceState.getBoolean(MapboxConstants.STATE_HAS_SAVED_STATE)) { + this.savedInstanceState = savedInstanceState; + } + } + + private void initialiseDrawingSurface(MapboxMapOptions options) { + String localFontFamily = options.getLocalIdeographFontFamily(); + if (options.getTextureMode()) { + TextureView textureView = new TextureView(getContext()); + boolean translucentSurface = options.getTranslucentTextureSurface(); + mapRenderer = new TextureViewMapRenderer(getContext(), + textureView, localFontFamily, translucentSurface) { + @Override + protected void onSurfaceCreated(GL10 gl, EGLConfig config) { + MapView.this.onSurfaceCreated(); + super.onSurfaceCreated(gl, config); + } + }; + + addView(textureView, 0); + } else { + MapboxGLSurfaceView glSurfaceView = new MapboxGLSurfaceView(getContext()); + glSurfaceView.setZOrderMediaOverlay(mapboxMapOptions.getRenderSurfaceOnTop()); + mapRenderer = new GLSurfaceViewMapRenderer(getContext(), glSurfaceView, localFontFamily) { + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + MapView.this.onSurfaceCreated(); + super.onSurfaceCreated(gl, config); + } + }; + + addView(glSurfaceView, 0); + } + + boolean crossSourceCollisions = mapboxMapOptions.getCrossSourceCollisions(); + nativeMapView = new NativeMapView( + getContext(), getPixelRatio(), crossSourceCollisions, this, mapChangeReceiver, mapRenderer + ); + } + + private void onSurfaceCreated() { + post(new Runnable() { + @Override + public void run() { + // Initialise only when not destroyed and only once + if (!destroyed && mapboxMap == null) { + MapView.this.initialiseMap(); + mapboxMap.onStart(); + } + } + }); + } + + /** + * You must call this method from the parent's Activity#onSaveInstanceState(Bundle) + * or Fragment#onSaveInstanceState(Bundle). + * + * @param outState Pass in the parent's outState. + */ + @UiThread + public void onSaveInstanceState(@NonNull Bundle outState) { + if (mapboxMap != null) { + outState.putBoolean(MapboxConstants.STATE_HAS_SAVED_STATE, true); + mapboxMap.onSaveInstanceState(outState); + } + } + + /** + * You must call this method from the parent's Activity#onStart() or Fragment#onStart() + */ + @UiThread + public void onStart() { + if (!isStarted) { + ConnectivityReceiver.instance(getContext()).activate(); + FileSource.getInstance(getContext()).activate(); + isStarted = true; + } + if (mapboxMap != null) { + mapboxMap.onStart(); + } + + if (mapRenderer != null) { + mapRenderer.onStart(); + } + } + + /** + * You must call this method from the parent's Activity#onResume() or Fragment#onResume(). + */ + @UiThread + public void onResume() { + if (mapRenderer != null) { + mapRenderer.onResume(); + } + } + + /** + * You must call this method from the parent's Activity#onPause() or Fragment#onPause(). + */ + @UiThread + public void onPause() { + if (mapRenderer != null) { + mapRenderer.onPause(); + } + } + + /** + * You must call this method from the parent's Activity#onStop() or Fragment#onStop(). + */ + @UiThread + public void onStop() { + if (attributionClickListener != null) { + attributionClickListener.onStop(); + } + + if (mapboxMap != null) { + // map was destroyed before it was started + mapGestureDetector.cancelAnimators(); + mapboxMap.onStop(); + } + + if (mapRenderer != null) { + mapRenderer.onStop(); + } + + if (isStarted) { + ConnectivityReceiver.instance(getContext()).deactivate(); + FileSource.getInstance(getContext()).deactivate(); + isStarted = false; + } + } + + /** + * You must call this method from the parent's Activity#onDestroy() or Fragment#onDestroyView(). + */ + @UiThread + public void onDestroy() { + destroyed = true; + mapChangeReceiver.clear(); + mapCallback.onDestroy(); + initialRenderCallback.onDestroy(); + + if (compassView != null) { + // avoid leaking context through animator #13742 + compassView.resetAnimation(); + } + + if (mapboxMap != null) { + mapboxMap.onDestroy(); + } + + if (nativeMapView != null) { + // null when destroying an activity programmatically mapbox-navigation-android/issues/503 + nativeMapView.destroy(); + nativeMapView = null; + } + + if (mapRenderer != null) { + mapRenderer.onDestroy(); + } + } + + /** + * The maximum frame rate at which the map view is rendered, + * but it can't excess the ability of device hardware. + * + * @param maximumFps Can be set to arbitrary integer values. + */ + public void setMaximumFps(int maximumFps) { + if (mapRenderer != null) { + mapRenderer.setMaximumFps(maximumFps); + } else { + throw new IllegalStateException("Calling MapView#setMaximumFps before mapRenderer is created."); + } + } + + /** + * Returns if the map has been destroyed. + *

+ * This method can be used to determine if the result of an asynchronous operation should be set. + *

+ * + * @return true, if the map has been destroyed + */ + @UiThread + public boolean isDestroyed() { + return destroyed; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isGestureDetectorInitialized()) { + return super.onTouchEvent(event); + } + + return mapGestureDetector.onTouchEvent(event) || super.onTouchEvent(event); + } + + @Override + public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { + return mapKeyListener.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return mapKeyListener.onKeyLongPress(keyCode, event) || super.onKeyLongPress(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { + return mapKeyListener.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event); + } + + @Override + public boolean onTrackballEvent(@NonNull MotionEvent event) { + return mapKeyListener.onTrackballEvent(event) || super.onTrackballEvent(event); + } + + @Override + public boolean onGenericMotionEvent(@NonNull MotionEvent event) { + if (!isGestureDetectorInitialized()) { + return super.onGenericMotionEvent(event); + } + return mapGestureDetector.onGenericMotionEvent(event) || super.onGenericMotionEvent(event); + } + + /** + * You must call this method from the parent's Activity#onLowMemory() or Fragment#onLowMemory(). + */ + @UiThread + public void onLowMemory() { + if (nativeMapView != null && mapboxMap != null && !destroyed) { + nativeMapView.onLowMemory(); + } + } + + // + // Rendering + // + + @Override + protected void onSizeChanged(int width, int height, int oldw, int oldh) { + if (!isInEditMode() && nativeMapView != null) { + // null-checking the nativeMapView, see #13277 + nativeMapView.resizeView(width, height); + } + } + + private float getPixelRatio() { + // check is user defined his own pixel ratio value + float pixelRatio = mapboxMapOptions.getPixelRatio(); + if (pixelRatio == 0) { + // if not, get the one defined by the system + pixelRatio = getResources().getDisplayMetrics().density; + } + return pixelRatio; + } + + // + // ViewCallback + // + + @Nullable + @Override + public Bitmap getViewContent() { + return BitmapUtils.createBitmapFromView(this); + } + + // + // Map events + // + + /** + * Set a callback that's invoked when the camera region will change. + * + * @param listener The callback that's invoked when the camera region will change + */ + public void addOnCameraWillChangeListener(@NonNull OnCameraWillChangeListener listener) { + mapChangeReceiver.addOnCameraWillChangeListener(listener); + } + + /** + * Remove a callback that's invoked when the camera region will change. + * + * @param listener The callback that's invoked when the camera region will change + */ + public void removeOnCameraWillChangeListener(@NonNull OnCameraWillChangeListener listener) { + mapChangeReceiver.removeOnCameraWillChangeListener(listener); + } + + /** + * Set a callback that's invoked when the camera is changing. + * + * @param listener The callback that's invoked when the camera is changing + */ + public void addOnCameraIsChangingListener(@NonNull OnCameraIsChangingListener listener) { + mapChangeReceiver.addOnCameraIsChangingListener(listener); + } + + /** + * Remove a callback that's invoked when the camera is changing. + * + * @param listener The callback that's invoked when the camera is changing + */ + public void removeOnCameraIsChangingListener(@NonNull OnCameraIsChangingListener listener) { + mapChangeReceiver.removeOnCameraIsChangingListener(listener); + } + + /** + * Set a callback that's invoked when the camera region did change. + * + * @param listener The callback that's invoked when the camera region did change + */ + public void addOnCameraDidChangeListener(@NonNull OnCameraDidChangeListener listener) { + mapChangeReceiver.addOnCameraDidChangeListener(listener); + } + + /** + * Set a callback that's invoked when the camera region did change. + * + * @param listener The callback that's invoked when the camera region did change + */ + public void removeOnCameraDidChangeListener(@NonNull OnCameraDidChangeListener listener) { + mapChangeReceiver.removeOnCameraDidChangeListener(listener); + } + + /** + * Set a callback that's invoked when the map will start loading. + * + * @param listener The callback that's invoked when the map will start loading + */ + public void addOnWillStartLoadingMapListener(@NonNull OnWillStartLoadingMapListener listener) { + mapChangeReceiver.addOnWillStartLoadingMapListener(listener); + } + + /** + * Set a callback that's invoked when the map will start loading. + * + * @param listener The callback that's invoked when the map will start loading + */ + public void removeOnWillStartLoadingMapListener(@NonNull OnWillStartLoadingMapListener listener) { + mapChangeReceiver.removeOnWillStartLoadingMapListener(listener); + } + + /** + * Set a callback that's invoked when the map has finished loading. + * + * @param listener The callback that's invoked when the map has finished loading + */ + public void addOnDidFinishLoadingMapListener(@NonNull OnDidFinishLoadingMapListener listener) { + mapChangeReceiver.addOnDidFinishLoadingMapListener(listener); + } + + /** + * Set a callback that's invoked when the map has finished loading. + * + * @param listener The callback that's invoked when the map has finished loading + */ + public void removeOnDidFinishLoadingMapListener(@NonNull OnDidFinishLoadingMapListener listener) { + mapChangeReceiver.removeOnDidFinishLoadingMapListener(listener); + } + + /** + * Set a callback that's invoked when the map failed to load. + * + * @param listener The callback that's invoked when the map failed to load + */ + public void addOnDidFailLoadingMapListener(@NonNull OnDidFailLoadingMapListener listener) { + mapChangeReceiver.addOnDidFailLoadingMapListener(listener); + } + + /** + * Set a callback that's invoked when the map failed to load. + * + * @param listener The callback that's invoked when the map failed to load + */ + public void removeOnDidFailLoadingMapListener(@NonNull OnDidFailLoadingMapListener listener) { + mapChangeReceiver.removeOnDidFailLoadingMapListener(listener); + } + + /** + * Set a callback that's invoked when the map will start rendering a frame. + * + * @param listener The callback that's invoked when the camera will start rendering a frame + */ + public void addOnWillStartRenderingFrameListener(@NonNull OnWillStartRenderingFrameListener listener) { + mapChangeReceiver.addOnWillStartRenderingFrameListener(listener); + } + + /** + * Set a callback that's invoked when the map will start rendering a frame. + * + * @param listener The callback that's invoked when the camera will start rendering a frame + */ + public void removeOnWillStartRenderingFrameListener(@NonNull OnWillStartRenderingFrameListener listener) { + mapChangeReceiver.removeOnWillStartRenderingFrameListener(listener); + } + + /** + * Set a callback that's invoked when the map has finished rendering a frame. + * + * @param listener The callback that's invoked when the map has finished rendering a frame + */ + public void addOnDidFinishRenderingFrameListener(@NonNull OnDidFinishRenderingFrameListener listener) { + mapChangeReceiver.addOnDidFinishRenderingFrameListener(listener); + } + + /** + * Set a callback that's invoked when the map has finished rendering a frame. + * + * @param listener The callback that's invoked when the map has finished rendering a frame + */ + public void removeOnDidFinishRenderingFrameListener(@NonNull OnDidFinishRenderingFrameListener listener) { + mapChangeReceiver.removeOnDidFinishRenderingFrameListener(listener); + } + + /** + * Set a callback that's invoked when the map will start rendering. + * + * @param listener The callback that's invoked when the map will start rendering + */ + public void addOnWillStartRenderingMapListener(@NonNull OnWillStartRenderingMapListener listener) { + mapChangeReceiver.addOnWillStartRenderingMapListener(listener); + } + + /** + * Set a callback that's invoked when the map will start rendering. + * + * @param listener The callback that's invoked when the map will start rendering + */ + public void removeOnWillStartRenderingMapListener(@NonNull OnWillStartRenderingMapListener listener) { + mapChangeReceiver.removeOnWillStartRenderingMapListener(listener); + } + + /** + * Set a callback that's invoked when the map has finished rendering. + * + * @param listener The callback that's invoked when the map has finished rendering + */ + public void addOnDidFinishRenderingMapListener(@NonNull OnDidFinishRenderingMapListener listener) { + mapChangeReceiver.addOnDidFinishRenderingMapListener(listener); + } + + /** + * Remove a callback that's invoked when the map has finished rendering. + * + * @param listener The callback that's invoked when the map has has finished rendering. + */ + public void removeOnDidFinishRenderingMapListener(OnDidFinishRenderingMapListener listener) { + mapChangeReceiver.removeOnDidFinishRenderingMapListener(listener); + } + + /** + * Set a callback that's invoked when the map has entered the idle state. + * + * @param listener The callback that's invoked when the map has entered the idle state. + */ + public void addOnDidBecomeIdleListener(@NonNull OnDidBecomeIdleListener listener) { + mapChangeReceiver.addOnDidBecomeIdleListener(listener); + } + + /** + * Remove a callback that's invoked when the map has entered the idle state. + * + * @param listener The callback that's invoked when the map has entered the idle state. + */ + public void removeOnDidBecomeIdleListener(@NonNull OnDidBecomeIdleListener listener) { + mapChangeReceiver.removeOnDidBecomeIdleListener(listener); + } + + /** + * Set a callback that's invoked when the style has finished loading. + * + * @param listener The callback that's invoked when the style has finished loading + */ + public void addOnDidFinishLoadingStyleListener(@NonNull OnDidFinishLoadingStyleListener listener) { + mapChangeReceiver.addOnDidFinishLoadingStyleListener(listener); + } + + /** + * Set a callback that's invoked when the style has finished loading. + * + * @param listener The callback that's invoked when the style has finished loading + */ + public void removeOnDidFinishLoadingStyleListener(@NonNull OnDidFinishLoadingStyleListener listener) { + mapChangeReceiver.removeOnDidFinishLoadingStyleListener(listener); + } + + /** + * Set a callback that's invoked when a map source has changed. + * + * @param listener The callback that's invoked when the source has changed + */ + public void addOnSourceChangedListener(@NonNull OnSourceChangedListener listener) { + mapChangeReceiver.addOnSourceChangedListener(listener); + } + + /** + * Set a callback that's invoked when a map source has changed. + * + * @param listener The callback that's invoked when the source has changed + */ + public void removeOnSourceChangedListener(@NonNull OnSourceChangedListener listener) { + mapChangeReceiver.removeOnSourceChangedListener(listener); + } + + /** + * Set a callback that's invoked when the id of an icon is missing. + * + * @param listener The callback that's invoked when the id of an icon is missing + */ + public void addOnStyleImageMissingListener(@NonNull OnStyleImageMissingListener listener) { + mapChangeReceiver.addOnStyleImageMissingListener(listener); + } + + /** + * Set a callback that's invoked when a map source has changed. + * + * @param listener The callback that's invoked when the source has changed + */ + public void removeOnStyleImageMissingListener(@NonNull OnStyleImageMissingListener listener) { + mapChangeReceiver.removeOnStyleImageMissingListener(listener); + } + + /** + * Set a callback that's invoked when map needs to release unused image resources. + *

+ * A callback will be called only for unused images that were provided by the client via + * {@link OnStyleImageMissingListener#onStyleImageMissing(String)} listener interface. + *

+ *

+ * By default, platform will remove unused images from the style. By adding listener, default + * behavior can be overridden and client can control whether to release unused resources. + *

+ * + * @param listener The callback that's invoked when map needs to release unused image resources + */ + public void addOnCanRemoveUnusedStyleImageListener(@NonNull OnCanRemoveUnusedStyleImageListener listener) { + mapChangeReceiver.addOnCanRemoveUnusedStyleImageListener(listener); + } + + /** + * Removes a callback that's invoked when map needs to release unused image resources. + *

+ * When all listeners are removed, platform will fallback to default behavior, which is to remove + * unused images from the style. + *

+ * + * @param listener The callback that's invoked when map needs to release unused image resources + */ + public void removeOnCanRemoveUnusedStyleImageListener(@NonNull OnCanRemoveUnusedStyleImageListener listener) { + mapChangeReceiver.removeOnCanRemoveUnusedStyleImageListener(listener); + } + + /** + * Interface definition for a callback to be invoked when the camera will change. + *

+ * {@link MapView#addOnCameraWillChangeListener(OnCameraWillChangeListener)} + *

+ */ + public interface OnCameraWillChangeListener { + + /** + * Called when the camera region will change. + */ + void onCameraWillChange(boolean animated); + } + + /** + * Interface definition for a callback to be invoked when the camera is changing. + *

+ * {@link MapView#addOnCameraIsChangingListener(OnCameraIsChangingListener)} + *

+ */ + public interface OnCameraIsChangingListener { + /** + * Called when the camera is changing. + */ + void onCameraIsChanging(); + } + + /** + * Interface definition for a callback to be invoked when the map region did change. + *

+ * {@link MapView#addOnCameraDidChangeListener(OnCameraDidChangeListener)} + *

+ */ + public interface OnCameraDidChangeListener { + /** + * Called when the camera did change. + */ + void onCameraDidChange(boolean animated); + } + + /** + * Interface definition for a callback to be invoked when the map will start loading. + *

+ * {@link MapView#addOnWillStartLoadingMapListener(OnWillStartLoadingMapListener)} + *

+ */ + public interface OnWillStartLoadingMapListener { + /** + * Called when the map will start loading. + */ + void onWillStartLoadingMap(); + } + + /** + * Interface definition for a callback to be invoked when the map finished loading. + *

+ * {@link MapView#addOnDidFinishLoadingMapListener(OnDidFinishLoadingMapListener)} + *

+ */ + public interface OnDidFinishLoadingMapListener { + /** + * Called when the map has finished loading. + */ + void onDidFinishLoadingMap(); + } + + /** + * Interface definition for a callback to be invoked when the map is changing. + *

+ * {@link MapView#addOnDidFailLoadingMapListener(OnDidFailLoadingMapListener)} + *

+ */ + public interface OnDidFailLoadingMapListener { + /** + * Called when the map failed to load. + * + * @param errorMessage The reason why the map failed to load + */ + void onDidFailLoadingMap(String errorMessage); + } + + /** + * Interface definition for a callback to be invoked when the map will start rendering a frame. + *

+ * {@link MapView#addOnWillStartRenderingFrameListener(OnWillStartRenderingFrameListener)} + *

+ */ + public interface OnWillStartRenderingFrameListener { + /** + * Called when the map will start rendering a frame. + */ + void onWillStartRenderingFrame(); + } + + /** + * Interface definition for a callback to be invoked when the map finished rendering a frame. + *

+ * {@link MapView#addOnDidFinishRenderingFrameListener(OnDidFinishRenderingFrameListener)} + *

+ */ + public interface OnDidFinishRenderingFrameListener { + /** + * Called when the map has finished rendering a frame + * + * @param fully true if all frames have been rendered, false if partially rendered + */ + void onDidFinishRenderingFrame(boolean fully); + } + + /** + * Interface definition for a callback to be invoked when the map will start rendering the map. + *

+ * {@link MapView#addOnDidFailLoadingMapListener(OnDidFailLoadingMapListener)} + *

+ */ + public interface OnWillStartRenderingMapListener { + /** + * Called when the map will start rendering. + */ + void onWillStartRenderingMap(); + } + + /** + * Interface definition for a callback to be invoked when the map is changing. + *

+ * {@link MapView#addOnDidFinishRenderingMapListener(OnDidFinishRenderingMapListener)} + *

+ */ + public interface OnDidFinishRenderingMapListener { + /** + * Called when the map has finished rendering. + * + * @param fully true if map is fully rendered, false if not fully rendered + */ + void onDidFinishRenderingMap(boolean fully); + } + + /** + * Interface definition for a callback to be invoked when the map has entered the idle state. + *

+ * Calling {@link MapboxMap#snapshot(MapboxMap.SnapshotReadyCallback)} from this callback + * will result in recursive execution. Use {@link OnDidFinishRenderingFrameListener} instead. + *

+ *

+ * {@link MapView#addOnDidBecomeIdleListener(OnDidBecomeIdleListener)} + *

+ */ + public interface OnDidBecomeIdleListener { + /** + * Called when the map has entered the idle state. + */ + void onDidBecomeIdle(); + } + + /** + * Interface definition for a callback to be invoked when the map has loaded the style. + *

+ * {@link MapView#addOnDidFailLoadingMapListener(OnDidFailLoadingMapListener)} + *

+ */ + public interface OnDidFinishLoadingStyleListener { + /** + * Called when a style has finished loading. + */ + void onDidFinishLoadingStyle(); + } + + /** + * Interface definition for a callback to be invoked when a map source has changed. + *

+ * {@link MapView#addOnDidFailLoadingMapListener(OnDidFailLoadingMapListener)} + *

+ */ + public interface OnSourceChangedListener { + /** + * Called when a map source has changed. + * + * @param id the id of the source that has changed + */ + void onSourceChangedListener(String id); + } + + /** + * Interface definition for a callback to be invoked with the id of a missing icon. The icon should be added + * synchronously with {@link Style#addImage(String, Bitmap)} to be rendered on the current zoom level. When loading + * icons asynchronously, you can load a placeholder image and replace it when you icon has loaded. + *

+ * {@link MapView#addOnStyleImageMissingListener(OnStyleImageMissingListener)} + *

+ */ + public interface OnStyleImageMissingListener { + /** + * Called when the map is missing an icon.The icon should be added synchronously with + * {@link Style#addImage(String, Bitmap)} to be rendered on the current zoom level. When loading icons + * asynchronously, you can load a placeholder image and replace it when you icon has loaded. + * + * @param id the id of the icon that is missing + */ + void onStyleImageMissing(@NonNull String id); + } + + /** + * Interface definition for a callback to be invoked with an unused image identifier. + *

+ * {@link MapView#addOnCanRemoveUnusedStyleImageListener(OnCanRemoveUnusedStyleImageListener)} + *

+ */ + public interface OnCanRemoveUnusedStyleImageListener { + /** + * Called when the map needs to release unused image resources. + * + * @param id of an image that is not used by the map and can be removed from the style. + * @return true if image can be removed, false otherwise. + */ + boolean onCanRemoveUnusedStyleImage(@NonNull String id); + } + + /** + * Sets a callback object which will be triggered when the {@link MapboxMap} instance is ready to be used. + * + * @param callback The callback object that will be triggered when the map is ready to be used. + */ + @UiThread + public void getMapAsync(final @NonNull OnMapReadyCallback callback) { + if (mapboxMap == null) { + // Add callback to the list only if the style hasn't loaded, or the drawing surface isn't ready + mapCallback.addOnMapReadyCallback(callback); + } else { + callback.onMapReady(mapboxMap); + } + } + + private boolean isGestureDetectorInitialized() { + return mapGestureDetector != null; + } + + @Nullable + MapboxMap getMapboxMap() { + return mapboxMap; + } + + void setMapboxMap(MapboxMap mapboxMap) { + this.mapboxMap = mapboxMap; + } + + private class FocalPointInvalidator implements FocalPointChangeListener { + + private final List focalPointChangeListeners = new ArrayList<>(); + + void addListener(FocalPointChangeListener focalPointChangeListener) { + focalPointChangeListeners.add(focalPointChangeListener); + } + + @Override + public void onFocalPointChanged(PointF pointF) { + mapGestureDetector.setFocalPoint(pointF); + for (FocalPointChangeListener focalPointChangeListener : focalPointChangeListeners) { + focalPointChangeListener.onFocalPointChanged(pointF); + } + } + } + + /** + * The initial render callback waits for rendering to happen before making the map visible for end-users. + * We wait for the second DID_FINISH_RENDERING_FRAME map change event as the first will still show a black surface. + */ + private class InitialRenderCallback implements OnDidFinishRenderingFrameListener { + + private int renderCount; + + InitialRenderCallback() { + addOnDidFinishRenderingFrameListener(this); + } + + @Override + public void onDidFinishRenderingFrame(boolean fully) { + if (mapboxMap != null && mapboxMap.getStyle() != null && mapboxMap.getStyle().isFullyLoaded()) { + renderCount++; + if (renderCount == 3) { + MapView.this.setForeground(null); + removeOnDidFinishRenderingFrameListener(this); + } + } + } + + private void onDestroy() { + removeOnDidFinishRenderingFrameListener(this); + } + } + + private class GesturesManagerInteractionListener implements MapboxMap.OnGesturesManagerInteractionListener { + + @Override + public void onAddMapClickListener(MapboxMap.OnMapClickListener listener) { + mapGestureDetector.addOnMapClickListener(listener); + } + + @Override + public void onRemoveMapClickListener(MapboxMap.OnMapClickListener listener) { + mapGestureDetector.removeOnMapClickListener(listener); + } + + @Override + public void onAddMapLongClickListener(MapboxMap.OnMapLongClickListener listener) { + mapGestureDetector.addOnMapLongClickListener(listener); + } + + @Override + public void onRemoveMapLongClickListener(MapboxMap.OnMapLongClickListener listener) { + mapGestureDetector.removeOnMapLongClickListener(listener); + } + + @Override + public void onAddFlingListener(MapboxMap.OnFlingListener listener) { + mapGestureDetector.addOnFlingListener(listener); + } + + @Override + public void onRemoveFlingListener(MapboxMap.OnFlingListener listener) { + mapGestureDetector.removeOnFlingListener(listener); + } + + @Override + public void onAddMoveListener(MapboxMap.OnMoveListener listener) { + mapGestureDetector.addOnMoveListener(listener); + } + + @Override + public void onRemoveMoveListener(MapboxMap.OnMoveListener listener) { + mapGestureDetector.removeOnMoveListener(listener); + } + + @Override + public void onAddRotateListener(MapboxMap.OnRotateListener listener) { + mapGestureDetector.addOnRotateListener(listener); + } + + @Override + public void onRemoveRotateListener(MapboxMap.OnRotateListener listener) { + mapGestureDetector.removeOnRotateListener(listener); + } + + @Override + public void onAddScaleListener(MapboxMap.OnScaleListener listener) { + mapGestureDetector.addOnScaleListener(listener); + } + + @Override + public void onRemoveScaleListener(MapboxMap.OnScaleListener listener) { + mapGestureDetector.removeOnScaleListener(listener); + } + + @Override + public void onAddShoveListener(MapboxMap.OnShoveListener listener) { + mapGestureDetector.addShoveListener(listener); + } + + @Override + public void onRemoveShoveListener(MapboxMap.OnShoveListener listener) { + mapGestureDetector.removeShoveListener(listener); + } + + @Override + public AndroidGesturesManager getGesturesManager() { + return mapGestureDetector.getGesturesManager(); + } + + @Override + public void setGesturesManager(AndroidGesturesManager gesturesManager, boolean attachDefaultListeners, + boolean setDefaultMutuallyExclusives) { + mapGestureDetector.setGesturesManager( + getContext(), gesturesManager, attachDefaultListeners, setDefaultMutuallyExclusives); + } + + @Override + public void cancelAllVelocityAnimations() { + mapGestureDetector.cancelAnimators(); + } + } + + private class MapCallback implements OnDidFinishLoadingStyleListener, + OnDidFinishRenderingFrameListener, OnDidFinishLoadingMapListener, + OnCameraIsChangingListener, OnCameraDidChangeListener, OnDidFailLoadingMapListener { + + private final List onMapReadyCallbackList = new ArrayList<>(); + + MapCallback() { + addOnDidFinishLoadingStyleListener(this); + addOnDidFinishRenderingFrameListener(this); + addOnDidFinishLoadingMapListener(this); + addOnCameraIsChangingListener(this); + addOnCameraDidChangeListener(this); + addOnDidFailLoadingMapListener(this); + } + + void initialised() { + mapboxMap.onPreMapReady(); + onMapReady(); + mapboxMap.onPostMapReady(); + } + + /** + * Notify listeners, clear when done + */ + private void onMapReady() { + if (onMapReadyCallbackList.size() > 0) { + Iterator iterator = onMapReadyCallbackList.iterator(); + while (iterator.hasNext()) { + OnMapReadyCallback callback = iterator.next(); + if (callback != null) { + // null checking required for #13279 + callback.onMapReady(mapboxMap); + } + iterator.remove(); + } + } + } + + void addOnMapReadyCallback(OnMapReadyCallback callback) { + onMapReadyCallbackList.add(callback); + } + + void onDestroy() { + onMapReadyCallbackList.clear(); + removeOnDidFinishLoadingStyleListener(this); + removeOnDidFinishRenderingFrameListener(this); + removeOnDidFinishLoadingMapListener(this); + removeOnCameraIsChangingListener(this); + removeOnCameraDidChangeListener(this); + removeOnDidFailLoadingMapListener(this); + } + + @Override + public void onDidFinishLoadingStyle() { + if (mapboxMap != null) { + mapboxMap.onFinishLoadingStyle(); + } + } + + @Override + public void onDidFailLoadingMap(String errorMessage) { + if (mapboxMap != null) { + mapboxMap.onFailLoadingStyle(); + } + } + + @Override + public void onDidFinishRenderingFrame(boolean fully) { + if (mapboxMap != null) { + mapboxMap.onUpdateFullyRendered(); + } + } + + @Override + public void onDidFinishLoadingMap() { + if (mapboxMap != null) { + mapboxMap.onUpdateRegionChange(); + } + } + + @Override + public void onCameraIsChanging() { + if (mapboxMap != null) { + mapboxMap.onUpdateRegionChange(); + } + } + + @Override + public void onCameraDidChange(boolean animated) { + if (mapboxMap != null) { + mapboxMap.onUpdateRegionChange(); + } + } + } + + /** + * Click event hook for providing a custom attribution dialog manager. + */ + private static class AttributionClickListener implements OnClickListener { + + @NonNull + private final AttributionDialogManager defaultDialogManager; + private UiSettings uiSettings; + + private AttributionClickListener(@NonNull Context context, @NonNull MapboxMap mapboxMap) { + this.defaultDialogManager = new AttributionDialogManager(context, mapboxMap); + this.uiSettings = mapboxMap.getUiSettings(); + } + + @Override + public void onClick(View v) { + getDialogManager().onClick(v); + } + + public void onStop() { + getDialogManager().onStop(); + } + + private AttributionDialogManager getDialogManager() { + AttributionDialogManager customDialogManager = uiSettings.getAttributionDialogManager(); + if (customDialogManager != null) { + return uiSettings.getAttributionDialogManager(); + } else { + return defaultDialogManager; + } + } + } + + /** + * Sets the strict mode that will throw the {@link com.mapbox.mapboxsdk.MapStrictModeException} + * whenever the map would fail silently otherwise. + * + * @param strictModeEnabled true to enable the strict mode, false otherwise + */ + public static void setMapStrictModeEnabled(boolean strictModeEnabled) { + MapStrictMode.setStrictModeEnabled(strictModeEnabled); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java new file mode 100644 index 000000000..acd5093da --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java @@ -0,0 +1,2391 @@ +package com.mapbox.mapboxsdk.maps; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.PointF; +import android.graphics.RectF; +import android.os.Bundle; +import android.support.annotation.FloatRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.Size; +import android.support.annotation.UiThread; +import android.text.TextUtils; +import android.view.View; + +import com.mapbox.android.gestures.AndroidGesturesManager; +import com.mapbox.android.gestures.MoveGestureDetector; +import com.mapbox.android.gestures.RotateGestureDetector; +import com.mapbox.android.gestures.ShoveGestureDetector; +import com.mapbox.android.gestures.StandardScaleGestureDetector; +import com.mapbox.geojson.Feature; +import com.mapbox.geojson.Geometry; +import com.mapbox.mapboxsdk.MapStrictMode; +import com.mapbox.mapboxsdk.annotations.Annotation; +import com.mapbox.mapboxsdk.annotations.BaseMarkerOptions; +import com.mapbox.mapboxsdk.annotations.Marker; +import com.mapbox.mapboxsdk.annotations.MarkerOptions; +import com.mapbox.mapboxsdk.annotations.Polygon; +import com.mapbox.mapboxsdk.annotations.PolygonOptions; +import com.mapbox.mapboxsdk.annotations.Polyline; +import com.mapbox.mapboxsdk.annotations.PolylineOptions; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.camera.CameraUpdate; +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; +import com.mapbox.mapboxsdk.constants.MapboxConstants; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.mapboxsdk.location.LocationComponent; +import com.mapbox.mapboxsdk.log.Logger; +import com.mapbox.mapboxsdk.offline.OfflineRegionDefinition; +import com.mapbox.mapboxsdk.style.expressions.Expression; + +import java.util.ArrayList; +import java.util.List; + +/** + * The general class to interact with in the Android Mapbox SDK. It exposes the entry point for all + * methods related to the MapView. You cannot instantiate {@link MapboxMap} object directly, rather, + * you must obtain one from the getMapAsync() method on a MapFragment or MapView that you have + * added to your application. + *

+ * Note: Similar to a View object, a MapboxMap should only be read and modified from the main thread. + *

+ */ +@UiThread +public final class MapboxMap { + + private static final String TAG = "Mbgl-MapboxMap"; + + private final NativeMap nativeMapView; + private final UiSettings uiSettings; + private final Projection projection; + private final Transform transform; + private final CameraChangeDispatcher cameraChangeDispatcher; + private final OnGesturesManagerInteractionListener onGesturesManagerInteractionListener; + private final List awaitingStyleGetters = new ArrayList<>(); + private final List developerAnimationStartedListeners; + + @Nullable + private Style.OnStyleLoaded styleLoadedCallback; + + private LocationComponent locationComponent; + private AnnotationManager annotationManager; + + @Nullable + private MapboxMap.OnFpsChangedListener onFpsChangedListener; + + @Nullable + private Style style; + + private boolean debugActive; + + MapboxMap(NativeMap map, Transform transform, UiSettings ui, Projection projection, + OnGesturesManagerInteractionListener listener, CameraChangeDispatcher cameraChangeDispatcher, + List developerAnimationStartedListeners) { + this.nativeMapView = map; + this.uiSettings = ui; + this.projection = projection; + this.transform = transform; + this.onGesturesManagerInteractionListener = listener; + this.cameraChangeDispatcher = cameraChangeDispatcher; + this.developerAnimationStartedListeners = developerAnimationStartedListeners; + } + + void initialise(@NonNull Context context, @NonNull MapboxMapOptions options) { + transform.initialise(this, options); + uiSettings.initialise(context, options); + + // Map configuration + setDebugActive(options.getDebugActive()); + setApiBaseUrl(options); + setPrefetchesTiles(options); + } + + /** + * Get the Style of the map asynchronously. + */ + public void getStyle(@NonNull Style.OnStyleLoaded onStyleLoaded) { + if (style != null && style.isFullyLoaded()) { + onStyleLoaded.onStyleLoaded(style); + } else { + awaitingStyleGetters.add(onStyleLoaded); + } + } + + /** + * Get the Style of the map. + *

+ * Returns null when style is being loaded. + *

+ * + * @return the style of the map + */ + @Nullable + public Style getStyle() { + if (style == null || !style.isFullyLoaded()) { + return null; + } else { + return style; + } + } + + /** + * Called when the hosting Activity/Fragment onStart() method is called. + */ + void onStart() { + locationComponent.onStart(); + } + + /** + * Called when the hosting Activity/Fragment onStop() method is called. + */ + void onStop() { + locationComponent.onStop(); + } + + /** + * Called when the hosting Activity/Fragment is going to be destroyed and map state needs to be saved. + * + * @param outState the bundle to save the state to. + */ + void onSaveInstanceState(@NonNull Bundle outState) { + outState.putParcelable(MapboxConstants.STATE_CAMERA_POSITION, transform.getCameraPosition()); + outState.putBoolean(MapboxConstants.STATE_DEBUG_ACTIVE, isDebugActive()); + uiSettings.onSaveInstanceState(outState); + } + + /** + * Called when the hosting Activity/Fragment is recreated and map state needs to be restored. + * + * @param savedInstanceState the bundle containing the saved state + */ + void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + final CameraPosition cameraPosition = savedInstanceState.getParcelable(MapboxConstants.STATE_CAMERA_POSITION); + + uiSettings.onRestoreInstanceState(savedInstanceState); + + if (cameraPosition != null) { + moveCamera(CameraUpdateFactory.newCameraPosition( + new CameraPosition.Builder(cameraPosition).build()) + ); + } + + nativeMapView.setDebug(savedInstanceState.getBoolean(MapboxConstants.STATE_DEBUG_ACTIVE)); + } + + /** + * Called when the hosting Activity/Fragment onDestroy()/onDestroyView() method is called. + */ + void onDestroy() { + locationComponent.onDestroy(); + if (style != null) { + style.clear(); + } + cameraChangeDispatcher.onDestroy(); + } + + /** + * Called before the OnMapReadyCallback is invoked. + */ + void onPreMapReady() { + transform.invalidateCameraPosition(); + annotationManager.reloadMarkers(); + annotationManager.adjustTopOffsetPixels(this); + } + + /** + * Called when the OnMapReadyCallback has finished executing. + *

+ * Invalidation of the camera position is required to update the added components in + * OnMapReadyCallback with the correct transformation. + *

+ */ + void onPostMapReady() { + transform.invalidateCameraPosition(); + } + + /** + * Called when the map finished loading a style. + */ + void onFinishLoadingStyle() { + notifyStyleLoaded(); + } + + /** + * Called when the map failed loading a style. + */ + void onFailLoadingStyle() { + styleLoadedCallback = null; + } + + /** + * Called when the region is changing or has changed. + */ + void onUpdateRegionChange() { + annotationManager.update(); + } + + /** + * Called when the map frame is fully rendered. + */ + void onUpdateFullyRendered() { + CameraPosition cameraPosition = transform.invalidateCameraPosition(); + if (cameraPosition != null) { + uiSettings.update(cameraPosition); + } + } + + /** + * Experimental feature. Do not use. + */ + long getNativeMapPtr() { + return nativeMapView.getNativePtr(); + } + + // Style + + /** + * Sets tile pre-fetching from MapboxOptions. + * + * @param options the options object + */ + private void setPrefetchesTiles(@NonNull MapboxMapOptions options) { + setPrefetchesTiles(options.getPrefetchesTiles()); + } + + /** + * Enable or disable tile pre-fetching. Pre-fetching makes sure that a low-resolution + * tile is rendered as soon as possible at the expense of a little bandwidth. + * + * @param enable true to enable + */ + public void setPrefetchesTiles(boolean enable) { + nativeMapView.setPrefetchTiles(enable); + } + + /** + * Check whether tile pre-fetching is enabled or not. + * + * @return true if enabled + * @see MapboxMap#setPrefetchesTiles(boolean) + */ + public boolean getPrefetchesTiles() { + return nativeMapView.getPrefetchTiles(); + } + + // + // MinZoom + // + + /** + *

+ * Sets the minimum zoom level the map can be displayed at. + *

+ * + * @param minZoom The new minimum zoom level. + */ + public void setMinZoomPreference( + @FloatRange(from = MapboxConstants.MINIMUM_ZOOM, to = MapboxConstants.MAXIMUM_ZOOM) double minZoom) { + transform.setMinZoom(minZoom); + } + + /** + *

+ * Gets the minimum zoom level the map can be displayed at. + *

+ * + * @return The minimum zoom level. + */ + public double getMinZoomLevel() { + return transform.getMinZoom(); + } + + // + // MaxZoom + // + + /** + *

+ * Sets the maximum zoom level the map can be displayed at. + *

+ *

+ * The default maximum zoomn level is 22. The upper bound for this value is 25.5. + *

+ * + * @param maxZoom The new maximum zoom level. + */ + public void setMaxZoomPreference(@FloatRange(from = MapboxConstants.MINIMUM_ZOOM, + to = MapboxConstants.MAXIMUM_ZOOM) double maxZoom) { + transform.setMaxZoom(maxZoom); + } + + /** + *

+ * Gets the maximum zoom level the map can be displayed at. + *

+ * + * @return The maximum zoom level. + */ + public double getMaxZoomLevel() { + return transform.getMaxZoom(); + } + + // + // UiSettings + // + + /** + * Gets the user interface settings for the map. + * + * @return the UiSettings associated with this map + */ + @NonNull + public UiSettings getUiSettings() { + return uiSettings; + } + + // + // Projection + // + + /** + * Get the Projection object that you can use to convert between screen coordinates and latitude/longitude + * coordinates. + * + * @return the Projection associated with this map + */ + @NonNull + public Projection getProjection() { + return projection; + } + + // + // Camera API + // + + /** + * Cancels ongoing animations. + *

+ * This invokes the {@link CancelableCallback} for ongoing camera updates. + *

+ */ + public void cancelTransitions() { + transform.cancelTransitions(); + } + + /** + * Gets the current position of the camera. + * The CameraPosition returned is a snapshot of the current position, and will not automatically update when the + * camera moves. + * + * @return The current position of the Camera. + */ + @NonNull + public final CameraPosition getCameraPosition() { + return transform.getCameraPosition(); + } + + /** + * Repositions the camera according to the cameraPosition. + * The move is instantaneous, and a subsequent getCameraPosition() will reflect the new position. + * See CameraUpdateFactory for a set of updates. + * + * @param cameraPosition the camera position to set + */ + public void setCameraPosition(@NonNull CameraPosition cameraPosition) { + moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition), null); + } + + /** + * Repositions the camera according to the instructions defined in the update. + * The move is instantaneous, and a subsequent getCameraPosition() will reflect the new position. + * See CameraUpdateFactory for a set of updates. + * + * @param update The change that should be applied to the camera. + */ + public final void moveCamera(@NonNull CameraUpdate update) { + moveCamera(update, null); + } + + /** + * Repositions the camera according to the instructions defined in the update. + * The move is instantaneous, and a subsequent getCameraPosition() will reflect the new position. + * See CameraUpdateFactory for a set of updates. + * + * @param update The change that should be applied to the camera + * @param callback the callback to be invoked when an animation finishes or is canceled + */ + public final void moveCamera(@NonNull final CameraUpdate update, + @Nullable final MapboxMap.CancelableCallback callback) { + notifyDeveloperAnimationListeners(); + transform.moveCamera(MapboxMap.this, update, callback); + } + + /** + * Gradually move the camera by the default duration, zoom will not be affected unless specified + * within {@link CameraUpdate}. If {@link #getCameraPosition()} is called during the animation, + * it will return the current location of the camera in flight. + * + * @param update The change that should be applied to the camera. + * @see com.mapbox.mapboxsdk.camera.CameraUpdateFactory for a set of updates. + */ + public final void easeCamera(@NonNull CameraUpdate update) { + easeCamera(update, MapboxConstants.ANIMATION_DURATION); + } + + /** + * Gradually move the camera by the default duration, zoom will not be affected unless specified + * within {@link CameraUpdate}. If {@link #getCameraPosition()} is called during the animation, + * it will return the current location of the camera in flight. + * + * @param update The change that should be applied to the camera. + * @param callback An optional callback to be notified from the main thread when the animation + * stops. If the animation stops due to its natural completion, the callback + * will be notified with onFinish(). If the animation stops due to interruption + * by a later camera movement or a user gesture, onCancel() will be called. + * Do not update or ease the camera from within onCancel(). + * @see com.mapbox.mapboxsdk.camera.CameraUpdateFactory for a set of updates. + */ + public final void easeCamera(@NonNull CameraUpdate update, @Nullable final MapboxMap.CancelableCallback callback) { + easeCamera(update, MapboxConstants.ANIMATION_DURATION, callback); + } + + /** + * Gradually move the camera by a specified duration in milliseconds, zoom will not be affected + * unless specified within {@link CameraUpdate}. If {@link #getCameraPosition()} is called + * during the animation, it will return the current location of the camera in flight. + * + * @param update The change that should be applied to the camera. + * @param durationMs The duration of the animation in milliseconds. This must be strictly + * positive, otherwise an IllegalArgumentException will be thrown. + * @see com.mapbox.mapboxsdk.camera.CameraUpdateFactory for a set of updates. + */ + public final void easeCamera(@NonNull CameraUpdate update, int durationMs) { + easeCamera(update, durationMs, null); + } + + /** + * Gradually move the camera by a specified duration in milliseconds, zoom will not be affected + * unless specified within {@link CameraUpdate}. A callback can be used to be notified when + * easing the camera stops. If {@link #getCameraPosition()} is called during the animation, it + * will return the current location of the camera in flight. + *

+ * Note that this will cancel location tracking mode if enabled. + *

+ * + * @param update The change that should be applied to the camera. + * @param durationMs The duration of the animation in milliseconds. This must be strictly + * positive, otherwise an IllegalArgumentException will be thrown. + * @param callback An optional callback to be notified from the main thread when the animation + * stops. If the animation stops due to its natural completion, the callback + * will be notified with onFinish(). If the animation stops due to interruption + * by a later camera movement or a user gesture, onCancel() will be called. + * Do not update or ease the camera from within onCancel(). + * @see com.mapbox.mapboxsdk.camera.CameraUpdateFactory for a set of updates. + */ + public final void easeCamera(@NonNull CameraUpdate update, int durationMs, + @Nullable final MapboxMap.CancelableCallback callback) { + easeCamera(update, durationMs, true, callback); + } + + /** + * Gradually move the camera by a specified duration in milliseconds, zoom will not be affected + * unless specified within {@link CameraUpdate}. A callback can be used to be notified when + * easing the camera stops. If {@link #getCameraPosition()} is called during the animation, it + * will return the current location of the camera in flight. + *

+ * Note that this will cancel location tracking mode if enabled. + *

+ * + * @param update The change that should be applied to the camera. + * @param durationMs The duration of the animation in milliseconds. This must be strictly + * positive, otherwise an IllegalArgumentException will be thrown. + * @param easingInterpolator True for easing interpolator, false for linear. + */ + public final void easeCamera(@NonNull CameraUpdate update, int durationMs, boolean easingInterpolator) { + easeCamera(update, durationMs, easingInterpolator, null); + } + + /** + * Gradually move the camera by a specified duration in milliseconds, zoom will not be affected + * unless specified within {@link CameraUpdate}. A callback can be used to be notified when + * easing the camera stops. If {@link #getCameraPosition()} is called during the animation, it + * will return the current location of the camera in flight. + * + * @param update The change that should be applied to the camera. + * @param durationMs The duration of the animation in milliseconds. This must be strictly + * positive, otherwise an IllegalArgumentException will be thrown. + * @param easingInterpolator True for easing interpolator, false for linear. + * @param callback An optional callback to be notified from the main thread when the animation + * stops. If the animation stops due to its natural completion, the callback + * will be notified with onFinish(). If the animation stops due to interruption + * by a later camera movement or a user gesture, onCancel() will be called. + * Do not update or ease the camera from within onCancel(). + */ + public final void easeCamera(@NonNull final CameraUpdate update, + final int durationMs, + final boolean easingInterpolator, + @Nullable final MapboxMap.CancelableCallback callback) { + if (durationMs <= 0) { + throw new IllegalArgumentException("Null duration passed into easeCamera"); + } + notifyDeveloperAnimationListeners(); + transform.easeCamera(MapboxMap.this, update, durationMs, easingInterpolator, callback); + } + + /** + * Animate the camera to a new location defined within {@link CameraUpdate} using a transition + * animation that evokes powered flight. The animation will last the default amount of time. + * During the animation, a call to {@link #getCameraPosition()} returns an intermediate location + * of the camera in flight. + * + * @param update The change that should be applied to the camera. + * @see com.mapbox.mapboxsdk.camera.CameraUpdateFactory for a set of updates. + */ + public final void animateCamera(@NonNull CameraUpdate update) { + animateCamera(update, MapboxConstants.ANIMATION_DURATION, null); + } + + /** + * Animate the camera to a new location defined within {@link CameraUpdate} using a transition + * animation that evokes powered flight. The animation will last the default amount of time. A + * callback can be used to be notified when animating the camera stops. During the animation, a + * call to {@link #getCameraPosition()} returns an intermediate location of the camera in flight. + * + * @param update The change that should be applied to the camera. + * @param callback The callback to invoke from the main thread when the animation stops. If the + * animation completes normally, onFinish() is called; otherwise, onCancel() is + * called. Do not update or animate the camera from within onCancel(). + * @see com.mapbox.mapboxsdk.camera.CameraUpdateFactory for a set of updates. + */ + public final void animateCamera(@NonNull CameraUpdate update, @Nullable MapboxMap.CancelableCallback callback) { + animateCamera(update, MapboxConstants.ANIMATION_DURATION, callback); + } + + /** + * Animate the camera to a new location defined within {@link CameraUpdate} using a transition + * animation that evokes powered flight. The animation will last a specified amount of time + * given in milliseconds. During the animation, a call to {@link #getCameraPosition()} returns + * an intermediate location of the camera in flight. + * + * @param update The change that should be applied to the camera. + * @param durationMs The duration of the animation in milliseconds. This must be strictly + * positive, otherwise an IllegalArgumentException will be thrown. + * @see com.mapbox.mapboxsdk.camera.CameraUpdateFactory for a set of updates. + */ + public final void animateCamera(@NonNull CameraUpdate update, int durationMs) { + animateCamera(update, durationMs, null); + } + + /** + * Animate the camera to a new location defined within {@link CameraUpdate} using a transition + * animation that evokes powered flight. The animation will last a specified amount of time + * given in milliseconds. A callback can be used to be notified when animating the camera stops. + * During the animation, a call to {@link #getCameraPosition()} returns an intermediate location + * of the camera in flight. + * + * @param update The change that should be applied to the camera. + * @param durationMs The duration of the animation in milliseconds. This must be strictly + * positive, otherwise an IllegalArgumentException will be thrown. + * @param callback An optional callback to be notified from the main thread when the animation + * stops. If the animation stops due to its natural completion, the callback + * will be notified with onFinish(). If the animation stops due to interruption + * by a later camera movement or a user gesture, onCancel() will be called. + * Do not update or animate the camera from within onCancel(). If a callback + * isn't required, leave it as null. + * @see com.mapbox.mapboxsdk.camera.CameraUpdateFactory for a set of updates. + */ + public final void animateCamera(@NonNull final CameraUpdate update, final int durationMs, + @Nullable final MapboxMap.CancelableCallback callback) { + if (durationMs <= 0) { + throw new IllegalArgumentException("Null duration passed into animateCamera"); + } + notifyDeveloperAnimationListeners(); + transform.animateCamera(MapboxMap.this, update, durationMs, callback); + } + + /** + * Scrolls the camera over the map, shifting the center of view by the specified number of pixels in the x and y + * directions. + * + * @param x Amount of pixels to scroll to in x direction + * @param y Amount of pixels to scroll to in y direction + */ + public void scrollBy(float x, float y) { + scrollBy(x, y, 0); + } + + /** + * Scrolls the camera over the map, shifting the center of view by the specified number of pixels in the x and y + * directions. + * + * @param x Amount of pixels to scroll to in x direction + * @param y Amount of pixels to scroll to in y direction + * @param duration Amount of time the scrolling should take + */ + public void scrollBy(float x, float y, long duration) { + notifyDeveloperAnimationListeners(); + nativeMapView.moveBy(x, y, duration); + } + + // + // Reset North + // + + /** + * Resets the map view to face north. + */ + public void resetNorth() { + notifyDeveloperAnimationListeners(); + transform.resetNorth(); + } + + /** + * Transform the map bearing given a bearing, focal point coordinates, and a duration. + * + * @param bearing The bearing of the Map to be transformed to + * @param focalX The x coordinate of the focal point + * @param focalY The y coordinate of the focal point + * @param duration The duration of the transformation + */ + public void setFocalBearing(double bearing, float focalX, float focalY, long duration) { + notifyDeveloperAnimationListeners(); + transform.setBearing(bearing, focalX, focalY, duration); + } + + /** + * Returns the measured height of the Map. + * + * @return the height of the map + */ + public float getHeight() { + return projection.getHeight(); + } + + /** + * Returns the measured width of the Map. + * + * @return the width of the map + */ + public float getWidth() { + return projection.getWidth(); + } + + // + // Offline + // + + /** + * Loads a new style from the specified offline region definition and moves the map camera to that region. + * + * @param definition the offline region definition + * @see OfflineRegionDefinition + */ + public void setOfflineRegionDefinition(@NonNull OfflineRegionDefinition definition) { + setOfflineRegionDefinition(definition, null); + } + + /** + * Loads a new style from the specified offline region definition and moves the map camera to that region. + * + * @param definition the offline region definition + * @param callback the callback to be invoked when the style has loaded + * @see OfflineRegionDefinition + */ + public void setOfflineRegionDefinition(@NonNull OfflineRegionDefinition definition, + @Nullable Style.OnStyleLoaded callback) { + double minZoom = definition.getMinZoom(); + double maxZoom = definition.getMaxZoom(); + CameraPosition cameraPosition = new CameraPosition.Builder() + .target(definition.getBounds().getCenter()) + .zoom(minZoom) + .build(); + moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); + setMinZoomPreference(minZoom); + setMaxZoomPreference(maxZoom); + setStyle(new Style.Builder().fromUri(definition.getStyleURL()), callback); + } + + // + // Debug + // + + /** + * Returns whether the map debug information is currently shown. + * + * @return If true, map debug information is currently shown. + */ + public boolean isDebugActive() { + return debugActive; + } + + /** + *

+ * Changes whether the map debug information is shown. + *

+ * The default value is false. + * + * @param debugActive If true, map debug information is shown. + */ + public void setDebugActive(boolean debugActive) { + this.debugActive = debugActive; + nativeMapView.setDebug(debugActive); + } + + /** + *

+ * Cycles through the map debug options. + *

+ * The value of isDebugActive reflects whether there are + * any map debug options enabled or disabled. + * + * @see #isDebugActive() + */ + public void cycleDebugOptions() { + nativeMapView.cycleDebugOptions(); + this.debugActive = nativeMapView.getDebug(); + } + + // + // API endpoint config + // + + private void setApiBaseUrl(@NonNull MapboxMapOptions options) { + String apiBaseUrl = options.getApiBaseUrl(); + if (!TextUtils.isEmpty(apiBaseUrl)) { + nativeMapView.setApiBaseUrl(apiBaseUrl); + } + } + + // + // Styling + // + + /** + * Loads a new map style from the specified bundled style. + *

+ * This method is asynchronous and will return before the style finishes loading. + * If you wish to wait for the map to finish loading, listen to the {@link MapView.OnDidFinishLoadingStyleListener} + * callback or use the {@link #setStyle(String, Style.OnStyleLoaded)} method instead. + *

+ * If the style fails to load or an invalid style URL is set, the map view will become blank. + * An error message will be logged in the Android logcat and {@link MapView.OnDidFailLoadingMapListener} callback + * will be triggered. + * + * @param style The bundled style + * @see Style + */ + public void setStyle(@Style.StyleUrl String style) { + this.setStyle(style, null); + } + + /** + * Loads a new map style from the specified bundled style. + *

+ * If the style fails to load or an invalid style URL is set, the map view will become blank. + * An error message will be logged in the Android logcat and {@link MapView.OnDidFailLoadingMapListener} callback + * will be triggered. + *

+ * + * @param style The bundled style + * @param callback The callback to be invoked when the style has loaded + * @see Style + */ + public void setStyle(@Style.StyleUrl String style, final Style.OnStyleLoaded callback) { + this.setStyle(new Style.Builder().fromUri(style), callback); + } + + /** + * Loads a new map style from the specified builder. + *

+ * If the builder fails to load, the map view will become blank. An error message will be logged in the Android logcat + * and {@link MapView.OnDidFailLoadingMapListener} callback will be triggered. If you wish to wait for the map to + * finish loading, listen to the {@link MapView.OnDidFinishLoadingStyleListener} callback or use the + * {@link #setStyle(String, Style.OnStyleLoaded)} instead. + *

+ * + * @param builder The style builder + * @see Style + */ + public void setStyle(Style.Builder builder) { + this.setStyle(builder, null); + } + + /** + * Loads a new map style from the specified builder. + *

+ * If the builder fails to load, the map view will become blank. An error message will be logged in the Android logcat + * and {@link MapView.OnDidFailLoadingMapListener} callback will be triggered. + *

+ * + * @param builder The style builder + * @param callback The callback to be invoked when the style has loaded + * @see Style + */ + public void setStyle(Style.Builder builder, final Style.OnStyleLoaded callback) { + styleLoadedCallback = callback; + locationComponent.onStartLoadingMap(); + if (style != null) { + style.clear(); + } + + style = builder.build(nativeMapView); + if (!TextUtils.isEmpty(builder.getUri())) { + nativeMapView.setStyleUri(builder.getUri()); + } else if (!TextUtils.isEmpty(builder.getJson())) { + nativeMapView.setStyleJson(builder.getJson()); + } else { + // user didn't provide a `from` component, load a blank style instead + nativeMapView.setStyleJson(Style.EMPTY_JSON); + } + } + + void notifyStyleLoaded() { + if (nativeMapView.isDestroyed()) { + return; + } + + if (style != null) { + style.onDidFinishLoadingStyle(); + locationComponent.onFinishLoadingStyle(); + + // notify the listener provided with the style setter + if (styleLoadedCallback != null) { + styleLoadedCallback.onStyleLoaded(style); + } + + // notify style getters + for (Style.OnStyleLoaded styleGetter : awaitingStyleGetters) { + styleGetter.onStyleLoaded(style); + } + } else { + MapStrictMode.strictModeViolation("No style to provide."); + } + styleLoadedCallback = null; + awaitingStyleGetters.clear(); + } + + // + // Annotations + // + + /** + *

+ * Adds a marker to this map. + *

+ * The marker's icon is rendered on the map at the location {@code Marker.position}. + * If {@code Marker.title} is defined, the map shows an info box with the marker's title and snippet. + * + * @param markerOptions A marker options object that defines how to render the marker + * @return The {@code Marker} that was added to the map + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @NonNull + public Marker addMarker(@NonNull MarkerOptions markerOptions) { + return annotationManager.addMarker(markerOptions, this); + } + + /** + *

+ * Adds a marker to this map. + *

+ * The marker's icon is rendered on the map at the location {@code Marker.position}. + * If {@code Marker.title} is defined, the map shows an info box with the marker's title and snippet. + * + * @param markerOptions A marker options object that defines how to render the marker + * @return The {@code Marker} that was added to the map + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @NonNull + public Marker addMarker(@NonNull BaseMarkerOptions markerOptions) { + return annotationManager.addMarker(markerOptions, this); + } + + /** + *

+ * Adds multiple markers to this map. + *

+ * The marker's icon is rendered on the map at the location {@code Marker.position}. + * If {@code Marker.title} is defined, the map shows an info box with the marker's title and snippet. + * + * @param markerOptionsList A list of marker options objects that defines how to render the markers + * @return A list of the {@code Marker}s that were added to the map + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @NonNull + public List addMarkers(@NonNull List markerOptionsList) { + return annotationManager.addMarkers(markerOptionsList, this); + } + + /** + *

+ * Updates a marker on this map. Does nothing if the marker isn't already added. + *

+ * + * @param updatedMarker An updated marker object + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void updateMarker(@NonNull Marker updatedMarker) { + annotationManager.updateMarker(updatedMarker, this); + } + + /** + * Adds a polyline to this map. + * + * @param polylineOptions A polyline options object that defines how to render the polyline + * @return The {@code Polyine} that was added to the map + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @NonNull + public Polyline addPolyline(@NonNull PolylineOptions polylineOptions) { + return annotationManager.addPolyline(polylineOptions, this); + } + + /** + * Adds multiple polylines to this map. + * + * @param polylineOptionsList A list of polyline options objects that defines how to render the polylines. + * @return A list of the {@code Polyline}s that were added to the map. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @NonNull + public List addPolylines(@NonNull List polylineOptionsList) { + return annotationManager.addPolylines(polylineOptionsList, this); + } + + /** + * Update a polyline on this map. + * + * @param polyline An updated polyline object. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void updatePolyline(@NonNull Polyline polyline) { + annotationManager.updatePolyline(polyline); + } + + /** + * Adds a polygon to this map. + * + * @param polygonOptions A polygon options object that defines how to render the polygon. + * @return The {@code Polygon} that was added to the map. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @NonNull + public Polygon addPolygon(@NonNull PolygonOptions polygonOptions) { + return annotationManager.addPolygon(polygonOptions, this); + } + + /** + * Adds multiple polygons to this map. + * + * @param polygonOptionsList A list of polygon options objects that defines how to render the polygons + * @return A list of the {@code Polygon}s that were added to the map + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @NonNull + public List addPolygons(@NonNull List polygonOptionsList) { + return annotationManager.addPolygons(polygonOptionsList, this); + } + + /** + * Update a polygon on this map. + * + * @param polygon An updated polygon object + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void updatePolygon(@NonNull Polygon polygon) { + annotationManager.updatePolygon(polygon); + } + + /** + *

+ * Convenience method for removing a Marker from the map. + *

+ * Calls removeAnnotation() internally. + * + * @param marker Marker to remove + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void removeMarker(@NonNull Marker marker) { + annotationManager.removeAnnotation(marker); + } + + /** + *

+ * Convenience method for removing a Polyline from the map. + *

+ * Calls removeAnnotation() internally. + * + * @param polyline Polyline to remove + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void removePolyline(@NonNull Polyline polyline) { + annotationManager.removeAnnotation(polyline); + } + + /** + *

+ * Convenience method for removing a Polygon from the map. + *

+ * Calls removeAnnotation() internally. + * + * @param polygon Polygon to remove + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void removePolygon(@NonNull Polygon polygon) { + annotationManager.removeAnnotation(polygon); + } + + /** + * Removes an annotation from the map. + * + * @param annotation The annotation object to remove. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void removeAnnotation(@NonNull Annotation annotation) { + annotationManager.removeAnnotation(annotation); + } + + /** + * Removes an annotation from the map + * + * @param id The identifier associated to the annotation to be removed + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void removeAnnotation(long id) { + annotationManager.removeAnnotation(id); + } + + /** + * Removes multiple annotations from the map. + * + * @param annotationList A list of annotation objects to remove. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void removeAnnotations(@NonNull List annotationList) { + annotationManager.removeAnnotations(annotationList); + } + + /** + * Removes all annotations from the map. + * + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void removeAnnotations() { + annotationManager.removeAnnotations(); + } + + /** + * Removes all markers, polylines, polygons, overlays, etc from the map. + * + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void clear() { + annotationManager.removeAnnotations(); + } + + /** + * Return a annotation based on its id. + * + * @param id the id used to look up an annotation + * @return An annotation with a matched id, null is returned if no match was found + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @Nullable + public Annotation getAnnotation(long id) { + return annotationManager.getAnnotation(id); + } + + /** + * Returns a list of all the annotations on the map. + * + * @return A list of all the annotation objects. The returned object is a copy so modifying this + * list will not update the map + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @NonNull + public List getAnnotations() { + return annotationManager.getAnnotations(); + } + + /** + * Returns a list of all the markers on the map. + * + * @return A list of all the markers objects. The returned object is a copy so modifying this + * list will not update the map. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @NonNull + public List getMarkers() { + return annotationManager.getMarkers(); + } + + /** + * Returns a list of all the polygons on the map. + * + * @return A list of all the polygon objects. The returned object is a copy so modifying this + * list will not update the map. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @NonNull + public List getPolygons() { + return annotationManager.getPolygons(); + } + + /** + * Returns a list of all the polylines on the map. + * + * @return A list of all the polylines objects. The returned object is a copy so modifying this + * list will not update the map. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @NonNull + public List getPolylines() { + return annotationManager.getPolylines(); + } + + /** + * Sets a callback that's invoked when the user clicks on a marker. + * + * @param listener The callback that's invoked when the user clicks on a marker. + * To unset the callback, use null. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void setOnMarkerClickListener(@Nullable OnMarkerClickListener listener) { + annotationManager.setOnMarkerClickListener(listener); + } + + /** + * Sets a callback that's invoked when the user clicks on a polygon. + * + * @param listener The callback that's invoked when the user clicks on a polygon. + * To unset the callback, use null. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void setOnPolygonClickListener(@Nullable OnPolygonClickListener listener) { + annotationManager.setOnPolygonClickListener(listener); + } + + /** + * Sets a callback that's invoked when the user clicks on a polyline. + * + * @param listener The callback that's invoked when the user clicks on a polyline. + * To unset the callback, use null. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void setOnPolylineClickListener(@Nullable OnPolylineClickListener listener) { + annotationManager.setOnPolylineClickListener(listener); + } + + /** + *

+ * Selects a marker. The selected marker will have it's info window opened. + * Any other open info windows will be closed unless isAllowConcurrentMultipleOpenInfoWindows() + * is true. + *

+ * Selecting an already selected marker will have no effect. + * + * @param marker The marker to select. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void selectMarker(@NonNull Marker marker) { + if (marker == null) { + Logger.w(TAG, "marker was null, so just returning"); + return; + } + annotationManager.selectMarker(marker); + } + + /** + * Deselects any currently selected marker. All markers will have it's info window closed. + * + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void deselectMarkers() { + annotationManager.deselectMarkers(); + } + + /** + * Deselects a currently selected marker. The selected marker will have it's info window closed. + * + * @param marker the marker to deselect + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void deselectMarker(@NonNull Marker marker) { + annotationManager.deselectMarker(marker); + } + + /** + * Gets the currently selected marker. + * + * @return The currently selected marker. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @NonNull + public List getSelectedMarkers() { + return annotationManager.getSelectedMarkers(); + } + + // + // InfoWindow + // + + /** + *

+ * Sets a custom renderer for the contents of info window. + *

+ * When set your callback is invoked when an info window is about to be shown. By returning + * a custom {@link View}, the default info window will be replaced. + * + * @param infoWindowAdapter The callback to be invoked when an info window will be shown. + * To unset the callback, use null. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void setInfoWindowAdapter(@Nullable InfoWindowAdapter infoWindowAdapter) { + annotationManager.getInfoWindowManager().setInfoWindowAdapter(infoWindowAdapter); + } + + /** + * Gets the callback to be invoked when an info window will be shown. + * + * @return The callback to be invoked when an info window will be shown. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + @Nullable + public InfoWindowAdapter getInfoWindowAdapter() { + return annotationManager.getInfoWindowManager().getInfoWindowAdapter(); + } + + /** + * Changes whether the map allows concurrent multiple infowindows to be shown. + * + * @param allow If true, map allows concurrent multiple infowindows to be shown. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public void setAllowConcurrentMultipleOpenInfoWindows(boolean allow) { + annotationManager.getInfoWindowManager().setAllowConcurrentMultipleOpenInfoWindows(allow); + } + + /** + * Returns whether the map allows concurrent multiple infowindows to be shown. + * + * @return If true, map allows concurrent multiple infowindows to be shown. + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public boolean isAllowConcurrentMultipleOpenInfoWindows() { + return annotationManager.getInfoWindowManager().isAllowConcurrentMultipleOpenInfoWindows(); + } + + // + // LatLngBounds + // + + /** + * Sets a LatLngBounds that constraints map transformations to this bounds. + *

+ * Set to null to clear current bounds, newly set bounds will override previously set bounds. + *

+ * + * @param latLngBounds the bounds to constrain the map with + */ + public void setLatLngBoundsForCameraTarget(@Nullable LatLngBounds latLngBounds) { + nativeMapView.setLatLngBounds(latLngBounds); + } + + + /** + * Get a camera position that fits a provided bounds and the current camera tilt and bearing. + * + * @param latLngBounds the bounds to set the map with + * @return the camera position that fits the bounds + */ + @Nullable + public CameraPosition getCameraForLatLngBounds(@NonNull LatLngBounds latLngBounds) { + // we use current camera tilt value to provide expected transformations as #11993 + return getCameraForLatLngBounds(latLngBounds, new int[] {0, 0, 0, 0}); + } + + + /** + * Get a camera position that fits a provided bounds and padding and the current camera tilt and bearing. + * + * @param latLngBounds the bounds to set the map with + * @param padding the padding to apply to the bounds + * @return the camera position that fits the bounds and padding + */ + @Nullable + public CameraPosition getCameraForLatLngBounds(@NonNull LatLngBounds latLngBounds, + @NonNull @Size(value = 4) int[] padding) { + // we use current camera tilt/bearing value to provide expected transformations as #11993 + return getCameraForLatLngBounds(latLngBounds, padding, transform.getRawBearing(), transform.getTilt()); + } + + + /** + * Get a camera position that fits a provided bounds, bearing and tilt. + * + * @param latLngBounds the bounds to set the map with + * @param bearing the bearing to transform the camera position with + * @param tilt to transform the camera position with + * @return the camera position that fits the bounds and given bearing and tilt + */ + @Nullable + public CameraPosition getCameraForLatLngBounds(@NonNull LatLngBounds latLngBounds, + @FloatRange(from = MapboxConstants.MINIMUM_DIRECTION, + to = MapboxConstants.MAXIMUM_DIRECTION) double bearing, + @FloatRange(from = MapboxConstants.MINIMUM_TILT, + to = MapboxConstants.MAXIMUM_TILT) double tilt) { + return getCameraForLatLngBounds(latLngBounds, new int[] {0, 0, 0, 0}, bearing, tilt); + } + + + /** + * Get a camera position that fits a provided bounds, padding, bearing and tilt. + * + * @param latLngBounds the bounds to set the map with + * @param padding the padding to apply to the bounds + * @param bearing the bearing to transform the camera position with + * @param tilt to transform the camera position with + * @return the camera position that fits the bounds, bearing and tilt + */ + @Nullable + public CameraPosition getCameraForLatLngBounds(@NonNull LatLngBounds latLngBounds, + @NonNull @Size(value = 4) int[] padding, + @FloatRange(from = MapboxConstants.MINIMUM_DIRECTION, + to = MapboxConstants.MAXIMUM_DIRECTION) double bearing, + @FloatRange(from = MapboxConstants.MINIMUM_TILT, + to = MapboxConstants.MAXIMUM_TILT) double tilt) { + return nativeMapView.getCameraForLatLngBounds(latLngBounds, padding, bearing, tilt); + } + + /** + * Get a camera position that fits a provided shape. + * + * @param geometry the geometry to wraps the map with + * @return the camera position that fits the geometry inside + */ + @Nullable + public CameraPosition getCameraForGeometry(@NonNull Geometry geometry) { + // we use current camera tilt value to provide expected transformations as #11993 + return getCameraForGeometry(geometry, new int[] {0, 0, 0, 0}); + } + + /** + * Get a camera position that fits a provided shape and padding. + * + * @param geometry the geometry to wraps the map with + * @param padding the padding to apply to the bounds + * @return the camera position that fits the geometry inside and padding + */ + @Nullable + public CameraPosition getCameraForGeometry(@NonNull Geometry geometry, + @NonNull @Size(value = 4) int[] padding) { + // we use current camera tilt/bearing value to provide expected transformations as #11993 + return getCameraForGeometry(geometry, padding, transform.getBearing(), transform.getTilt()); + } + + /** + * Get a camera position that fits a provided shape with a given bearing and tilt. + * + * @param geometry the geometry to wraps the map with + * @param bearing the bearing at which to compute the geometry's bounds + * @param tilt the tilt at which to compute the geometry's bounds + * @return the camera position that the geometry inside with bearing and tilt + */ + @Nullable + public CameraPosition getCameraForGeometry(@NonNull Geometry geometry, + @FloatRange(from = MapboxConstants.MINIMUM_DIRECTION, + to = MapboxConstants.MAXIMUM_DIRECTION) double bearing, + @FloatRange(from = MapboxConstants.MINIMUM_TILT, + to = MapboxConstants.MAXIMUM_TILT) double tilt) { + return getCameraForGeometry(geometry, new int[] {0, 0, 0, 0}, bearing, tilt); + } + + /** + * Get a camera position that fits a provided shape with a given padding, bearing and tilt. + * + * @param geometry the geometry to wraps the map with + * @param padding the padding to apply to the bounds + * @param bearing the bearing at which to compute the geometry's bounds + * @param tilt the tilt at which to compute the geometry's bounds + * @return the camera position that fits the geometry inside with padding, bearing and tilt + */ + @Nullable + public CameraPosition getCameraForGeometry(@NonNull Geometry geometry, + @NonNull @Size(value = 4) int[] padding, + @FloatRange(from = MapboxConstants.MINIMUM_DIRECTION, + to = MapboxConstants.MAXIMUM_DIRECTION) double bearing, + @FloatRange(from = MapboxConstants.MINIMUM_TILT, + to = MapboxConstants.MAXIMUM_TILT) double tilt) { + return nativeMapView.getCameraForGeometry(geometry, padding, bearing, tilt); + } + + // + // Padding + // + + /** + *

+ * Sets the distance from the edges of the map view's frame to the edges of the map + * view's logical viewport. + *

+ *

+ * When the value of this property is equal to {0,0,0,0}, viewport + * properties such as 'centerCoordinate' assume a viewport that matches the map + * view's frame. Otherwise, those properties are inset, excluding part of the + * frame from the viewport. For instance, if the only the top edge is inset, the + * map center is effectively shifted downward. + *

+ *

+ * This method sets the padding "lazily". + * This means that the padding is going to be applied with the next camera transformation. + * To apply the padding immediately use {@link CameraPosition.Builder#padding(double, double, double, double)} + * or {@link CameraUpdateFactory#paddingTo(double, double, double, double)}. + *

+ * + * @param left The left margin in pixels. + * @param top The top margin in pixels. + * @param right The right margin in pixels. + * @param bottom The bottom margin in pixels. + * @deprecated Use {@link CameraPosition.Builder#padding(double, double, double, double)} + * or {@link CameraUpdateFactory#paddingTo(double, double, double, double)} instead. + */ + @Deprecated + public void setPadding(int left, int top, int right, int bottom) { + // TODO padding should be passed as doubles + projection.setContentPadding(new int[] {left, top, right, bottom}); + uiSettings.invalidate(); + } + + /** + * Returns the current configured content padding on map view. This might return the currently visible padding + * or the padding cached but not yet applied by {@link #setPadding(int, int, int, int)}. + * + * @return An array with length 4 in the LTRB order. + * @deprecated Use {@link CameraPosition#padding} instead. + */ + @Deprecated + @NonNull + public int[] getPadding() { + // TODO this should return double[] (semver major change) + return projection.getContentPadding(); + } + + // + // Map events + // + + /** + * Adds a callback that is invoked when camera movement has ended. + * + * @param listener the listener to notify + */ + public void addOnCameraIdleListener(@NonNull OnCameraIdleListener listener) { + cameraChangeDispatcher.addOnCameraIdleListener(listener); + } + + /** + * Removes a callback that is invoked when camera movement has ended. + * + * @param listener the listener to remove + */ + public void removeOnCameraIdleListener(@NonNull OnCameraIdleListener listener) { + cameraChangeDispatcher.removeOnCameraIdleListener(listener); + } + + /** + * Adds a callback that is invoked when camera movement was cancelled. + * + * @param listener the listener to notify + */ + public void addOnCameraMoveCancelListener(@NonNull OnCameraMoveCanceledListener listener) { + cameraChangeDispatcher.addOnCameraMoveCancelListener(listener); + } + + /** + * Removes a callback that is invoked when camera movement was cancelled. + * + * @param listener the listener to remove + */ + public void removeOnCameraMoveCancelListener(@NonNull OnCameraMoveCanceledListener listener) { + cameraChangeDispatcher.removeOnCameraMoveCancelListener(listener); + } + + /** + * Adds a callback that is invoked when camera movement has started. + * + * @param listener the listener to notify + */ + public void addOnCameraMoveStartedListener(@NonNull OnCameraMoveStartedListener listener) { + cameraChangeDispatcher.addOnCameraMoveStartedListener(listener); + } + + /** + * Removes a callback that is invoked when camera movement has started. + * + * @param listener the listener to remove + */ + public void removeOnCameraMoveStartedListener(@NonNull OnCameraMoveStartedListener listener) { + cameraChangeDispatcher.removeOnCameraMoveStartedListener(listener); + } + + /** + * Adds a callback that is invoked when camera position changes. + * + * @param listener the listener to notify + */ + public void addOnCameraMoveListener(@NonNull OnCameraMoveListener listener) { + cameraChangeDispatcher.addOnCameraMoveListener(listener); + } + + /** + * Removes a callback that is invoked when camera position changes. + * + * @param listener the listener to remove + */ + public void removeOnCameraMoveListener(@NonNull OnCameraMoveListener listener) { + cameraChangeDispatcher.removeOnCameraMoveListener(listener); + } + + /** + * Sets a callback that's invoked on every frame rendered to the map view. + * + * @param listener The callback that's invoked on every frame rendered to the map view. + * To unset the callback, use null. + */ + public void setOnFpsChangedListener(@Nullable OnFpsChangedListener listener) { + onFpsChangedListener = listener; + nativeMapView.setOnFpsChangedListener(listener); + } + + // used by MapView + @Nullable + OnFpsChangedListener getOnFpsChangedListener() { + return onFpsChangedListener; + } + + /** + * Adds a callback that's invoked when the map is flinged. + * + * @param listener The callback that's invoked when the map is flinged. + */ + public void addOnFlingListener(@NonNull OnFlingListener listener) { + onGesturesManagerInteractionListener.onAddFlingListener(listener); + } + + /** + * Removes a callback that's invoked when the map is flinged. + * + * @param listener The callback that's invoked when the map is flinged. + */ + public void removeOnFlingListener(@NonNull OnFlingListener listener) { + onGesturesManagerInteractionListener.onRemoveFlingListener(listener); + } + + /** + * Adds a callback that's invoked when the map is moved. + * + * @param listener The callback that's invoked when the map is moved. + */ + public void addOnMoveListener(@NonNull OnMoveListener listener) { + onGesturesManagerInteractionListener.onAddMoveListener(listener); + } + + /** + * Removes a callback that's invoked when the map is moved. + * + * @param listener The callback that's invoked when the map is moved. + */ + public void removeOnMoveListener(@NonNull OnMoveListener listener) { + onGesturesManagerInteractionListener.onRemoveMoveListener(listener); + } + + /** + * Adds a callback that's invoked when the map is rotated. + * + * @param listener The callback that's invoked when the map is rotated. + */ + public void addOnRotateListener(@NonNull OnRotateListener listener) { + onGesturesManagerInteractionListener.onAddRotateListener(listener); + } + + /** + * Removes a callback that's invoked when the map is rotated. + * + * @param listener The callback that's invoked when the map is rotated. + */ + public void removeOnRotateListener(@NonNull OnRotateListener listener) { + onGesturesManagerInteractionListener.onRemoveRotateListener(listener); + } + + /** + * Adds a callback that's invoked when the map is scaled. + * + * @param listener The callback that's invoked when the map is scaled. + */ + public void addOnScaleListener(@NonNull OnScaleListener listener) { + onGesturesManagerInteractionListener.onAddScaleListener(listener); + } + + /** + * Removes a callback that's invoked when the map is scaled. + * + * @param listener The callback that's invoked when the map is scaled. + */ + public void removeOnScaleListener(@NonNull OnScaleListener listener) { + onGesturesManagerInteractionListener.onRemoveScaleListener(listener); + } + + /** + * Adds a callback that's invoked when the map is tilted. + * + * @param listener The callback that's invoked when the map is tilted. + */ + public void addOnShoveListener(@NonNull OnShoveListener listener) { + onGesturesManagerInteractionListener.onAddShoveListener(listener); + } + + /** + * Remove a callback that's invoked when the map is tilted. + * + * @param listener The callback that's invoked when the map is tilted. + */ + public void removeOnShoveListener(@NonNull OnShoveListener listener) { + onGesturesManagerInteractionListener.onRemoveShoveListener(listener); + } + + /** + * Sets a custom {@link AndroidGesturesManager} to handle {@link android.view.MotionEvent}s + * registered by the {@link MapView}. + * + * @param androidGesturesManager Gestures manager that interprets gestures based on the motion events. + * @param attachDefaultListeners If true, pre-defined listeners will be attach + * to change map based on {@link AndroidGesturesManager} callbacks. + * @param setDefaultMutuallyExclusives If true, pre-defined mutually exclusive gesture sets + * will be added to the passed gestures manager. + * @see mapbox-gestures-android library + */ + public void setGesturesManager(@NonNull AndroidGesturesManager androidGesturesManager, boolean attachDefaultListeners, + boolean setDefaultMutuallyExclusives) { + onGesturesManagerInteractionListener.setGesturesManager( + androidGesturesManager, attachDefaultListeners, setDefaultMutuallyExclusives); + } + + /** + * Get current {@link AndroidGesturesManager} that handles {@link android.view.MotionEvent}s + * registered by the {@link MapView} + * + * @return Current gestures manager. + */ + @NonNull + public AndroidGesturesManager getGesturesManager() { + return onGesturesManagerInteractionListener.getGesturesManager(); + } + + /** + * Interrupts any ongoing gesture velocity animations. + */ + public void cancelAllVelocityAnimations() { + onGesturesManagerInteractionListener.cancelAllVelocityAnimations(); + } + + /** + * Adds a callback that's invoked when the user clicks on the map view. + * + * @param listener The callback that's invoked when the user clicks on the map view. + */ + public void addOnMapClickListener(@NonNull OnMapClickListener listener) { + onGesturesManagerInteractionListener.onAddMapClickListener(listener); + } + + /** + * Removes a callback that's invoked when the user clicks on the map view. + * + * @param listener The callback that's invoked when the user clicks on the map view. + */ + public void removeOnMapClickListener(@NonNull OnMapClickListener listener) { + onGesturesManagerInteractionListener.onRemoveMapClickListener(listener); + } + + /** + * Adds a callback that's invoked when the user long clicks on the map view. + * + * @param listener The callback that's invoked when the user long clicks on the map view. + */ + public void addOnMapLongClickListener(@NonNull OnMapLongClickListener listener) { + onGesturesManagerInteractionListener.onAddMapLongClickListener(listener); + } + + /** + * Removes a callback that's invoked when the user long clicks on the map view. + * + * @param listener The callback that's invoked when the user long clicks on the map view. + */ + public void removeOnMapLongClickListener(@NonNull OnMapLongClickListener listener) { + onGesturesManagerInteractionListener.onRemoveMapLongClickListener(listener); + } + + /** + * Sets a callback that's invoked when the user clicks on an info window. + * + * @param listener The callback that's invoked when the user clicks on an info window. + * To unset the callback, use null. + */ + public void setOnInfoWindowClickListener(@Nullable OnInfoWindowClickListener listener) { + annotationManager.getInfoWindowManager().setOnInfoWindowClickListener(listener); + } + + /** + * Return the InfoWindow click listener + * + * @return Current active InfoWindow Click Listener + */ + @Nullable + public OnInfoWindowClickListener getOnInfoWindowClickListener() { + return annotationManager.getInfoWindowManager().getOnInfoWindowClickListener(); + } + + /** + * Sets a callback that's invoked when a marker's info window is long pressed. + * + * @param listener The callback that's invoked when a marker's info window is long pressed. To unset the callback, + * use null. + */ + public void setOnInfoWindowLongClickListener(@Nullable OnInfoWindowLongClickListener + listener) { + annotationManager.getInfoWindowManager().setOnInfoWindowLongClickListener(listener); + } + + /** + * Return the InfoWindow long click listener + * + * @return Current active InfoWindow long Click Listener + */ + @Nullable + public OnInfoWindowLongClickListener getOnInfoWindowLongClickListener() { + return annotationManager.getInfoWindowManager().getOnInfoWindowLongClickListener(); + } + + /** + * Set an callback to be invoked when an InfoWindow closes. + * + * @param listener callback invoked when an InfoWindow closes + */ + public void setOnInfoWindowCloseListener(@Nullable OnInfoWindowCloseListener listener) { + annotationManager.getInfoWindowManager().setOnInfoWindowCloseListener(listener); + } + + /** + * Return the InfoWindow close listener + * + * @return Current active InfoWindow Close Listener + */ + @Nullable + public OnInfoWindowCloseListener getOnInfoWindowCloseListener() { + return annotationManager.getInfoWindowManager().getOnInfoWindowCloseListener(); + } + + // + // Invalidate + // + + /** + * Takes a snapshot of the map. + * + * @param callback Callback method invoked when the snapshot is taken. + */ + public void snapshot(@NonNull SnapshotReadyCallback callback) { + nativeMapView.addSnapshotCallback(callback); + } + + /** + * Queries the map for rendered features. + *

+ * Returns an empty list if either the map or underlying render surface has been destroyed. + *

+ * + * @param coordinates the point to query + * @param layerIds optionally - only query these layers + * @return the list of feature + */ + @NonNull + public List queryRenderedFeatures(@NonNull PointF coordinates, @Nullable String... layerIds) { + return nativeMapView.queryRenderedFeatures(coordinates, layerIds, null); + } + + /** + * Queries the map for rendered features + *

+ * Returns an empty list if either the map or underlying render surface has been destroyed. + *

+ * + * @param coordinates the point to query + * @param filter filters the returned features with an expression + * @param layerIds optionally - only query these layers + * @return the list of feature + */ + @NonNull + public List queryRenderedFeatures(@NonNull PointF coordinates, + @Nullable Expression filter, + @Nullable String... layerIds) { + return nativeMapView.queryRenderedFeatures(coordinates, layerIds, filter); + } + + /** + * Queries the map for rendered features + *

+ * Returns an empty list if either the map or underlying render surface has been destroyed. + *

+ * + * @param coordinates the box to query + * @param layerIds optionally - only query these layers + * @return the list of feature + */ + @NonNull + public List queryRenderedFeatures(@NonNull RectF coordinates, @Nullable String... layerIds) { + return nativeMapView.queryRenderedFeatures(coordinates, layerIds, null); + } + + /** + * Queries the map for rendered features + *

+ * Returns an empty list if either the map or underlying render surface has been destroyed. + *

+ * + * @param coordinates the box to query + * @param filter filters the returned features with an expression + * @param layerIds optionally - only query these layers + * @return the list of feature + */ + @NonNull + public List queryRenderedFeatures(@NonNull RectF coordinates, + @Nullable Expression filter, + @Nullable String... layerIds) { + return nativeMapView.queryRenderedFeatures(coordinates, layerIds, filter); + } + + // + // LocationComponent + // + + void injectLocationComponent(LocationComponent locationComponent) { + this.locationComponent = locationComponent; + } + + void injectAnnotationManager(AnnotationManager annotationManager) { + this.annotationManager = annotationManager.bind(this); + } + + /** + * Returns the {@link LocationComponent} that can be used to display user's location on the map. + *

+ * Use {@link LocationComponent#activateLocationComponent(Context, Style)} or any overload to activate the component, + * then, enable it with {@link LocationComponent#setLocationComponentEnabled(boolean)}. + *

+ * You can customize the location icon and more with {@link com.mapbox.mapboxsdk.location.LocationComponentOptions}. + * + * @return the Location Component + */ + @NonNull + public LocationComponent getLocationComponent() { + return locationComponent; + } + + // + // Interfaces + // + + /** + * Interface definition for a callback to be invoked when the map is flinged. + * + * @see MapboxMap#addOnFlingListener(OnFlingListener) + */ + public interface OnFlingListener { + /** + * Called when the map is flinged. + */ + void onFling(); + } + + /** + * Interface definition for a callback to be invoked when the map is moved. + * + * @see MapboxMap#addOnMoveListener(OnMoveListener) + */ + public interface OnMoveListener { + void onMoveBegin(@NonNull MoveGestureDetector detector); + + void onMove(@NonNull MoveGestureDetector detector); + + void onMoveEnd(@NonNull MoveGestureDetector detector); + } + + /** + * Interface definition for a callback to be invoked when the map is rotated. + * + * @see MapboxMap#addOnRotateListener(OnRotateListener) + */ + public interface OnRotateListener { + void onRotateBegin(@NonNull RotateGestureDetector detector); + + void onRotate(@NonNull RotateGestureDetector detector); + + void onRotateEnd(@NonNull RotateGestureDetector detector); + } + + /** + * Interface definition for a callback to be invoked when the map is scaled. + * + * @see MapboxMap#addOnScaleListener(OnScaleListener) + */ + public interface OnScaleListener { + void onScaleBegin(@NonNull StandardScaleGestureDetector detector); + + void onScale(@NonNull StandardScaleGestureDetector detector); + + void onScaleEnd(@NonNull StandardScaleGestureDetector detector); + } + + /** + * Interface definition for a callback to be invoked when the map is tilted. + * + * @see MapboxMap#addOnShoveListener(OnShoveListener) + */ + public interface OnShoveListener { + void onShoveBegin(@NonNull ShoveGestureDetector detector); + + void onShove(@NonNull ShoveGestureDetector detector); + + void onShoveEnd(@NonNull ShoveGestureDetector detector); + } + + /** + * Interface definition for a callback to be invoked for when the camera motion starts. + */ + public interface OnCameraMoveStartedListener { + int REASON_API_GESTURE = 1; + int REASON_DEVELOPER_ANIMATION = 2; + int REASON_API_ANIMATION = 3; + + /** + * Called when the camera starts moving after it has been idle or when the reason for camera motion has changed. + * + * @param reason the reason for the camera change + */ + void onCameraMoveStarted(int reason); + } + + /** + * Interface definition for a callback to be invoked for when the camera changes position. + */ + public interface OnCameraMoveListener { + /** + * Called repeatedly as the camera continues to move after an onCameraMoveStarted call. + * This may be called as often as once every frame and should not perform expensive operations. + */ + void onCameraMove(); + } + + /** + * Interface definition for a callback to be invoked for when the camera's motion has been stopped or when the camera + * starts moving for a new reason. + */ + public interface OnCameraMoveCanceledListener { + /** + * Called when the developer explicitly calls the cancelTransitions() method or if the reason for camera motion has + * changed before the onCameraIdle had a chance to fire after the previous animation. + * Do not update or animate the camera from within this method. + */ + void onCameraMoveCanceled(); + } + + /** + * Interface definition for a callback to be invoked for when camera movement has ended. + */ + public interface OnCameraIdleListener { + /** + * Called when camera movement has ended. + */ + void onCameraIdle(); + } + + /** + * Interface definition for a callback to be invoked for when the compass is animating. + */ + public interface OnCompassAnimationListener { + /** + * Called repeatedly as the compass continues to move after clicking on it. + */ + void onCompassAnimation(); + + /** + * Called when compass animation has ended. + */ + void onCompassAnimationFinished(); + } + + /** + * Interface definition for a callback to be invoked when a frame is rendered to the map view. + * + * @see MapboxMap#setOnFpsChangedListener(OnFpsChangedListener) + */ + public interface OnFpsChangedListener { + /** + * Called for every frame rendered to the map view. + * + * @param fps The average number of frames rendered over the last second. + */ + void onFpsChanged(double fps); + } + + /** + * Interface definition for a callback to be invoked when a user registers an listener that is + * related to touch and click events. + */ + interface OnGesturesManagerInteractionListener { + + void onAddMapClickListener(OnMapClickListener listener); + + void onRemoveMapClickListener(OnMapClickListener listener); + + void onAddMapLongClickListener(OnMapLongClickListener listener); + + void onRemoveMapLongClickListener(OnMapLongClickListener listener); + + void onAddFlingListener(OnFlingListener listener); + + void onRemoveFlingListener(OnFlingListener listener); + + void onAddMoveListener(OnMoveListener listener); + + void onRemoveMoveListener(OnMoveListener listener); + + void onAddRotateListener(OnRotateListener listener); + + void onRemoveRotateListener(OnRotateListener listener); + + void onAddScaleListener(OnScaleListener listener); + + void onRemoveScaleListener(OnScaleListener listener); + + void onAddShoveListener(OnShoveListener listener); + + void onRemoveShoveListener(OnShoveListener listener); + + AndroidGesturesManager getGesturesManager(); + + void setGesturesManager(AndroidGesturesManager gesturesManager, boolean attachDefaultListeners, + boolean setDefaultMutuallyExclusives); + + void cancelAllVelocityAnimations(); + } + + /** + * Interface definition for a callback to be invoked when the user clicks on the map view. + * + * @see MapboxMap#addOnMapClickListener(OnMapClickListener) + */ + public interface OnMapClickListener { + /** + * Called when the user clicks on the map view. + * + * @param point The projected map coordinate the user clicked on. + * @return True if this click should be consumed and not passed further to other listeners registered afterwards, + * false otherwise. + */ + boolean onMapClick(@NonNull LatLng point); + } + + /** + * Interface definition for a callback to be invoked when the user long clicks on the map view. + * + * @see MapboxMap#addOnMapLongClickListener(OnMapLongClickListener) + */ + public interface OnMapLongClickListener { + /** + * Called when the user long clicks on the map view. + * + * @param point The projected map coordinate the user long clicked on. + * @return True if this click should be consumed and not passed further to other listeners registered afterwards, + * false otherwise. + */ + boolean onMapLongClick(@NonNull LatLng point); + } + + /** + * Interface definition for a callback to be invoked when the user clicks on a marker. + * + * @see MapboxMap#setOnMarkerClickListener(OnMarkerClickListener) + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public interface OnMarkerClickListener { + /** + * Called when the user clicks on a marker. + * + * @param marker The marker the user clicked on. + * @return If true the listener has consumed the event and the info window will not be shown. + */ + boolean onMarkerClick(@NonNull Marker marker); + } + + /** + * Interface definition for a callback to be invoked when the user clicks on a polygon. + * + * @see MapboxMap#setOnPolygonClickListener(OnPolygonClickListener) + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public interface OnPolygonClickListener { + /** + * Called when the user clicks on a polygon. + * + * @param polygon The polygon the user clicked on. + */ + void onPolygonClick(@NonNull Polygon polygon); + } + + /** + * Interface definition for a callback to be invoked when the user clicks on a polyline. + * + * @see MapboxMap#setOnPolylineClickListener(OnPolylineClickListener) + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public interface OnPolylineClickListener { + /** + * Called when the user clicks on a polyline. + * + * @param polyline The polyline the user clicked on. + */ + void onPolylineClick(@NonNull Polyline polyline); + } + + /** + * Interface definition for a callback to be invoked when the user clicks on an info window. + * + * @see MapboxMap#setOnInfoWindowClickListener(OnInfoWindowClickListener) + */ + public interface OnInfoWindowClickListener { + /** + * Called when the user clicks on an info window. + * + * @param marker The marker of the info window the user clicked on. + * @return If true the listener has consumed the event and the info window will not be closed. + */ + boolean onInfoWindowClick(@NonNull Marker marker); + } + + /** + * Interface definition for a callback to be invoked when the user long presses on a marker's info window. + * + * @see MapboxMap#setOnInfoWindowClickListener(OnInfoWindowClickListener) + */ + public interface OnInfoWindowLongClickListener { + + /** + * Called when the user makes a long-press gesture on the marker's info window. + * + * @param marker The marker were the info window is attached to + */ + void onInfoWindowLongClick(@NonNull Marker marker); + } + + /** + * Interface definition for a callback to be invoked when a marker's info window is closed. + * + * @see MapboxMap#setOnInfoWindowCloseListener(OnInfoWindowCloseListener) + */ + public interface OnInfoWindowCloseListener { + + /** + * Called when the marker's info window is closed. + * + * @param marker The marker of the info window that was closed. + */ + void onInfoWindowClose(@NonNull Marker marker); + } + + /** + * Interface definition for a callback to be invoked when an info window will be shown. + * + * @see MapboxMap#setInfoWindowAdapter(InfoWindowAdapter) + * @deprecated As of 7.0.0, + * use + * Mapbox Annotation Plugin instead + */ + @Deprecated + public interface InfoWindowAdapter { + /** + * Called when an info window will be shown as a result of a marker click. + * + * @param marker The marker the user clicked on. + * @return View to be shown as a info window. If null is returned the default + * info window will be shown. + */ + @Nullable + View getInfoWindow(@NonNull Marker marker); + } + + /** + * Interface definition for a callback to be invoked when a task is complete or cancelled. + */ + public interface CancelableCallback { + /** + * Invoked when a task is cancelled. + */ + void onCancel(); + + /** + * Invoked when a task is complete. + */ + void onFinish(); + } + + /** + * Interface definition for a callback to be invoked when the snapshot has been taken. + */ + public interface SnapshotReadyCallback { + /** + * Invoked when the snapshot has been taken. + * + * @param snapshot the snapshot bitmap + */ + void onSnapshotReady(@NonNull Bitmap snapshot); + } + + /** + * Internal use. + */ + public interface OnDeveloperAnimationListener { + + /** + * Notifies listener when a developer invoked animation is about to start. + */ + void onDeveloperAnimationStarted(); + } + + // + // Used for instrumentation testing + // + @NonNull + Transform getTransform() { + return transform; + } + + private void notifyDeveloperAnimationListeners() { + for (OnDeveloperAnimationListener listener : developerAnimationStartedListeners) { + listener.onDeveloperAnimationStarted(); + } + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMapOptions.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMapOptions.java new file mode 100644 index 000000000..6cd3271d1 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMapOptions.java @@ -0,0 +1,1242 @@ +package com.mapbox.mapboxsdk.maps; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.content.res.ResourcesCompat; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.Gravity; + +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.constants.MapboxConstants; +import com.mapbox.mapboxsdk.utils.BitmapUtils; +import com.mapbox.mapboxsdk.utils.FontUtils; + +import java.util.Arrays; + +/** + * Defines configuration MapboxMapMapOptions for a MapboxMap. These options can be used when adding a + * map to your application programmatically (as opposed to via XML). If you are using a MapFragment, + * you can pass these options in using the static factory method newInstance(MapboxMapOptions). + * If you are using a MapView, you can pass these options in using the constructor + * MapView(Context, MapboxMapOptions). If you add a map using XML, then you can apply these options + * using custom XML tags. + */ +public class MapboxMapOptions implements Parcelable { + + private static final int LIGHT_GRAY = 0xFFF0E9E1; // RGB(240, 233, 225)) + private static final float FOUR_DP = 4f; + private static final float NINETY_TWO_DP = 92f; + private static final int UNDEFINED_COLOR = -1; + + private CameraPosition cameraPosition; + + private boolean debugActive; + + private boolean compassEnabled = true; + private boolean fadeCompassFacingNorth = true; + private int compassGravity = Gravity.TOP | Gravity.END; + private int[] compassMargins; + private Drawable compassImage; + + private boolean logoEnabled = true; + private int logoGravity = Gravity.BOTTOM | Gravity.START; + private int[] logoMargins; + + @ColorInt + private int attributionTintColor = UNDEFINED_COLOR; + private boolean attributionEnabled = true; + private int attributionGravity = Gravity.BOTTOM | Gravity.START; + private int[] attributionMargins; + + private double minZoom = MapboxConstants.MINIMUM_ZOOM; + private double maxZoom = MapboxConstants.MAXIMUM_ZOOM; + + private boolean rotateGesturesEnabled = true; + private boolean scrollGesturesEnabled = true; + private boolean tiltGesturesEnabled = true; + private boolean zoomGesturesEnabled = true; + private boolean doubleTapGesturesEnabled = true; + private boolean quickZoomGesturesEnabled = true; + + private boolean prefetchesTiles = true; + private boolean zMediaOverlay = false; + + private boolean localIdeographFontFamilyEnabled = true; + private String localIdeographFontFamily; + private String[] localIdeographFontFamilies; + + private String apiBaseUri; + + private boolean textureMode; + private boolean translucentTextureSurface; + + @ColorInt + private int foregroundLoadColor; + + private float pixelRatio; + + private boolean crossSourceCollisions = true; + + /** + * Creates a new MapboxMapOptions object. + * + * @deprecated Use {@link #createFromAttributes(Context, AttributeSet)} instead. + */ + @Deprecated + public MapboxMapOptions() { + } + + private MapboxMapOptions(Parcel in) { + cameraPosition = in.readParcelable(CameraPosition.class.getClassLoader()); + debugActive = in.readByte() != 0; + + compassEnabled = in.readByte() != 0; + compassGravity = in.readInt(); + compassMargins = in.createIntArray(); + fadeCompassFacingNorth = in.readByte() != 0; + + Bitmap compassBitmap = in.readParcelable(getClass().getClassLoader()); + if (compassBitmap != null) { + compassImage = new BitmapDrawable(compassBitmap); + } + + logoEnabled = in.readByte() != 0; + logoGravity = in.readInt(); + logoMargins = in.createIntArray(); + + attributionEnabled = in.readByte() != 0; + attributionGravity = in.readInt(); + attributionMargins = in.createIntArray(); + attributionTintColor = in.readInt(); + + minZoom = in.readDouble(); + maxZoom = in.readDouble(); + + rotateGesturesEnabled = in.readByte() != 0; + scrollGesturesEnabled = in.readByte() != 0; + tiltGesturesEnabled = in.readByte() != 0; + zoomGesturesEnabled = in.readByte() != 0; + doubleTapGesturesEnabled = in.readByte() != 0; + quickZoomGesturesEnabled = in.readByte() != 0; + + apiBaseUri = in.readString(); + textureMode = in.readByte() != 0; + translucentTextureSurface = in.readByte() != 0; + prefetchesTiles = in.readByte() != 0; + zMediaOverlay = in.readByte() != 0; + localIdeographFontFamilyEnabled = in.readByte() != 0; + localIdeographFontFamily = in.readString(); + localIdeographFontFamilies = in.createStringArray(); + pixelRatio = in.readFloat(); + foregroundLoadColor = in.readInt(); + crossSourceCollisions = in.readByte() != 0; + } + + /** + * Creates a default MapboxMapsOptions from a given context. + * + * @param context Context related to a map view. + * @return the MapboxMapOptions created from attributes + */ + @NonNull + public static MapboxMapOptions createFromAttributes(@NonNull Context context) { + return createFromAttributes(context, null); + } + + /** + * Creates a MapboxMapsOptions from the attribute set. + * + * @param context Context related to a map view. + * @param attrs Attributeset containing configuration + * @return the MapboxMapOptions created from attributes + */ + @NonNull + public static MapboxMapOptions createFromAttributes(@NonNull Context context, @Nullable AttributeSet attrs) { + TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.mapbox_MapView, 0, 0); + return createFromAttributes(new MapboxMapOptions(), context, typedArray); + } + + @VisibleForTesting + static MapboxMapOptions createFromAttributes(@NonNull MapboxMapOptions mapboxMapOptions, + @NonNull Context context, + @Nullable TypedArray typedArray) { + float pxlRatio = context.getResources().getDisplayMetrics().density; + try { + mapboxMapOptions.camera(new CameraPosition.Builder(typedArray).build()); + + // deprecated + mapboxMapOptions.apiBaseUrl(typedArray.getString(R.styleable.mapbox_MapView_mapbox_apiBaseUrl)); + + String baseUri = typedArray.getString(R.styleable.mapbox_MapView_mapbox_apiBaseUri); + if (!TextUtils.isEmpty(baseUri)) { + // override deprecated property if a value of the new type was provided + mapboxMapOptions.apiBaseUri(baseUri); + } + + mapboxMapOptions.zoomGesturesEnabled( + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_uiZoomGestures, true)); + mapboxMapOptions.scrollGesturesEnabled( + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_uiScrollGestures, true)); + mapboxMapOptions.rotateGesturesEnabled( + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_uiRotateGestures, true)); + mapboxMapOptions.tiltGesturesEnabled( + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_uiTiltGestures, true)); + mapboxMapOptions.doubleTapGesturesEnabled( + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_uiDoubleTapGestures, true)); + mapboxMapOptions.quickZoomGesturesEnabled( + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_uiQuickZoomGestures, true)); + + mapboxMapOptions.maxZoomPreference(typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_cameraZoomMax, + MapboxConstants.MAXIMUM_ZOOM)); + mapboxMapOptions.minZoomPreference(typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_cameraZoomMin, + MapboxConstants.MINIMUM_ZOOM)); + + mapboxMapOptions.compassEnabled(typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_uiCompass, true)); + mapboxMapOptions.compassGravity(typedArray.getInt(R.styleable.mapbox_MapView_mapbox_uiCompassGravity, + Gravity.TOP | Gravity.END)); + mapboxMapOptions.compassMargins(new int[] { + (int) (typedArray.getDimension(R.styleable.mapbox_MapView_mapbox_uiCompassMarginLeft, + FOUR_DP * pxlRatio)), + ((int) typedArray.getDimension(R.styleable.mapbox_MapView_mapbox_uiCompassMarginTop, + FOUR_DP * pxlRatio)), + ((int) typedArray.getDimension(R.styleable.mapbox_MapView_mapbox_uiCompassMarginRight, + FOUR_DP * pxlRatio)), + ((int) typedArray.getDimension(R.styleable.mapbox_MapView_mapbox_uiCompassMarginBottom, + FOUR_DP * pxlRatio))}); + mapboxMapOptions.compassFadesWhenFacingNorth(typedArray.getBoolean( + R.styleable.mapbox_MapView_mapbox_uiCompassFadeFacingNorth, true)); + Drawable compassDrawable = typedArray.getDrawable( + R.styleable.mapbox_MapView_mapbox_uiCompassDrawable); + if (compassDrawable == null) { + compassDrawable = ResourcesCompat.getDrawable(context.getResources(), R.drawable.mapbox_compass_icon, null); + } + mapboxMapOptions.compassImage(compassDrawable); + + mapboxMapOptions.logoEnabled(typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_uiLogo, true)); + mapboxMapOptions.logoGravity(typedArray.getInt(R.styleable.mapbox_MapView_mapbox_uiLogoGravity, + Gravity.BOTTOM | Gravity.START)); + mapboxMapOptions.logoMargins(new int[] { + (int) (typedArray.getDimension(R.styleable.mapbox_MapView_mapbox_uiLogoMarginLeft, + FOUR_DP * pxlRatio)), + (int) (typedArray.getDimension(R.styleable.mapbox_MapView_mapbox_uiLogoMarginTop, + FOUR_DP * pxlRatio)), + (int) (typedArray.getDimension(R.styleable.mapbox_MapView_mapbox_uiLogoMarginRight, + FOUR_DP * pxlRatio)), + (int) (typedArray.getDimension(R.styleable.mapbox_MapView_mapbox_uiLogoMarginBottom, + FOUR_DP * pxlRatio))}); + + mapboxMapOptions.attributionTintColor(typedArray.getColor( + R.styleable.mapbox_MapView_mapbox_uiAttributionTintColor, UNDEFINED_COLOR)); + mapboxMapOptions.attributionEnabled(typedArray.getBoolean( + R.styleable.mapbox_MapView_mapbox_uiAttribution, true)); + mapboxMapOptions.attributionGravity(typedArray.getInt( + R.styleable.mapbox_MapView_mapbox_uiAttributionGravity, Gravity.BOTTOM | Gravity.START)); + mapboxMapOptions.attributionMargins(new int[] { + (int) (typedArray.getDimension(R.styleable.mapbox_MapView_mapbox_uiAttributionMarginLeft, + NINETY_TWO_DP * pxlRatio)), + (int) (typedArray.getDimension(R.styleable.mapbox_MapView_mapbox_uiAttributionMarginTop, + FOUR_DP * pxlRatio)), + (int) (typedArray.getDimension(R.styleable.mapbox_MapView_mapbox_uiAttributionMarginRight, + FOUR_DP * pxlRatio)), + (int) (typedArray.getDimension(R.styleable.mapbox_MapView_mapbox_uiAttributionMarginBottom, + FOUR_DP * pxlRatio))}); + mapboxMapOptions.textureMode( + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_renderTextureMode, false)); + mapboxMapOptions.translucentTextureSurface( + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_renderTextureTranslucentSurface, false)); + mapboxMapOptions.setPrefetchesTiles( + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_enableTilePrefetch, true)); + mapboxMapOptions.renderSurfaceOnTop( + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_enableZMediaOverlay, false)); + + mapboxMapOptions.localIdeographFontFamilyEnabled = + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_localIdeographEnabled, true); + + int localIdeographFontFamiliesResId = + typedArray.getResourceId(R.styleable.mapbox_MapView_mapbox_localIdeographFontFamilies, 0); + if (localIdeographFontFamiliesResId != 0) { + String[] localIdeographFontFamilies = + context.getResources().getStringArray(localIdeographFontFamiliesResId); + mapboxMapOptions.localIdeographFontFamily(localIdeographFontFamilies); + } else { + // did user provide xml font string? + String localIdeographFontFamily = + typedArray.getString(R.styleable.mapbox_MapView_mapbox_localIdeographFontFamily); + if (localIdeographFontFamily == null) { + localIdeographFontFamily = MapboxConstants.DEFAULT_FONT; + } + mapboxMapOptions.localIdeographFontFamily(localIdeographFontFamily); + } + + mapboxMapOptions.pixelRatio( + typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_pixelRatio, 0)); + mapboxMapOptions.foregroundLoadColor( + typedArray.getInt(R.styleable.mapbox_MapView_mapbox_foregroundLoadColor, LIGHT_GRAY) + ); + mapboxMapOptions.crossSourceCollisions( + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_cross_source_collisions, true) + ); + } finally { + typedArray.recycle(); + } + return mapboxMapOptions; + } + + /** + * Specifies the URL used for API endpoint. + * + * @param apiBaseUrl The base of our API endpoint + * @return This + * @deprecated use {@link #apiBaseUri} instead + */ + @Deprecated + @NonNull + public MapboxMapOptions apiBaseUrl(String apiBaseUrl) { + this.apiBaseUri = apiBaseUrl; + return this; + } + + /** + * Specifies the URI used for API endpoint. + * + * @param apiBaseUri The base of our API endpoint + * @return This + */ + @NonNull + public MapboxMapOptions apiBaseUri(String apiBaseUri) { + this.apiBaseUri = apiBaseUri; + return this; + } + + /** + * Specifies a the initial camera position for the map view. + * + * @param cameraPosition Inital camera position + * @return This + */ + @NonNull + public MapboxMapOptions camera(CameraPosition cameraPosition) { + this.cameraPosition = cameraPosition; + return this; + } + + /** + * Specifies the used debug type for a map view. + * + * @param enabled True is debug is enabled + * @return This + */ + @NonNull + public MapboxMapOptions debugActive(boolean enabled) { + debugActive = enabled; + return this; + } + + /** + * Specifies the used minimum zoom level for a map view. + * + * @param minZoom Zoom level to be used + * @return This + */ + @NonNull + public MapboxMapOptions minZoomPreference(double minZoom) { + this.minZoom = minZoom; + return this; + } + + /** + * Specifies the used maximum zoom level for a map view. + * + * @param maxZoom Zoom level to be used + * @return This + */ + @NonNull + public MapboxMapOptions maxZoomPreference(double maxZoom) { + this.maxZoom = maxZoom; + return this; + } + + /** + * Specifies the visibility state of a mapbox_compass_icon for a map view. + * + * @param enabled True and mapbox_compass_icon is shown + * @return This + */ + @NonNull + public MapboxMapOptions compassEnabled(boolean enabled) { + compassEnabled = enabled; + return this; + } + + /** + * Specifies the gravity state of mapbox_compass_icon for a map view. + * + * @param gravity Android SDK Gravity. + * @return This + */ + @NonNull + public MapboxMapOptions compassGravity(int gravity) { + compassGravity = gravity; + return this; + } + + /** + * Specifies the margin state of mapbox_compass_icon for a map view + * + * @param margins 4 long array for LTRB margins + * @return This + */ + @NonNull + public MapboxMapOptions compassMargins(int[] margins) { + compassMargins = margins; + return this; + } + + /** + * Specifies if the mapbox_compass_icon fades to invisible when facing north. + *

+ * By default this value is true. + *

+ * + * @param compassFadeWhenFacingNorth true is mapbox_compass_icon fades to invisble + * @return This + */ + @NonNull + public MapboxMapOptions compassFadesWhenFacingNorth(boolean compassFadeWhenFacingNorth) { + this.fadeCompassFacingNorth = compassFadeWhenFacingNorth; + return this; + } + + /** + * Specifies the image of the CompassView. + *

+ * By default this value is R.drawable.mapbox_compass_icon. + *

+ * + * @param compass the drawable to show as image compass + * @return This + */ + @NonNull + public MapboxMapOptions compassImage(Drawable compass) { + this.compassImage = compass; + return this; + } + + /** + * Specifies the visibility state of a logo for a map view. + * + * @param enabled True and logo is shown + * @return This + */ + @NonNull + public MapboxMapOptions logoEnabled(boolean enabled) { + logoEnabled = enabled; + return this; + } + + /** + * Specifies the gravity state of logo for a map view. + * + * @param gravity Android SDK Gravity. + * @return This + */ + @NonNull + public MapboxMapOptions logoGravity(int gravity) { + logoGravity = gravity; + return this; + } + + /** + * Specifies the margin state of logo for a map view + * + * @param margins 4 long array for LTRB margins + * @return This + */ + @NonNull + public MapboxMapOptions logoMargins(int[] margins) { + logoMargins = margins; + return this; + } + + /** + * Specifies the visibility state of a attribution for a map view. + * + * @param enabled True and attribution is shown + * @return This + */ + @NonNull + public MapboxMapOptions attributionEnabled(boolean enabled) { + attributionEnabled = enabled; + return this; + } + + /** + * Specifies the gravity state of attribution for a map view. + * + * @param gravity Android SDK Gravity. + * @return This + */ + @NonNull + public MapboxMapOptions attributionGravity(int gravity) { + attributionGravity = gravity; + return this; + } + + /** + * Specifies the margin state of attribution for a map view + * + * @param margins 4 long array for LTRB margins + * @return This + */ + @NonNull + public MapboxMapOptions attributionMargins(int[] margins) { + attributionMargins = margins; + return this; + } + + /** + * Specifies the tint color of the attribution for a map view + * + * @param color integer resembling a color + * @return This + */ + @NonNull + public MapboxMapOptions attributionTintColor(@ColorInt int color) { + attributionTintColor = color; + return this; + } + + /** + * Specifies if the rotate gesture is enabled for a map view. + * + * @param enabled True and gesture will be enabled + * @return This + */ + @NonNull + public MapboxMapOptions rotateGesturesEnabled(boolean enabled) { + rotateGesturesEnabled = enabled; + return this; + } + + /** + * Specifies if the scroll gesture is enabled for a map view. + * + * @param enabled True and gesture will be enabled + * @return This + */ + @NonNull + public MapboxMapOptions scrollGesturesEnabled(boolean enabled) { + scrollGesturesEnabled = enabled; + return this; + } + + /** + * Specifies if the tilt gesture is enabled for a map view. + * + * @param enabled True and gesture will be enabled + * @return This + */ + @NonNull + public MapboxMapOptions tiltGesturesEnabled(boolean enabled) { + tiltGesturesEnabled = enabled; + return this; + } + + /** + * Specifies if the zoom gesture is enabled for a map view. + * + * @param enabled True and gesture will be enabled + * @return This + */ + @NonNull + public MapboxMapOptions zoomGesturesEnabled(boolean enabled) { + zoomGesturesEnabled = enabled; + return this; + } + + /** + * Specifies if the double tap gesture is enabled for a map view. + * + * @param enabled True and gesture will be enabled + * @return This + */ + @NonNull + public MapboxMapOptions doubleTapGesturesEnabled(boolean enabled) { + doubleTapGesturesEnabled = enabled; + return this; + } + + /** + * Specifies whether the user may zoom the map by tapping twice, holding and moving the pointer up and down. + * + * @param enabled True and gesture will be enabled + * @return This + */ + @NonNull + public MapboxMapOptions quickZoomGesturesEnabled(boolean enabled) { + quickZoomGesturesEnabled = enabled; + return this; + } + + /** + * Enable {@link android.view.TextureView} as rendered surface. + *

+ * Since the 5.2.0 release we replaced our TextureView with an {@link android.opengl.GLSurfaceView} + * implementation. Enabling this option will use the {@link android.view.TextureView} instead. + * {@link android.view.TextureView} can be useful in situations where you need to animate, scale + * or transform the view. This comes at a siginficant performance penalty and should not be considered + * unless absolutely needed. + *

+ * + * @param textureMode True to enable texture mode + * @return This + */ + @NonNull + public MapboxMapOptions textureMode(boolean textureMode) { + this.textureMode = textureMode; + return this; + } + + @NonNull + public MapboxMapOptions translucentTextureSurface(boolean translucentTextureSurface) { + this.translucentTextureSurface = translucentTextureSurface; + return this; + } + + /** + * Set the MapView foreground color that is used when the map surface is being created. + * + * @param loadColor the color to show during map creation + * @return This + */ + @NonNull + public MapboxMapOptions foregroundLoadColor(@ColorInt int loadColor) { + this.foregroundLoadColor = loadColor; + return this; + } + + /** + * Enable tile pre-fetching. Loads tiles at a lower zoom-level to pre-render + * a low resolution preview while more detailed tiles are loaded. + * Enabled by default + * + * @param enable true to enable + * @return This + */ + @NonNull + public MapboxMapOptions setPrefetchesTiles(boolean enable) { + this.prefetchesTiles = enable; + return this; + } + + /** + * Enable cross-source symbol collision detection, defaults to true. + *

+ * If set to false, symbol layers will only run collision detection against + * other symbol layers that are part of the same source. + *

+ * + * @param crossSourceCollisions true to enable, false to disable + * @return This + */ + @NonNull + public MapboxMapOptions crossSourceCollisions(boolean crossSourceCollisions) { + this.crossSourceCollisions = crossSourceCollisions; + return this; + } + + /** + * Enable local ideograph font family, defaults to true. + * + * @param enabled true to enable, false to disable + * @return This + */ + @NonNull + public MapboxMapOptions localIdeographFontFamilyEnabled(boolean enabled) { + this.localIdeographFontFamilyEnabled = enabled; + return this; + } + + /** + * Set the font family for generating glyphs locally for ideographs in the 'CJK Unified Ideographs' + * and 'Hangul Syllables' ranges. + *

+ * The font family argument is passed to {@link android.graphics.Typeface#create(String, int)}. + * Default system fonts are defined in '/system/etc/fonts.xml' + * Default font for local ideograph font family is {@link MapboxConstants#DEFAULT_FONT}. + * + * @param fontFamily font family for local ideograph generation. + * @return This + */ + @NonNull + public MapboxMapOptions localIdeographFontFamily(String fontFamily) { + this.localIdeographFontFamily = FontUtils.extractValidFont(fontFamily); + return this; + } + + /** + * Set a font family from range of font families for generating glyphs locally for ideographs in the + * 'CJK Unified Ideographs' and 'Hangul Syllables' ranges. The first matching font + * will be selected. If no valid font found, it defaults to {@link MapboxConstants#DEFAULT_FONT}. + *

+ * The font families are checked against the default system fonts defined in + * '/system/etc/fonts.xml' Default font for local ideograph font family is + * {@link MapboxConstants#DEFAULT_FONT}. + *

+ * + * @param fontFamilies an array of font families for local ideograph generation. + * @return This + */ + @NonNull + public MapboxMapOptions localIdeographFontFamily(String... fontFamilies) { + this.localIdeographFontFamily = FontUtils.extractValidFont(fontFamilies); + return this; + } + + /** + * Set the custom pixel ratio configuration to override the default value from resources. + * This ratio will be used to initialise the map with. + * + * @param pixelRatio the custom pixel ratio of the map under construction + * @return This + */ + @NonNull + public MapboxMapOptions pixelRatio(float pixelRatio) { + this.pixelRatio = pixelRatio; + return this; + } + + /** + * Check whether tile pre-fetching is enabled. + * + * @return true if enabled + */ + public boolean getPrefetchesTiles() { + return prefetchesTiles; + } + + /** + * Check whether cross-source symbol collision detection is enabled. + * + * @return true if enabled + */ + public boolean getCrossSourceCollisions() { + return crossSourceCollisions; + } + + /** + * Set the flag to render the map surface on top of another surface. + * + * @param renderOnTop true if this map is shown on top of another one, false if bottom. + */ + public void renderSurfaceOnTop(boolean renderOnTop) { + this.zMediaOverlay = renderOnTop; + } + + /** + * Get the flag to render the map surface on top of another surface. + * + * @return true if this map is + */ + public boolean getRenderSurfaceOnTop() { + return zMediaOverlay; + } + + /** + * Get the current configured API endpoint base URL. + * + * @return Base URL to be used API endpoint. + * @deprecated use {@link #getApiBaseUri()} instead + */ + @Deprecated + public String getApiBaseUrl() { + return apiBaseUri; + } + + /** + * Get the current configured API endpoint base URI. + * + * @return Base URI to be used API endpoint. + */ + public String getApiBaseUri() { + return apiBaseUri; + } + + /** + * Get the current configured initial camera position for a map view. + * + * @return CameraPosition to be initially used. + */ + public CameraPosition getCamera() { + return cameraPosition; + } + + /** + * Get the current configured min zoom for a map view. + * + * @return Mininum zoom level to be used. + */ + public double getMinZoomPreference() { + return minZoom; + } + + /** + * Get the current configured maximum zoom for a map view. + * + * @return Maximum zoom to be used. + */ + public double getMaxZoomPreference() { + return maxZoom; + } + + /** + * Get the current configured visibility state for mapbox_compass_icon for a map view. + * + * @return Visibility state of the mapbox_compass_icon + */ + public boolean getCompassEnabled() { + return compassEnabled; + } + + /** + * Get the current configured gravity state for mapbox_compass_icon for a map view. + * + * @return Gravity state of the mapbox_compass_icon + */ + public int getCompassGravity() { + return compassGravity; + } + + /** + * Get the current configured margins for mapbox_compass_icon for a map view. + * + * @return Margins state of the mapbox_compass_icon + */ + public int[] getCompassMargins() { + return compassMargins; + } + + /** + * Get the current configured state for fading the mapbox_compass_icon when facing north. + * + * @return True if mapbox_compass_icon fades to invisible when facing north + */ + public boolean getCompassFadeFacingNorth() { + return fadeCompassFacingNorth; + } + + /** + * Get the current configured CompassView image. + * + * @return the drawable used as compass image + */ + public Drawable getCompassImage() { + return compassImage; + } + + /** + * Get the current configured visibility state for mapbox_compass_icon for a map view. + * + * @return Visibility state of the mapbox_compass_icon + */ + public boolean getLogoEnabled() { + return logoEnabled; + } + + /** + * Get the current configured gravity state for logo for a map view. + * + * @return Gravity state of the logo + */ + public int getLogoGravity() { + return logoGravity; + } + + /** + * Get the current configured margins for logo for a map view. + * + * @return Margins state of the logo + */ + public int[] getLogoMargins() { + return logoMargins; + } + + /** + * Get the current configured rotate gesture state for a map view. + * + * @return True indicates gesture is enabled + */ + public boolean getRotateGesturesEnabled() { + return rotateGesturesEnabled; + } + + /** + * Get the current configured scroll gesture state for a map view. + * + * @return True indicates gesture is enabled + */ + public boolean getScrollGesturesEnabled() { + return scrollGesturesEnabled; + } + + /** + * Get the current configured tilt gesture state for a map view. + * + * @return True indicates gesture is enabled + */ + public boolean getTiltGesturesEnabled() { + return tiltGesturesEnabled; + } + + /** + * Get the current configured zoom gesture state for a map view. + * + * @return True indicates gesture is enabled + */ + public boolean getZoomGesturesEnabled() { + return zoomGesturesEnabled; + } + + /** + * Get the current configured double tap gesture state for a map view. + * + * @return True indicates gesture is enabled + */ + public boolean getDoubleTapGesturesEnabled() { + return doubleTapGesturesEnabled; + } + + /** + * Get whether the user may zoom the map by tapping twice, holding and moving the pointer up and down. + * + * @return True indicates gesture is enabled + */ + public boolean getQuickZoomGesturesEnabled() { + return quickZoomGesturesEnabled; + } + + /** + * Get the current configured visibility state for attribution for a map view. + * + * @return Visibility state of the attribution + */ + public boolean getAttributionEnabled() { + return attributionEnabled; + } + + /** + * Get the current configured gravity state for attribution for a map view. + * + * @return Gravity state of the logo + */ + public int getAttributionGravity() { + return attributionGravity; + } + + /** + * Get the current configured margins for attribution for a map view. + * + * @return Margins state of the logo + */ + public int[] getAttributionMargins() { + return attributionMargins; + } + + /** + * Get the current configured tint color for attribution for a map view. + * + * @return the tint color + */ + @ColorInt + public int getAttributionTintColor() { + return attributionTintColor; + } + + /** + * Get the current configured debug state for a map view. + * + * @return True indicates debug is enabled. + */ + public boolean getDebugActive() { + return debugActive; + } + + /** + * Returns true if TextureView is being used the render view. + * + * @return True if TextureView is used. + */ + public boolean getTextureMode() { + return textureMode; + } + + /** + * Returns true if TextureView supports a translucent surface + * + * @return True if translucent surface is active + */ + public boolean getTranslucentTextureSurface() { + return translucentTextureSurface; + } + + /** + * Returns the current configured foreground color that is used during map creation. + * + * @return the load color + */ + @ColorInt + public int getForegroundLoadColor() { + return foregroundLoadColor; + } + + /** + * Returns the font-family for locally overriding generation of glyphs in the + * 'CJK Unified Ideographs' and 'Hangul Syllables' ranges. + * Default font for local ideograph font family is {@link MapboxConstants#DEFAULT_FONT}. + * Returns null if local ideograph font families are disabled. + * + * @return Local ideograph font family name. + */ + @Nullable + public String getLocalIdeographFontFamily() { + return localIdeographFontFamilyEnabled ? localIdeographFontFamily : null; + } + + /** + * Returns true if local ideograph font family is enabled, defaults to true. + * + * @return True if local ideograph font family is enabled + */ + public boolean isLocalIdeographFontFamilyEnabled() { + return localIdeographFontFamilyEnabled; + } + + /** + * Return the custom configured pixel ratio, returns 0 if not configured. + * + * @return the pixel ratio used by the map under construction + */ + public float getPixelRatio() { + return pixelRatio; + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public MapboxMapOptions createFromParcel(@NonNull Parcel in) { + return new MapboxMapOptions(in); + } + + public MapboxMapOptions[] newArray(int size) { + return new MapboxMapOptions[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(cameraPosition, flags); + dest.writeByte((byte) (debugActive ? 1 : 0)); + + dest.writeByte((byte) (compassEnabled ? 1 : 0)); + dest.writeInt(compassGravity); + dest.writeIntArray(compassMargins); + dest.writeByte((byte) (fadeCompassFacingNorth ? 1 : 0)); + dest.writeParcelable(compassImage != null + ? BitmapUtils.getBitmapFromDrawable(compassImage) : null, flags); + + dest.writeByte((byte) (logoEnabled ? 1 : 0)); + dest.writeInt(logoGravity); + dest.writeIntArray(logoMargins); + + dest.writeByte((byte) (attributionEnabled ? 1 : 0)); + dest.writeInt(attributionGravity); + dest.writeIntArray(attributionMargins); + dest.writeInt(attributionTintColor); + + dest.writeDouble(minZoom); + dest.writeDouble(maxZoom); + + dest.writeByte((byte) (rotateGesturesEnabled ? 1 : 0)); + dest.writeByte((byte) (scrollGesturesEnabled ? 1 : 0)); + dest.writeByte((byte) (tiltGesturesEnabled ? 1 : 0)); + dest.writeByte((byte) (zoomGesturesEnabled ? 1 : 0)); + dest.writeByte((byte) (doubleTapGesturesEnabled ? 1 : 0)); + dest.writeByte((byte) (quickZoomGesturesEnabled ? 1 : 0)); + + dest.writeString(apiBaseUri); + dest.writeByte((byte) (textureMode ? 1 : 0)); + dest.writeByte((byte) (translucentTextureSurface ? 1 : 0)); + dest.writeByte((byte) (prefetchesTiles ? 1 : 0)); + dest.writeByte((byte) (zMediaOverlay ? 1 : 0)); + dest.writeByte((byte) (localIdeographFontFamilyEnabled ? 1 : 0)); + dest.writeString(localIdeographFontFamily); + dest.writeStringArray(localIdeographFontFamilies); + dest.writeFloat(pixelRatio); + dest.writeInt(foregroundLoadColor); + dest.writeByte((byte) (crossSourceCollisions ? 1 : 0)); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + MapboxMapOptions options = (MapboxMapOptions) o; + + if (debugActive != options.debugActive) { + return false; + } + if (compassEnabled != options.compassEnabled) { + return false; + } + if (fadeCompassFacingNorth != options.fadeCompassFacingNorth) { + return false; + } + if (compassImage != null + ? !compassImage.equals(options.compassImage) + : options.compassImage != null) { + return false; + } + if (compassGravity != options.compassGravity) { + return false; + } + if (logoEnabled != options.logoEnabled) { + return false; + } + if (logoGravity != options.logoGravity) { + return false; + } + if (attributionTintColor != options.attributionTintColor) { + return false; + } + if (attributionEnabled != options.attributionEnabled) { + return false; + } + if (attributionGravity != options.attributionGravity) { + return false; + } + if (Double.compare(options.minZoom, minZoom) != 0) { + return false; + } + if (Double.compare(options.maxZoom, maxZoom) != 0) { + return false; + } + if (rotateGesturesEnabled != options.rotateGesturesEnabled) { + return false; + } + if (scrollGesturesEnabled != options.scrollGesturesEnabled) { + return false; + } + if (tiltGesturesEnabled != options.tiltGesturesEnabled) { + return false; + } + if (zoomGesturesEnabled != options.zoomGesturesEnabled) { + return false; + } + if (doubleTapGesturesEnabled != options.doubleTapGesturesEnabled) { + return false; + } + if (quickZoomGesturesEnabled != options.quickZoomGesturesEnabled) { + return false; + } + if (cameraPosition != null ? !cameraPosition.equals(options.cameraPosition) : options.cameraPosition != null) { + return false; + } + if (!Arrays.equals(compassMargins, options.compassMargins)) { + return false; + } + if (!Arrays.equals(logoMargins, options.logoMargins)) { + return false; + } + if (!Arrays.equals(attributionMargins, options.attributionMargins)) { + return false; + } + if (apiBaseUri != null ? !apiBaseUri.equals(options.apiBaseUri) : options.apiBaseUri != null) { + return false; + } + if (prefetchesTiles != options.prefetchesTiles) { + return false; + } + if (zMediaOverlay != options.zMediaOverlay) { + return false; + } + if (localIdeographFontFamilyEnabled != options.localIdeographFontFamilyEnabled) { + return false; + } + if (!localIdeographFontFamily.equals(options.localIdeographFontFamily)) { + return false; + } + if (!Arrays.equals(localIdeographFontFamilies, options.localIdeographFontFamilies)) { + return false; + } + + if (pixelRatio != options.pixelRatio) { + return false; + } + + if (crossSourceCollisions != options.crossSourceCollisions) { + return false; + } + + return false; + } + + @Override + public int hashCode() { + int result; + long temp; + result = cameraPosition != null ? cameraPosition.hashCode() : 0; + result = 31 * result + (debugActive ? 1 : 0); + result = 31 * result + (compassEnabled ? 1 : 0); + result = 31 * result + (fadeCompassFacingNorth ? 1 : 0); + result = 31 * result + compassGravity; + result = 31 * result + (compassImage != null ? compassImage.hashCode() : 0); + result = 31 * result + Arrays.hashCode(compassMargins); + result = 31 * result + (logoEnabled ? 1 : 0); + result = 31 * result + logoGravity; + result = 31 * result + Arrays.hashCode(logoMargins); + result = 31 * result + attributionTintColor; + result = 31 * result + (attributionEnabled ? 1 : 0); + result = 31 * result + attributionGravity; + result = 31 * result + Arrays.hashCode(attributionMargins); + temp = Double.doubleToLongBits(minZoom); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(maxZoom); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + (rotateGesturesEnabled ? 1 : 0); + result = 31 * result + (scrollGesturesEnabled ? 1 : 0); + result = 31 * result + (tiltGesturesEnabled ? 1 : 0); + result = 31 * result + (zoomGesturesEnabled ? 1 : 0); + result = 31 * result + (doubleTapGesturesEnabled ? 1 : 0); + result = 31 * result + (quickZoomGesturesEnabled ? 1 : 0); + result = 31 * result + (apiBaseUri != null ? apiBaseUri.hashCode() : 0); + result = 31 * result + (textureMode ? 1 : 0); + result = 31 * result + (translucentTextureSurface ? 1 : 0); + result = 31 * result + (prefetchesTiles ? 1 : 0); + result = 31 * result + (zMediaOverlay ? 1 : 0); + result = 31 * result + (localIdeographFontFamilyEnabled ? 1 : 0); + result = 31 * result + (localIdeographFontFamily != null ? localIdeographFontFamily.hashCode() : 0); + result = 31 * result + Arrays.hashCode(localIdeographFontFamilies); + result = 31 * result + (int) pixelRatio; + result = 31 * result + (crossSourceCollisions ? 1 : 0); + return result; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MarkerContainer.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MarkerContainer.java new file mode 100644 index 000000000..b408d9f5c --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MarkerContainer.java @@ -0,0 +1,146 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.support.v4.util.LongSparseArray; + +import com.mapbox.mapboxsdk.annotations.Annotation; +import com.mapbox.mapboxsdk.annotations.BaseMarkerOptions; +import com.mapbox.mapboxsdk.annotations.Icon; +import com.mapbox.mapboxsdk.annotations.Marker; + +import java.util.ArrayList; +import java.util.List; + +/** + * Encapsulates {@link Marker}'s functionality. + */ +class MarkerContainer implements Markers { + + private final NativeMap nativeMapView; + private final LongSparseArray annotations; + private final IconManager iconManager; + + MarkerContainer(NativeMap nativeMapView, LongSparseArray annotations, IconManager iconManager) { + this.nativeMapView = nativeMapView; + this.annotations = annotations; + this.iconManager = iconManager; + } + + @Override + public Marker addBy(@NonNull BaseMarkerOptions markerOptions, @NonNull MapboxMap mapboxMap) { + Marker marker = prepareMarker(markerOptions); + long id = nativeMapView != null ? nativeMapView.addMarker(marker) : 0; + marker.setMapboxMap(mapboxMap); + marker.setId(id); + annotations.put(id, marker); + return marker; + } + + @NonNull + @Override + public List addBy(@NonNull List markerOptionsList, @NonNull MapboxMap + mapboxMap) { + int count = markerOptionsList.size(); + List markers = new ArrayList<>(count); + if (nativeMapView != null && count > 0) { + BaseMarkerOptions markerOptions; + Marker marker; + for (int i = 0; i < count; i++) { + markerOptions = markerOptionsList.get(i); + marker = prepareMarker(markerOptions); + markers.add(marker); + } + + if (markers.size() > 0) { + long[] ids = nativeMapView.addMarkers(markers); + for (int i = 0; i < ids.length; i++) { + Marker createdMarker = markers.get(i); + createdMarker.setMapboxMap(mapboxMap); + createdMarker.setId(ids[i]); + annotations.put(ids[i], createdMarker); + } + } + } + return markers; + } + + @Override + public void update(@NonNull Marker updatedMarker, @NonNull MapboxMap mapboxMap) { + ensureIconLoaded(updatedMarker, mapboxMap); + nativeMapView.updateMarker(updatedMarker); + annotations.setValueAt(annotations.indexOfKey(updatedMarker.getId()), updatedMarker); + } + + @NonNull + @Override + public List obtainAll() { + List markers = new ArrayList<>(); + Annotation annotation; + for (int i = 0; i < annotations.size(); i++) { + annotation = annotations.get(annotations.keyAt(i)); + if (annotation instanceof Marker) { + markers.add((Marker) annotation); + } + } + return markers; + } + + @NonNull + @Override + public List obtainAllIn(@NonNull RectF rectangle) { + RectF rect = nativeMapView.getDensityDependantRectangle(rectangle); + long[] ids = nativeMapView.queryPointAnnotations(rect); + List idsList = new ArrayList<>(ids.length); + for (long id : ids) { + idsList.add(id); + } + + List annotations = new ArrayList<>(ids.length); + List annotationList = obtainAnnotations(); + int count = annotationList.size(); + for (int i = 0; i < count; i++) { + Annotation annotation = annotationList.get(i); + if (annotation instanceof com.mapbox.mapboxsdk.annotations.Marker && idsList.contains(annotation.getId())) { + annotations.add((com.mapbox.mapboxsdk.annotations.Marker) annotation); + } + } + + return new ArrayList<>(annotations); + } + + @Override + public void reload() { + iconManager.reloadIcons(); + int count = annotations.size(); + for (int i = 0; i < count; i++) { + Annotation annotation = annotations.get(i); + if (annotation instanceof Marker) { + Marker marker = (Marker) annotation; + nativeMapView.removeAnnotation(annotation.getId()); + long newId = nativeMapView.addMarker(marker); + marker.setId(newId); + } + } + } + + private Marker prepareMarker(BaseMarkerOptions markerOptions) { + Marker marker = markerOptions.getMarker(); + Icon icon = iconManager.loadIconForMarker(marker); + marker.setTopOffsetPixels(iconManager.getTopOffsetPixelsForIcon(icon)); + return marker; + } + + private void ensureIconLoaded(Marker marker, @NonNull MapboxMap mapboxMap) { + iconManager.ensureIconLoaded(marker, mapboxMap); + } + + @NonNull + private List obtainAnnotations() { + List annotations = new ArrayList<>(); + for (int i = 0; i < this.annotations.size(); i++) { + annotations.add(this.annotations.get(this.annotations.keyAt(i))); + } + return annotations; + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Markers.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Markers.java new file mode 100644 index 000000000..62859d58a --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Markers.java @@ -0,0 +1,27 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.RectF; +import android.support.annotation.NonNull; + +import com.mapbox.mapboxsdk.annotations.BaseMarkerOptions; +import com.mapbox.mapboxsdk.annotations.Marker; + +import java.util.List; + +/** + * Interface that defines convenient methods for working with a {@link Marker}'s collection. + */ +interface Markers { + Marker addBy(@NonNull BaseMarkerOptions markerOptions, @NonNull MapboxMap mapboxMap); + + List addBy(@NonNull List markerOptionsList, @NonNull MapboxMap mapboxMap); + + void update(@NonNull Marker updatedMarker, @NonNull MapboxMap mapboxMap); + + List obtainAll(); + + @NonNull + List obtainAllIn(@NonNull RectF rectangle); + + void reload(); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMap.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMap.java new file mode 100644 index 000000000..7f3017c7a --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMap.java @@ -0,0 +1,268 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.Bitmap; +import android.graphics.PointF; +import android.graphics.RectF; +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import com.mapbox.geojson.Feature; +import com.mapbox.geojson.Geometry; +import com.mapbox.mapboxsdk.annotations.Marker; +import com.mapbox.mapboxsdk.annotations.Polygon; +import com.mapbox.mapboxsdk.annotations.Polyline; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.mapboxsdk.geometry.ProjectedMeters; +import com.mapbox.mapboxsdk.style.expressions.Expression; +import com.mapbox.mapboxsdk.style.layers.Layer; +import com.mapbox.mapboxsdk.style.layers.TransitionOptions; +import com.mapbox.mapboxsdk.style.light.Light; +import com.mapbox.mapboxsdk.style.sources.Source; + +import java.util.List; + +interface NativeMap { + + // + // Lifecycle API + // + + void resizeView(int width, int height); + + void onLowMemory(); + + void destroy(); + + boolean isDestroyed(); + + // + // Camera API + // + + void jumpTo(@NonNull LatLng center, double zoom, double pitch, double bearing, double[] padding); + + void easeTo(@NonNull LatLng center, double zoom, double bearing, double pitch, double[] padding, long duration, + boolean easingInterpolator); + + void flyTo(@NonNull LatLng center, double zoom, double bearing, double pitch, double[] padding, long duration); + + void moveBy(double deltaX, double deltaY, long duration); + + @NonNull + CameraPosition getCameraPosition(); + + CameraPosition getCameraForLatLngBounds(@NonNull LatLngBounds bounds, int[] padding, double bearing, double pitch); + + CameraPosition getCameraForGeometry(@NonNull Geometry geometry, int[] padding, double bearing, double pitch); + + void resetPosition(); + + void setLatLng(@NonNull LatLng latLng, long duration); + + LatLng getLatLng(); + + void setLatLngBounds(@Nullable LatLngBounds latLngBounds); + + void setVisibleCoordinateBounds(@NonNull LatLng[] coordinates, @NonNull RectF padding, + double direction, long duration); + + void setPitch(double pitch, long duration); + + double getPitch(); + + void setZoom(double zoom, @NonNull PointF focalPoint, long duration); + + double getZoom(); + + void setMinZoom(double zoom); + + double getMinZoom(); + + void setMaxZoom(double zoom); + + double getMaxZoom(); + + void resetZoom(); + + void rotateBy(double sx, double sy, double ex, double ey, long duration); + + void setBearing(double degrees, long duration); + + void setBearing(double degrees, double fx, double fy, long duration); + + double getBearing(); + + void resetNorth(); + + void cancelTransitions(); + + // + // Style API + // + + void setStyleUri(String url); + + @NonNull + String getStyleUri(); + + void setStyleJson(String newStyleJson); + + @NonNull + String getStyleJson(); + + boolean isFullyLoaded(); + + void addLayer(@NonNull Layer layer); + + void addLayerBelow(@NonNull Layer layer, @NonNull String below); + + void addLayerAbove(@NonNull Layer layer, @NonNull String above); + + void addLayerAt(@NonNull Layer layer, @IntRange(from = 0) int index); + + @NonNull + List getLayers(); + + Layer getLayer(String layerId); + + boolean removeLayer(@NonNull String layerId); + + boolean removeLayer(@NonNull Layer layer); + + boolean removeLayerAt(@IntRange(from = 0) int index); + + void addSource(@NonNull Source source); + + @NonNull + List getSources(); + + Source getSource(@NonNull String sourceId); + + boolean removeSource(@NonNull String sourceId); + + boolean removeSource(@NonNull Source source); + + void setTransitionOptions(@NonNull TransitionOptions transitionOptions); + + @NonNull + TransitionOptions getTransitionOptions(); + + void addImages(Image[] images); + + Bitmap getImage(String name); + + void removeImage(String name); + + Light getLight(); + + // + // Content padding API + // + + void setContentPadding(double[] padding); + + double[] getContentPadding(); + + // + // Query API + // + + @NonNull + List queryRenderedFeatures(@NonNull PointF coordinates, + @Nullable String[] layerIds, + @Nullable Expression filter); + + @NonNull + List queryRenderedFeatures(@NonNull RectF coordinates, + @Nullable String[] layerIds, + @Nullable Expression filter); + + // + // Projection API + // + + double getMetersPerPixelAtLatitude(double lat); + + ProjectedMeters projectedMetersForLatLng(@NonNull LatLng latLng); + + LatLng latLngForProjectedMeters(@NonNull ProjectedMeters projectedMeters); + + @NonNull + PointF pixelForLatLng(@NonNull LatLng latLng); + + LatLng latLngForPixel(@NonNull PointF pixel); + + // + // Utils API + // + + void setOnFpsChangedListener(@NonNull MapboxMap.OnFpsChangedListener listener); + + void setDebug(boolean debug); + + boolean getDebug(); + + void cycleDebugOptions(); + + void setReachability(boolean status); + + void setApiBaseUrl(String baseUrl); + + void setPrefetchTiles(boolean enable); + + boolean getPrefetchTiles(); + + void setGestureInProgress(boolean inProgress); + + float getPixelRatio(); + + // + // Deprecated Annotations API + // + + long addMarker(Marker marker); + + @NonNull + long[] addMarkers(@NonNull List markers); + + long addPolyline(Polyline polyline); + + @NonNull + long[] addPolylines(@NonNull List polylines); + + long addPolygon(Polygon polygon); + + @NonNull + long[] addPolygons(@NonNull List polygons); + + void updateMarker(@NonNull Marker marker); + + void updatePolygon(@NonNull Polygon polygon); + + void updatePolyline(@NonNull Polyline polyline); + + void removeAnnotation(long id); + + void removeAnnotations(long[] ids); + + double getTopOffsetPixelsForAnnotationSymbol(String symbolName); + + void addAnnotationIcon(String symbol, int width, int height, float scale, byte[] pixels); + + void removeAnnotationIcon(String symbol); + + @NonNull + long[] queryPointAnnotations(RectF rectF); + + @NonNull + long[] queryShapeAnnotations(RectF rectF); + + @NonNull + RectF getDensityDependantRectangle(RectF rectangle); + + long getNativePtr(); + + void addSnapshotCallback(@NonNull MapboxMap.SnapshotReadyCallback callback); +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java new file mode 100755 index 000000000..8496160c7 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java @@ -0,0 +1,1489 @@ +package com.mapbox.mapboxsdk.maps; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.PointF; +import android.graphics.RectF; +import android.os.Handler; +import android.support.annotation.IntRange; +import android.support.annotation.Keep; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.mapbox.geojson.Feature; +import com.mapbox.geojson.Geometry; +import com.mapbox.mapboxsdk.LibraryLoader; +import com.mapbox.mapboxsdk.MapStrictMode; +import com.mapbox.mapboxsdk.annotations.Icon; +import com.mapbox.mapboxsdk.annotations.Marker; +import com.mapbox.mapboxsdk.annotations.Polygon; +import com.mapbox.mapboxsdk.annotations.Polyline; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.exceptions.CalledFromWorkerThreadException; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.mapboxsdk.geometry.ProjectedMeters; +import com.mapbox.mapboxsdk.log.Logger; +import com.mapbox.mapboxsdk.maps.renderer.MapRenderer; +import com.mapbox.mapboxsdk.storage.FileSource; +import com.mapbox.mapboxsdk.style.expressions.Expression; +import com.mapbox.mapboxsdk.style.layers.CannotAddLayerException; +import com.mapbox.mapboxsdk.style.layers.Layer; +import com.mapbox.mapboxsdk.style.layers.TransitionOptions; +import com.mapbox.mapboxsdk.style.light.Light; +import com.mapbox.mapboxsdk.style.sources.CannotAddSourceException; +import com.mapbox.mapboxsdk.style.sources.Source; +import com.mapbox.mapboxsdk.utils.BitmapUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +// Class that wraps the native methods for convenience +final class NativeMapView implements NativeMap { + + private static final String TAG = "Mbgl-NativeMapView"; + + // Hold a reference to prevent it from being GC'd as long as it's used on the native side + private final FileSource fileSource; + + // Used to schedule work on the MapRenderer Thread + private final MapRenderer mapRenderer; + + // Used to validate if methods are called from the correct thread + @NonNull + private final Thread thread; + + // Used for view callbacks + @Nullable + private ViewCallback viewCallback; + + // Used for map change callbacks + @Nullable + private StateCallback stateCallback; + + // Device density + private final float pixelRatio; + + // Flag to indicate destroy was called + private boolean destroyed = false; + + // Cached to enable lazily set padding. + // Whenever an animation is schedule, this value is cleared and the source of truth becomes the core transform state. + private double[] edgeInsets; + + // Holds the pointer to JNI NativeMapView + @Keep + private long nativePtr = 0; + + // Listener invoked to return a bitmap of the map + private MapboxMap.SnapshotReadyCallback snapshotReadyCallback; + + static { + LibraryLoader.load(); + } + + // + // Constructors + // + + public NativeMapView(@NonNull final Context context, final boolean crossSourceCollisions, + final ViewCallback viewCallback, final StateCallback stateCallback, + final MapRenderer mapRenderer) { + this(context, context.getResources().getDisplayMetrics().density, crossSourceCollisions, viewCallback, + stateCallback, mapRenderer); + } + + public NativeMapView(final Context context, final float pixelRatio, final boolean crossSourceCollisions, + final ViewCallback viewCallback, final StateCallback stateCallback, + final MapRenderer mapRenderer) { + this.mapRenderer = mapRenderer; + this.viewCallback = viewCallback; + this.fileSource = FileSource.getInstance(context); + this.pixelRatio = pixelRatio; + this.thread = Thread.currentThread(); + this.stateCallback = stateCallback; + nativeInitialize(this, fileSource, mapRenderer, pixelRatio, crossSourceCollisions); + } + + // + // Methods + // + + private boolean checkState(String callingMethod) { + // validate if invocation has occurred on the main thread + if (thread != Thread.currentThread()) { + throw new CalledFromWorkerThreadException( + String.format( + "Map interactions should happen on the UI thread. Method invoked from wrong thread is %s.", + callingMethod) + ); + } + + // validate if map has already been destroyed + if (destroyed && !TextUtils.isEmpty(callingMethod)) { + String message = String.format( + "You're calling `%s` after the `MapView` was destroyed, were you invoking it after `onDestroy()`?", + callingMethod); + Logger.e(TAG, message); + + MapStrictMode.strictModeViolation(message); + } + return destroyed; + } + + @Override + public void destroy() { + destroyed = true; + viewCallback = null; + nativeDestroy(); + } + + @Override + public void resizeView(int width, int height) { + if (checkState("resizeView")) { + return; + } + width = (int) Math.ceil(width / pixelRatio); + height = (int) Math.ceil(height / pixelRatio); + + if (width < 0) { + throw new IllegalArgumentException("width cannot be negative."); + } + + if (height < 0) { + throw new IllegalArgumentException("height cannot be negative."); + } + + if (width > 65535) { + // we have seen edge cases where devices return incorrect values #6111 + Logger.e(TAG, String.format("Device returned an out of range width size, " + + "capping value at 65535 instead of %s", width) + ); + width = 65535; + } + + if (height > 65535) { + // we have seen edge cases where devices return incorrect values #6111 + Logger.e(TAG, String.format("Device returned an out of range height size, " + + "capping value at 65535 instead of %s", height) + ); + height = 65535; + } + + nativeResizeView(width, height); + } + + @Override + public void setStyleUri(String url) { + if (checkState("setStyleUri")) { + return; + } + nativeSetStyleUrl(url); + } + + @Override + @NonNull + public String getStyleUri() { + if (checkState("getStyleUri")) { + return ""; + } + return nativeGetStyleUrl(); + } + + @Override + public void setStyleJson(String newStyleJson) { + if (checkState("setStyleJson")) { + return; + } + nativeSetStyleJson(newStyleJson); + } + + @Override + @NonNull + public String getStyleJson() { + if (checkState("getStyleJson")) { + return ""; + } + return nativeGetStyleJson(); + } + + @Override + public void setLatLngBounds(LatLngBounds latLngBounds) { + if (checkState("setLatLngBounds")) { + return; + } + nativeSetLatLngBounds(latLngBounds); + } + + @Override + public void cancelTransitions() { + if (checkState("cancelTransitions")) { + return; + } + nativeCancelTransitions(); + } + + @Override + public void setGestureInProgress(boolean inProgress) { + if (checkState("setGestureInProgress")) { + return; + } + nativeSetGestureInProgress(inProgress); + } + + @Override + public void moveBy(double dx, double dy, long duration) { + if (checkState("moveBy")) { + return; + } + nativeMoveBy(dx / pixelRatio, dy / pixelRatio, duration); + } + + @Override + public void setLatLng(@NonNull LatLng latLng, long duration) { + if (checkState("setLatLng")) { + return; + } + nativeSetLatLng(latLng.getLatitude(), latLng.getLongitude(), + getAnimationPaddingAndClearCachedInsets(null), duration); + } + + @Override + public LatLng getLatLng() { + if (checkState("")) { + return new LatLng(); + } + return nativeGetLatLng(); + } + + @Override + public CameraPosition getCameraForLatLngBounds(LatLngBounds bounds, int[] padding, double bearing, double tilt) { + if (checkState("getCameraForLatLngBounds")) { + return null; + } + return nativeGetCameraForLatLngBounds( + bounds, + padding[1] / pixelRatio, + padding[0] / pixelRatio, + padding[3] / pixelRatio, + padding[2] / pixelRatio, + bearing, + tilt + ); + } + + @Override + public CameraPosition getCameraForGeometry(Geometry geometry, int[] padding, double bearing, double tilt) { + if (checkState("getCameraForGeometry")) { + return null; + } + return nativeGetCameraForGeometry( + geometry, + padding[1] / pixelRatio, + padding[0] / pixelRatio, + padding[3] / pixelRatio, + padding[2] / pixelRatio, + bearing, + tilt + ); + } + + @Override + public void resetPosition() { + if (checkState("resetPosition")) { + return; + } + nativeResetPosition(); + } + + @Override + public double getPitch() { + if (checkState("getPitch")) { + return 0; + } + return nativeGetPitch(); + } + + @Override + public void setPitch(double pitch, long duration) { + if (checkState("setPitch")) { + return; + } + nativeSetPitch(pitch, duration); + } + + @Override + public void setZoom(double zoom, @NonNull PointF focalPoint, long duration) { + if (checkState("setZoom")) { + return; + } + nativeSetZoom(zoom, focalPoint.x / pixelRatio, focalPoint.y / pixelRatio, duration); + } + + @Override + public double getZoom() { + if (checkState("getZoom")) { + return 0; + } + return nativeGetZoom(); + } + + @Override + public void resetZoom() { + if (checkState("resetZoom")) { + return; + } + nativeResetZoom(); + } + + @Override + public void setMinZoom(double zoom) { + if (checkState("setMinZoom")) { + return; + } + nativeSetMinZoom(zoom); + } + + @Override + public double getMinZoom() { + if (checkState("getMinZoom")) { + return 0; + } + return nativeGetMinZoom(); + } + + @Override + public void setMaxZoom(double zoom) { + if (checkState("setMaxZoom")) { + return; + } + nativeSetMaxZoom(zoom); + } + + @Override + public double getMaxZoom() { + if (checkState("getMaxZoom")) { + return 0; + } + return nativeGetMaxZoom(); + } + + @Override + public void rotateBy(double sx, double sy, double ex, double ey, + long duration) { + if (checkState("rotateBy")) { + return; + } + nativeRotateBy(sx / pixelRatio, sy / pixelRatio, ex, ey, duration); + } + + @Override + public void setContentPadding(double[] padding) { + if (checkState("setContentPadding")) { + return; + } + this.edgeInsets = padding; + } + + @Override + public double[] getContentPadding() { + if (checkState("getContentPadding")) { + return new double[] {0, 0, 0, 0}; + } + // if cached insets are not applied yet, return them, otherwise, get the padding from the camera + return edgeInsets != null ? edgeInsets : getCameraPosition().padding; + } + + @Override + public void setBearing(double degrees, long duration) { + if (checkState("setBearing")) { + return; + } + nativeSetBearing(degrees, duration); + } + + @Override + public void setBearing(double degrees, double fx, double fy, long duration) { + if (checkState("setBearing")) { + return; + } + nativeSetBearingXY(degrees, fx / pixelRatio, fy / pixelRatio, duration); + } + + @Override + public double getBearing() { + if (checkState("getBearing")) { + return 0; + } + return nativeGetBearing(); + } + + @Override + public void resetNorth() { + if (checkState("resetNorth")) { + return; + } + nativeResetNorth(); + } + + @Override + public long addMarker(Marker marker) { + if (checkState("addMarker")) { + return 0; + } + Marker[] markers = {marker}; + return nativeAddMarkers(markers)[0]; + } + + @Override + @NonNull + public long[] addMarkers(@NonNull List markers) { + if (checkState("addMarkers")) { + return new long[] {}; + } + return nativeAddMarkers(markers.toArray(new Marker[markers.size()])); + } + + @Override + public long addPolyline(Polyline polyline) { + if (checkState("addPolyline")) { + return 0; + } + Polyline[] polylines = {polyline}; + return nativeAddPolylines(polylines)[0]; + } + + @Override + @NonNull + public long[] addPolylines(@NonNull List polylines) { + if (checkState("addPolylines")) { + return new long[] {}; + } + return nativeAddPolylines(polylines.toArray(new Polyline[polylines.size()])); + } + + @Override + public long addPolygon(Polygon polygon) { + if (checkState("addPolygon")) { + return 0; + } + Polygon[] polygons = {polygon}; + return nativeAddPolygons(polygons)[0]; + } + + @Override + @NonNull + public long[] addPolygons(@NonNull List polygons) { + if (checkState("addPolygons")) { + return new long[] {}; + } + return nativeAddPolygons(polygons.toArray(new Polygon[polygons.size()])); + } + + @Override + public void updateMarker(@NonNull Marker marker) { + if (checkState("updateMarker")) { + return; + } + LatLng position = marker.getPosition(); + Icon icon = marker.getIcon(); + nativeUpdateMarker(marker.getId(), position.getLatitude(), position.getLongitude(), icon.getId()); + } + + @Override + public void updatePolygon(@NonNull Polygon polygon) { + if (checkState("updatePolygon")) { + return; + } + nativeUpdatePolygon(polygon.getId(), polygon); + } + + @Override + public void updatePolyline(@NonNull Polyline polyline) { + if (checkState("updatePolyline")) { + return; + } + nativeUpdatePolyline(polyline.getId(), polyline); + } + + @Override + public void removeAnnotation(long id) { + if (checkState("removeAnnotation")) { + return; + } + long[] ids = {id}; + removeAnnotations(ids); + } + + @Override + public void removeAnnotations(long[] ids) { + if (checkState("removeAnnotations")) { + return; + } + nativeRemoveAnnotations(ids); + } + + @Override + @NonNull + public long[] queryPointAnnotations(RectF rect) { + if (checkState("queryPointAnnotations")) { + return new long[] {}; + } + return nativeQueryPointAnnotations(rect); + } + + @Override + @NonNull + public long[] queryShapeAnnotations(RectF rectF) { + if (checkState("queryShapeAnnotations")) { + return new long[] {}; + } + return nativeQueryShapeAnnotations(rectF); + } + + @Override + public void addAnnotationIcon(String symbol, int width, int height, float scale, byte[] pixels) { + if (checkState("addAnnotationIcon")) { + return; + } + nativeAddAnnotationIcon(symbol, width, height, scale, pixels); + } + + @Override + public void removeAnnotationIcon(String symbol) { + if (checkState("removeAnnotationIcon")) { + return; + } + nativeRemoveAnnotationIcon(symbol); + } + + @Override + public void setVisibleCoordinateBounds(LatLng[] coordinates, RectF padding, double direction, long duration) { + if (checkState("setVisibleCoordinateBounds")) { + return; + } + nativeSetVisibleCoordinateBounds(coordinates, padding, direction, duration); + } + + @Override + public void onLowMemory() { + if (checkState("onLowMemory")) { + return; + } + nativeOnLowMemory(); + } + + @Override + public void setDebug(boolean debug) { + if (checkState("setDebug")) { + return; + } + nativeSetDebug(debug); + } + + @Override + public void cycleDebugOptions() { + if (checkState("cycleDebugOptions")) { + return; + } + nativeCycleDebugOptions(); + } + + @Override + public boolean getDebug() { + if (checkState("getDebug")) { + return false; + } + return nativeGetDebug(); + } + + @Override + public boolean isFullyLoaded() { + if (checkState("isFullyLoaded")) { + return false; + } + return nativeIsFullyLoaded(); + } + + @Override + public void setReachability(boolean status) { + if (checkState("setReachability")) { + return; + } + nativeSetReachability(status); + } + + @Override + public double getMetersPerPixelAtLatitude(double lat) { + if (checkState("getMetersPerPixelAtLatitude")) { + return 0; + } + return nativeGetMetersPerPixelAtLatitude(lat, getZoom()) / pixelRatio; + } + + @Override + public ProjectedMeters projectedMetersForLatLng(@NonNull LatLng latLng) { + if (checkState("projectedMetersForLatLng")) { + return null; + } + return nativeProjectedMetersForLatLng(latLng.getLatitude(), latLng.getLongitude()); + } + + @Override + public LatLng latLngForProjectedMeters(@NonNull ProjectedMeters projectedMeters) { + if (checkState("latLngForProjectedMeters")) { + return new LatLng(); + } + return nativeLatLngForProjectedMeters(projectedMeters.getNorthing(), + projectedMeters.getEasting()); + } + + @Override + @NonNull + public PointF pixelForLatLng(@NonNull LatLng latLng) { + if (checkState("pixelForLatLng")) { + return new PointF(); + } + PointF pointF = nativePixelForLatLng(latLng.getLatitude(), latLng.getLongitude()); + pointF.set(pointF.x * pixelRatio, pointF.y * pixelRatio); + return pointF; + } + + @Override + public LatLng latLngForPixel(@NonNull PointF pixel) { + if (checkState("latLngForPixel")) { + return new LatLng(); + } + return nativeLatLngForPixel(pixel.x / pixelRatio, pixel.y / pixelRatio); + } + + @Override + public double getTopOffsetPixelsForAnnotationSymbol(String symbolName) { + if (checkState("getTopOffsetPixelsForAnnotationSymbol")) { + return 0; + } + return nativeGetTopOffsetPixelsForAnnotationSymbol(symbolName); + } + + @Override + public void jumpTo(@NonNull LatLng center, double zoom, double pitch, double angle, double[] padding) { + if (checkState("jumpTo")) { + return; + } + nativeJumpTo(angle, center.getLatitude(), center.getLongitude(), pitch, zoom, + getAnimationPaddingAndClearCachedInsets(padding)); + } + + @Override + public void easeTo(@NonNull LatLng center, double zoom, double angle, double pitch, double[] padding, long duration, + boolean easingInterpolator) { + if (checkState("easeTo")) { + return; + } + nativeEaseTo(angle, center.getLatitude(), center.getLongitude(), duration, pitch, zoom, + getAnimationPaddingAndClearCachedInsets(padding), easingInterpolator); + } + + @Override + public void flyTo(@NonNull LatLng center, double zoom, double angle, double pitch, double[] padding, long duration) { + if (checkState("flyTo")) { + return; + } + nativeFlyTo(angle, center.getLatitude(), center.getLongitude(), duration, pitch, zoom, + getAnimationPaddingAndClearCachedInsets(padding)); + } + + @Override + @NonNull + public CameraPosition getCameraPosition() { + if (checkState("getCameraValues")) { + return new CameraPosition.Builder().build(); + } + if (edgeInsets != null) { + return new CameraPosition.Builder(nativeGetCameraPosition()).padding(edgeInsets).build(); + } else { + return nativeGetCameraPosition(); + } + } + + @Override + public void setPrefetchTiles(boolean enable) { + if (checkState("setPrefetchTiles")) { + return; + } + nativeSetPrefetchTiles(enable); + } + + @Override + public boolean getPrefetchTiles() { + if (checkState("getPrefetchTiles")) { + return false; + } + return nativeGetPrefetchTiles(); + } + + // Runtime style Api + + @Override + public void setTransitionOptions(@NonNull TransitionOptions transitionOptions) { + nativeSetTransitionOptions(transitionOptions); + } + + @NonNull + @Override + public TransitionOptions getTransitionOptions() { + return nativeGetTransitionOptions(); + } + + @Override + @NonNull + public List getLayers() { + if (checkState("getLayers")) { + return new ArrayList<>(); + } + return Arrays.asList(nativeGetLayers()); + } + + @Override + public Layer getLayer(String layerId) { + if (checkState("getLayer")) { + return null; + } + return nativeGetLayer(layerId); + } + + @Override + public void addLayer(@NonNull Layer layer) { + if (checkState("addLayer")) { + return; + } + nativeAddLayer(layer.getNativePtr(), null); + } + + @Override + public void addLayerBelow(@NonNull Layer layer, @NonNull String below) { + if (checkState("addLayerBelow")) { + return; + } + nativeAddLayer(layer.getNativePtr(), below); + } + + @Override + public void addLayerAbove(@NonNull Layer layer, @NonNull String above) { + if (checkState("addLayerAbove")) { + return; + } + nativeAddLayerAbove(layer.getNativePtr(), above); + } + + @Override + public void addLayerAt(@NonNull Layer layer, @IntRange(from = 0) int index) { + if (checkState("addLayerAt")) { + return; + } + nativeAddLayerAt(layer.getNativePtr(), index); + } + + @Override + public boolean removeLayer(@NonNull String layerId) { + if (checkState("removeLayer")) { + return false; + } + + Layer layer = getLayer(layerId); + if (layer != null) { + return removeLayer(layer); + } + return false; + } + + + @Override + public boolean removeLayer(@NonNull Layer layer) { + if (checkState("removeLayer")) { + return false; + } + return nativeRemoveLayer(layer.getNativePtr()); + } + + @Override + public boolean removeLayerAt(@IntRange(from = 0) int index) { + if (checkState("removeLayerAt")) { + return false; + } + return nativeRemoveLayerAt(index); + } + + @Override + @NonNull + public List getSources() { + if (checkState("getSources")) { + return new ArrayList<>(); + } + return Arrays.asList(nativeGetSources()); + } + + @Override + public Source getSource(@NonNull String sourceId) { + if (checkState("getSource")) { + return null; + } + return nativeGetSource(sourceId); + } + + @Override + public void addSource(@NonNull Source source) { + if (checkState("addSource")) { + return; + } + nativeAddSource(source, source.getNativePtr()); + } + + @Override + public boolean removeSource(@NonNull String sourceId) { + if (checkState("removeSource")) { + return false; + } + Source source = getSource(sourceId); + if (source != null) { + return removeSource(source); + } + return false; + } + + @Override + public boolean removeSource(@NonNull Source source) { + if (checkState("removeSource")) { + return false; + } + return nativeRemoveSource(source, source.getNativePtr()); + } + + @Override + public void addImages(@NonNull Image[] images) { + if (checkState("addImages")) { + return; + } + nativeAddImages(images); + } + + @Override + public void removeImage(String name) { + if (checkState("removeImage")) { + return; + } + nativeRemoveImage(name); + } + + @Override + public Bitmap getImage(String name) { + if (checkState("getImage")) { + return null; + } + return nativeGetImage(name); + } + + // Feature querying + + @Override + @NonNull + public List queryRenderedFeatures(@NonNull PointF coordinates, + @Nullable String[] layerIds, + @Nullable Expression filter) { + if (checkState("queryRenderedFeatures")) { + return new ArrayList<>(); + } + Feature[] features = nativeQueryRenderedFeaturesForPoint(coordinates.x / pixelRatio, + coordinates.y / pixelRatio, layerIds, filter != null ? filter.toArray() : null); + return features != null ? Arrays.asList(features) : new ArrayList(); + } + + @Override + @NonNull + public List queryRenderedFeatures(@NonNull RectF coordinates, + @Nullable String[] layerIds, + @Nullable Expression filter) { + if (checkState("queryRenderedFeatures")) { + return new ArrayList<>(); + } + Feature[] features = nativeQueryRenderedFeaturesForBox( + coordinates.left / pixelRatio, + coordinates.top / pixelRatio, + coordinates.right / pixelRatio, + coordinates.bottom / pixelRatio, + layerIds, + filter != null ? filter.toArray() : null); + return features != null ? Arrays.asList(features) : new ArrayList(); + } + + @Override + public void setApiBaseUrl(String baseUrl) { + if (checkState("setApiBaseUrl")) { + return; + } + fileSource.setApiBaseUrl(baseUrl); + } + + @Override + public Light getLight() { + if (checkState("getLight")) { + return null; + } + return nativeGetLight(); + } + + @Override + public float getPixelRatio() { + return pixelRatio; + } + + @NonNull + @Override + public RectF getDensityDependantRectangle(final RectF rectangle) { + return new RectF( + rectangle.left / pixelRatio, + rectangle.top / pixelRatio, + rectangle.right / pixelRatio, + rectangle.bottom / pixelRatio + ); + } + + // + // Callbacks + // + + @Keep + private void onCameraWillChange(boolean animated) { + if (stateCallback != null) { + stateCallback.onCameraWillChange(animated); + } + } + + @Keep + private void onCameraIsChanging() { + if (stateCallback != null) { + stateCallback.onCameraIsChanging(); + } + } + + @Keep + private void onCameraDidChange(boolean animated) { + if (stateCallback != null) { + stateCallback.onCameraDidChange(animated); + } + } + + @Keep + private void onWillStartLoadingMap() { + if (stateCallback != null) { + stateCallback.onWillStartLoadingMap(); + } + } + + @Keep + private void onDidFinishLoadingMap() { + if (stateCallback != null) { + stateCallback.onDidFinishLoadingMap(); + } + } + + @Keep + private void onDidFailLoadingMap(String error) { + if (stateCallback != null) { + stateCallback.onDidFailLoadingMap(error); + } + } + + @Keep + private void onWillStartRenderingFrame() { + if (stateCallback != null) { + stateCallback.onWillStartRenderingFrame(); + } + } + + @Keep + private void onDidFinishRenderingFrame(boolean fully) { + if (stateCallback != null) { + stateCallback.onDidFinishRenderingFrame(fully); + } + } + + @Keep + private void onWillStartRenderingMap() { + if (stateCallback != null) { + stateCallback.onWillStartRenderingMap(); + } + } + + @Keep + private void onDidFinishRenderingMap(boolean fully) { + if (stateCallback != null) { + stateCallback.onDidFinishRenderingMap(fully); + } + } + + @Keep + private void onDidBecomeIdle() { + if (stateCallback != null) { + stateCallback.onDidBecomeIdle(); + } + } + + @Keep + private void onDidFinishLoadingStyle() { + if (stateCallback != null) { + stateCallback.onDidFinishLoadingStyle(); + } + } + + @Keep + private void onSourceChanged(String sourceId) { + if (stateCallback != null) { + stateCallback.onSourceChanged(sourceId); + } + } + + @Keep + private void onStyleImageMissing(String imageId) { + if (stateCallback != null) { + stateCallback.onStyleImageMissing(imageId); + } + } + + @Keep + private boolean onCanRemoveUnusedStyleImage(String imageId) { + if (stateCallback != null) { + return stateCallback.onCanRemoveUnusedStyleImage(imageId); + } + + return true; + } + + @Keep + protected void onSnapshotReady(@Nullable Bitmap mapContent) { + if (checkState("OnSnapshotReady")) { + return; + } + + try { + if (snapshotReadyCallback != null && mapContent != null) { + if (viewCallback == null) { + snapshotReadyCallback.onSnapshotReady(mapContent); + } else { + Bitmap viewContent = viewCallback.getViewContent(); + if (viewContent != null) { + snapshotReadyCallback.onSnapshotReady(BitmapUtils.mergeBitmap(mapContent, viewContent)); + } + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onSnapshotReady", err); + throw err; + } + } + + // + // JNI methods + // + + @Keep + private native void nativeInitialize(NativeMapView nativeMap, + FileSource fileSource, + MapRenderer mapRenderer, + float pixelRatio, + boolean crossSourceCollisions); + + @Keep + private native void nativeDestroy(); + + @Keep + private native void nativeResizeView(int width, int height); + + @Keep + private native void nativeSetStyleUrl(String url); + + @NonNull + @Keep + private native String nativeGetStyleUrl(); + + @Keep + private native void nativeSetStyleJson(String newStyleJson); + + @NonNull + @Keep + private native String nativeGetStyleJson(); + + @Keep + private native void nativeSetLatLngBounds(LatLngBounds latLngBounds); + + @Keep + private native void nativeCancelTransitions(); + + @Keep + private native void nativeSetGestureInProgress(boolean inProgress); + + @Keep + private native void nativeMoveBy(double dx, double dy, long duration); + + @Keep + private native void nativeSetLatLng(double latitude, double longitude, double[] padding, long duration); + + @NonNull + @Keep + private native LatLng nativeGetLatLng(); + + @NonNull + @Keep + private native CameraPosition nativeGetCameraForLatLngBounds( + LatLngBounds latLngBounds, double top, double left, double bottom, double right, double bearing, double tilt); + + @NonNull + @Keep + private native CameraPosition nativeGetCameraForGeometry( + Geometry geometry, double top, double left, double bottom, double right, double bearing, double tilt); + + @Keep + private native void nativeResetPosition(); + + @Keep + private native double nativeGetPitch(); + + @Keep + private native void nativeSetPitch(double pitch, long duration); + + @Keep + private native void nativeSetZoom(double zoom, double cx, double cy, long duration); + + @Keep + private native double nativeGetZoom(); + + @Keep + private native void nativeResetZoom(); + + @Keep + private native void nativeSetMinZoom(double zoom); + + @Keep + private native double nativeGetMinZoom(); + + @Keep + private native void nativeSetMaxZoom(double zoom); + + @Keep + private native double nativeGetMaxZoom(); + + @Keep + private native void nativeRotateBy(double sx, double sy, double ex, double ey, long duration); + + @Keep + private native void nativeSetBearing(double degrees, long duration); + + @Keep + private native void nativeSetBearingXY(double degrees, double fx, double fy, long duration); + + @Keep + private native double nativeGetBearing(); + + @Keep + private native void nativeResetNorth(); + + @Keep + private native void nativeUpdateMarker(long markerId, double lat, double lon, String iconId); + + @NonNull + @Keep + private native long[] nativeAddMarkers(Marker[] markers); + + @NonNull + @Keep + private native long[] nativeAddPolylines(Polyline[] polylines); + + @NonNull + @Keep + private native long[] nativeAddPolygons(Polygon[] polygons); + + @Keep + private native void nativeRemoveAnnotations(long[] id); + + @NonNull + @Keep + private native long[] nativeQueryPointAnnotations(RectF rect); + + @NonNull + @Keep + private native long[] nativeQueryShapeAnnotations(RectF rect); + + @Keep + private native void nativeAddAnnotationIcon(String symbol, int width, int height, float scale, byte[] pixels); + + @Keep + private native void nativeRemoveAnnotationIcon(String symbol); + + @Keep + private native void nativeSetVisibleCoordinateBounds(LatLng[] coordinates, RectF padding, + double direction, long duration); + + @Keep + private native void nativeOnLowMemory(); + + @Keep + private native void nativeSetDebug(boolean debug); + + @Keep + private native void nativeCycleDebugOptions(); + + @Keep + private native boolean nativeGetDebug(); + + @Keep + private native boolean nativeIsFullyLoaded(); + + @Keep + private native void nativeSetReachability(boolean status); + + @Keep + private native double nativeGetMetersPerPixelAtLatitude(double lat, double zoom); + + @NonNull + @Keep + private native ProjectedMeters nativeProjectedMetersForLatLng(double latitude, double longitude); + + @NonNull + @Keep + private native LatLng nativeLatLngForProjectedMeters(double northing, double easting); + + @NonNull + @Keep + private native PointF nativePixelForLatLng(double lat, double lon); + + @NonNull + @Keep + private native LatLng nativeLatLngForPixel(float x, float y); + + @Keep + private native double nativeGetTopOffsetPixelsForAnnotationSymbol(String symbolName); + + @Keep + private native void nativeJumpTo(double angle, double latitude, double longitude, double pitch, double zoom, + double[] padding); + + @Keep + private native void nativeEaseTo(double angle, double latitude, double longitude, + long duration, double pitch, double zoom, double[] padding, + boolean easingInterpolator); + + @Keep + private native void nativeFlyTo(double angle, double latitude, double longitude, + long duration, double pitch, double zoom, double[] padding); + + @NonNull + @Keep + private native CameraPosition nativeGetCameraPosition(); + + @Keep + private native void nativeSetTransitionOptions(TransitionOptions transitionOptions); + + @NonNull + @Keep + private native TransitionOptions nativeGetTransitionOptions(); + + @Keep + private native long nativeGetTransitionDuration(); + + @Keep + private native void nativeSetTransitionDuration(long duration); + + @Keep + private native long nativeGetTransitionDelay(); + + @Keep + private native void nativeSetTransitionDelay(long delay); + + @NonNull + @Keep + private native Layer[] nativeGetLayers(); + + @NonNull + @Keep + private native Layer nativeGetLayer(String layerId); + + @Keep + private native void nativeAddLayer(long layerPtr, String before) throws CannotAddLayerException; + + @Keep + private native void nativeAddLayerAbove(long layerPtr, String above) throws CannotAddLayerException; + + @Keep + private native void nativeAddLayerAt(long layerPtr, int index) throws CannotAddLayerException; + + @Keep + private native boolean nativeRemoveLayer(long layerId); + + @Keep + private native boolean nativeRemoveLayerAt(int index); + + @NonNull + @Keep + private native Source[] nativeGetSources(); + + @NonNull + @Keep + private native Source nativeGetSource(String sourceId); + + @Keep + private native void nativeAddSource(Source source, long sourcePtr) throws CannotAddSourceException; + + @Keep + private native boolean nativeRemoveSource(Source source, long sourcePtr); + + @Keep + private native void nativeAddImage(String name, Bitmap bitmap, float pixelRatio, boolean sdf); + + @Keep + private native void nativeAddImages(Image[] images); + + @Keep + private native void nativeRemoveImage(String name); + + @NonNull + @Keep + private native Bitmap nativeGetImage(String name); + + @Keep + private native void nativeUpdatePolygon(long polygonId, Polygon polygon); + + @Keep + private native void nativeUpdatePolyline(long polylineId, Polyline polyline); + + @Keep + private native void nativeTakeSnapshot(); + + @NonNull + @Keep + private native Feature[] nativeQueryRenderedFeaturesForPoint(float x, float y, + String[] layerIds, + Object[] filter); + + @NonNull + @Keep + private native Feature[] nativeQueryRenderedFeaturesForBox(float left, float top, + float right, float bottom, + String[] layerIds, + Object[] filter); + + @NonNull + @Keep + private native Light nativeGetLight(); + + @Keep + private native void nativeSetPrefetchTiles(boolean enable); + + @Keep + private native boolean nativeGetPrefetchTiles(); + + @Override + public long getNativePtr() { + return nativePtr; + } + + // + // Snapshot + // + + @Override + public void addSnapshotCallback(@NonNull MapboxMap.SnapshotReadyCallback callback) { + if (checkState("addSnapshotCallback")) { + return; + } + snapshotReadyCallback = callback; + nativeTakeSnapshot(); + } + + @Override + public void setOnFpsChangedListener(@Nullable final MapboxMap.OnFpsChangedListener listener) { + final Handler handler = new Handler(); + mapRenderer.queueEvent(new Runnable() { + + @Override + public void run() { + if (listener != null) { + mapRenderer.setOnFpsChangedListener(new MapboxMap.OnFpsChangedListener() { + @Override + public void onFpsChanged(final double fps) { + handler.post(new Runnable() { + + @Override + public void run() { + listener.onFpsChanged(fps); + } + + }); + } + }); + } else { + mapRenderer.setOnFpsChangedListener(null); + } + } + }); + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + private double[] getAnimationPaddingAndClearCachedInsets(double[] providedPadding) { + if (providedPadding == null) { + providedPadding = this.edgeInsets; + } + this.edgeInsets = null; + return providedPadding == null ? null : + new double[] { + providedPadding[1] / pixelRatio, + providedPadding[0] / pixelRatio, + providedPadding[3] / pixelRatio, + providedPadding[2] / pixelRatio + }; + } + + public interface ViewCallback { + @Nullable + Bitmap getViewContent(); + } + + interface StyleCallback { + void onWillStartLoadingMap(); + + void onDidFinishLoadingStyle(); + } + + interface StateCallback extends StyleCallback { + void onCameraWillChange(boolean animated); + + void onCameraIsChanging(); + + void onCameraDidChange(boolean animated); + + void onDidFinishLoadingMap(); + + void onDidFailLoadingMap(String error); + + void onWillStartRenderingFrame(); + + void onDidFinishRenderingFrame(boolean fully); + + void onWillStartRenderingMap(); + + void onDidFinishRenderingMap(boolean fully); + + void onDidBecomeIdle(); + + void onSourceChanged(String sourceId); + + void onStyleImageMissing(String imageId); + + boolean onCanRemoveUnusedStyleImage(String imageId); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/OnMapReadyCallback.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/OnMapReadyCallback.java new file mode 100644 index 000000000..31b459abb --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/OnMapReadyCallback.java @@ -0,0 +1,22 @@ +package com.mapbox.mapboxsdk.maps; + +import android.support.annotation.NonNull; + +/** + * Interface definition for a callback to be invoked when the map is ready to be used. + *

+ * Once an instance of this interface is set on a {@link MapFragment} or {@link MapView} object, + * the onMapReady(MapboxMap) method is triggered when the map is ready to be used and provides an instance of + * {@link MapboxMap}. + *

+ */ +public interface OnMapReadyCallback { + + /** + * Called when the map is ready to be used. + * + * @param mapboxMap An instance of MapboxMap associated with the {@link MapFragment} or + * {@link MapView} that defines the callback. + */ + void onMapReady(@NonNull MapboxMap mapboxMap); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/PolygonContainer.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/PolygonContainer.java new file mode 100644 index 000000000..a7e2ace06 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/PolygonContainer.java @@ -0,0 +1,84 @@ +package com.mapbox.mapboxsdk.maps; + + +import android.support.annotation.NonNull; +import android.support.v4.util.LongSparseArray; + +import com.mapbox.mapboxsdk.annotations.Annotation; +import com.mapbox.mapboxsdk.annotations.Polygon; +import com.mapbox.mapboxsdk.annotations.PolygonOptions; + +import java.util.ArrayList; +import java.util.List; + +/** + * Encapsulates {@link Polygon}'s functionality. + */ +class PolygonContainer implements Polygons { + + private final NativeMap nativeMap; + private final LongSparseArray annotations; + + PolygonContainer(NativeMap nativeMap, LongSparseArray annotations) { + this.nativeMap = nativeMap; + this.annotations = annotations; + } + + @Override + public Polygon addBy(@NonNull PolygonOptions polygonOptions, @NonNull MapboxMap mapboxMap) { + Polygon polygon = polygonOptions.getPolygon(); + if (!polygon.getPoints().isEmpty()) { + long id = nativeMap != null ? nativeMap.addPolygon(polygon) : 0; + polygon.setId(id); + polygon.setMapboxMap(mapboxMap); + annotations.put(id, polygon); + } + return polygon; + } + + @NonNull + @Override + public List addBy(@NonNull List polygonOptionsList, @NonNull MapboxMap mapboxMap) { + int count = polygonOptionsList.size(); + + Polygon polygon; + List polygons = new ArrayList<>(count); + if (nativeMap != null && count > 0) { + for (PolygonOptions polygonOptions : polygonOptionsList) { + polygon = polygonOptions.getPolygon(); + if (!polygon.getPoints().isEmpty()) { + polygons.add(polygon); + } + } + + long[] ids = nativeMap.addPolygons(polygons); + for (int i = 0; i < ids.length; i++) { + polygon = polygons.get(i); + polygon.setMapboxMap(mapboxMap); + polygon.setId(ids[i]); + annotations.put(ids[i], polygon); + } + } + return polygons; + } + + @Override + public void update(@NonNull Polygon polygon) { + nativeMap.updatePolygon(polygon); + annotations.setValueAt(annotations.indexOfKey(polygon.getId()), polygon); + } + + @NonNull + @Override + public List obtainAll() { + List polygons = new ArrayList<>(); + Annotation annotation; + for (int i = 0; i < annotations.size(); i++) { + annotation = annotations.get(annotations.keyAt(i)); + if (annotation instanceof Polygon) { + polygons.add((Polygon) annotation); + } + } + return polygons; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Polygons.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Polygons.java new file mode 100644 index 000000000..2a0190b5b --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Polygons.java @@ -0,0 +1,22 @@ +package com.mapbox.mapboxsdk.maps; + + +import android.support.annotation.NonNull; + +import com.mapbox.mapboxsdk.annotations.Polygon; +import com.mapbox.mapboxsdk.annotations.PolygonOptions; + +import java.util.List; + +/** + * Interface that defines convenient methods for working with a {@link Polygon}'s collection. + */ +interface Polygons { + Polygon addBy(@NonNull PolygonOptions polygonOptions, @NonNull MapboxMap mapboxMap); + + List addBy(@NonNull List polygonOptionsList, @NonNull MapboxMap mapboxMap); + + void update(Polygon polygon); + + List obtainAll(); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/PolylineContainer.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/PolylineContainer.java new file mode 100644 index 000000000..e11da26e0 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/PolylineContainer.java @@ -0,0 +1,83 @@ +package com.mapbox.mapboxsdk.maps; + + +import android.support.annotation.NonNull; +import android.support.v4.util.LongSparseArray; + +import com.mapbox.mapboxsdk.annotations.Annotation; +import com.mapbox.mapboxsdk.annotations.Polyline; +import com.mapbox.mapboxsdk.annotations.PolylineOptions; + +import java.util.ArrayList; +import java.util.List; + +/** + * Encapsulates {@link Polyline}'s functionality. + */ +class PolylineContainer implements Polylines { + + private final NativeMap nativeMap; + private final LongSparseArray annotations; + + PolylineContainer(NativeMap nativeMap, LongSparseArray annotations) { + this.nativeMap = nativeMap; + this.annotations = annotations; + } + + @Override + public Polyline addBy(@NonNull PolylineOptions polylineOptions, @NonNull MapboxMap mapboxMap) { + Polyline polyline = polylineOptions.getPolyline(); + if (!polyline.getPoints().isEmpty()) { + long id = nativeMap != null ? nativeMap.addPolyline(polyline) : 0; + polyline.setMapboxMap(mapboxMap); + polyline.setId(id); + annotations.put(id, polyline); + } + return polyline; + } + + @NonNull + @Override + public List addBy(@NonNull List polylineOptionsList, @NonNull MapboxMap mapboxMap) { + int count = polylineOptionsList.size(); + Polyline polyline; + List polylines = new ArrayList<>(count); + if (nativeMap != null && count > 0) { + for (PolylineOptions options : polylineOptionsList) { + polyline = options.getPolyline(); + if (!polyline.getPoints().isEmpty()) { + polylines.add(polyline); + } + } + + long[] ids = nativeMap.addPolylines(polylines); + for (int i = 0; i < ids.length; i++) { + Polyline polylineCreated = polylines.get(i); + polylineCreated.setMapboxMap(mapboxMap); + polylineCreated.setId(ids[i]); + annotations.put(ids[i], polylineCreated); + } + } + return polylines; + } + + @Override + public void update(@NonNull Polyline polyline) { + nativeMap.updatePolyline(polyline); + annotations.setValueAt(annotations.indexOfKey(polyline.getId()), polyline); + } + + @NonNull + @Override + public List obtainAll() { + List polylines = new ArrayList<>(); + Annotation annotation; + for (int i = 0; i < annotations.size(); i++) { + annotation = annotations.get(annotations.keyAt(i)); + if (annotation instanceof Polyline) { + polylines.add((Polyline) annotation); + } + } + return polylines; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Polylines.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Polylines.java new file mode 100644 index 000000000..c9a865cdd --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Polylines.java @@ -0,0 +1,22 @@ +package com.mapbox.mapboxsdk.maps; + + +import android.support.annotation.NonNull; + +import com.mapbox.mapboxsdk.annotations.Polyline; +import com.mapbox.mapboxsdk.annotations.PolylineOptions; + +import java.util.List; + +/** + * Interface that defines convenient methods for working with a {@link Polyline}'s collection. + */ +interface Polylines { + Polyline addBy(@NonNull PolylineOptions polylineOptions, @NonNull MapboxMap mapboxMap); + + List addBy(@NonNull List polylineOptionsList, @NonNull MapboxMap mapboxMap); + + void update(Polyline polyline); + + List obtainAll(); +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Projection.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Projection.java new file mode 100644 index 000000000..76395f956 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Projection.java @@ -0,0 +1,283 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.PointF; +import android.support.annotation.FloatRange; +import android.support.annotation.NonNull; + +import com.mapbox.geojson.Point; +import com.mapbox.mapboxsdk.constants.GeometryConstants; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.mapboxsdk.geometry.ProjectedMeters; +import com.mapbox.mapboxsdk.geometry.VisibleRegion; + +import java.util.ArrayList; +import java.util.List; + +/** + * A projection is used to translate between on screen location and geographic coordinates on + * the surface of the Earth. Screen location is in screen pixels (not display pixels) + * with respect to the top left corner of the map (and not necessarily of the whole screen). + */ +public class Projection { + + @NonNull + private final NativeMap nativeMapView; + @NonNull + private final MapView mapView; + + Projection(@NonNull NativeMap nativeMapView, @NonNull MapView mapView) { + this.nativeMapView = nativeMapView; + this.mapView = mapView; + } + + void setContentPadding(int[] contentPadding) { + double[] output = new double[contentPadding.length]; + for (int i = 0; i < contentPadding.length; i++) { + output[i] = contentPadding[i]; + } + nativeMapView.setContentPadding(output); + } + + int[] getContentPadding() { + double[] padding = nativeMapView.getCameraPosition().padding; + return new int[] {(int) padding[0], (int) padding[1], (int) padding[2], (int) padding[3]}; + } + + /** + * @deprecated unused + */ + @Deprecated + public void invalidateContentPadding() { + } + + /** + * Returns the spherical Mercator projected meters for a LatLng. + */ + @NonNull + public ProjectedMeters getProjectedMetersForLatLng(@NonNull LatLng latLng) { + return nativeMapView.projectedMetersForLatLng(latLng); + } + + /** + * Returns the LatLng for a spherical Mercator projected meters. + */ + @NonNull + public LatLng getLatLngForProjectedMeters(@NonNull ProjectedMeters projectedMeters) { + return nativeMapView.latLngForProjectedMeters(projectedMeters); + } + + /** + *

+ * Returns the distance spanned by one pixel at the specified latitude and current zoom level. + *

+ * The distance between pixels decreases as the latitude approaches the poles. + * This relationship parallels the relationship between longitudinal coordinates at different latitudes. + * + * @param latitude The latitude for which to return the value. + * @return The distance measured in meters. + */ + public double getMetersPerPixelAtLatitude(@FloatRange(from = -90, to = 90) double latitude) { + return nativeMapView.getMetersPerPixelAtLatitude(latitude); + } + + /** + * Returns the geographic location that corresponds to a screen location. + * The screen location is specified in screen pixels (not display pixels) relative to the + * top left of the map (not the top left of the whole screen). + * + * @param point A Point on the screen in screen pixels. + * @return The LatLng corresponding to the point on the screen, or null if the ray through + * the given screen point does not intersect the ground plane. + */ + @NonNull + public LatLng fromScreenLocation(@NonNull PointF point) { + return nativeMapView.latLngForPixel(point); + } + + /** + * Gets a projection of the viewing frustum for converting between screen coordinates and + * geo-latitude/longitude coordinates. + *

+ * This method ignores the content padding. + * + * @return The projection of the viewing frustum in its current state. + */ + @NonNull + public VisibleRegion getVisibleRegion() { + return getVisibleRegion(true); + } + + /** + * Gets a projection of the viewing frustum for converting between screen coordinates and + * geo-latitude/longitude coordinates. + * + * @param ignorePadding True if the padding should be ignored, + * false if the returned region should be reduced by the padding. + * @return The projection of the viewing frustum in its current state. + */ + @NonNull + public VisibleRegion getVisibleRegion(boolean ignorePadding) { + float left; + float right; + float top; + float bottom; + + if (ignorePadding) { + left = 0; + right = mapView.getWidth(); + top = 0; + bottom = mapView.getHeight(); + } else { + int[] contentPadding = getContentPadding(); + left = contentPadding[0]; + right = mapView.getWidth() - contentPadding[2]; + top = contentPadding[1]; + bottom = mapView.getHeight() - contentPadding[3]; + } + + LatLng center = fromScreenLocation(new PointF(left + (right - left) / 2, top + (bottom - top) / 2)); + + LatLng topLeft = fromScreenLocation(new PointF(left, top)); + LatLng topRight = fromScreenLocation(new PointF(right, top)); + LatLng bottomRight = fromScreenLocation(new PointF(right, bottom)); + LatLng bottomLeft = fromScreenLocation(new PointF(left, bottom)); + + List latLngs = new ArrayList<>(); + latLngs.add(topRight); + latLngs.add(bottomRight); + latLngs.add(bottomLeft); + latLngs.add(topLeft); + + double maxEastLonSpan = 0; + double maxWestLonSpan = 0; + + double east = 0; + double west = 0; + double north = GeometryConstants.MIN_LATITUDE; + double south = GeometryConstants.MAX_LATITUDE; + + for (LatLng latLng : latLngs) { + double bearing = bearing(center, latLng); + + if (bearing >= 0) { + double span = getLongitudeSpan(latLng.getLongitude(), center.getLongitude()); + if (span > maxEastLonSpan) { + maxEastLonSpan = span; + east = latLng.getLongitude(); + } + } else { + double span = getLongitudeSpan(center.getLongitude(), latLng.getLongitude()); + if (span > maxWestLonSpan) { + maxWestLonSpan = span; + west = latLng.getLongitude(); + } + } + + if (north < latLng.getLatitude()) { + north = latLng.getLatitude(); + } + if (south > latLng.getLatitude()) { + south = latLng.getLatitude(); + } + } + + if (east < west) { + return new VisibleRegion(topLeft, topRight, bottomLeft, bottomRight, + LatLngBounds.from(north, east + GeometryConstants.LONGITUDE_SPAN, south, west)); + } + return new VisibleRegion(topLeft, topRight, bottomLeft, bottomRight, + LatLngBounds.from(north, east, south, west)); + } + + /** + * Takes two {@link Point}s and finds the geographic bearing between them. + * + * @param latLng1 the first point used for calculating the bearing + * @param latLng2 the second point used for calculating the bearing + * @return bearing in decimal degrees + * @see Turf Bearing documentation + */ + static double bearing(@NonNull LatLng latLng1, @NonNull LatLng latLng2) { + + double lon1 = degreesToRadians(latLng1.getLongitude()); + double lon2 = degreesToRadians(latLng2.getLongitude()); + double lat1 = degreesToRadians(latLng1.getLatitude()); + double lat2 = degreesToRadians(latLng2.getLatitude()); + + double value1 = Math.sin(lon2 - lon1) * Math.cos(lat2); + double value2 = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) + * Math.cos(lat2) * Math.cos(lon2 - lon1); + + return radiansToDegrees(Math.atan2(value1, value2)); + } + + /** + * Converts an angle in degrees to radians. + * + * @param degrees angle between 0 and 360 degrees + * @return angle in radians + */ + static double degreesToRadians(double degrees) { + double radians = degrees % 360; + return radians * Math.PI / 180; + } + + /** + * Converts an angle in radians to degrees. + * + * @param radians angle in radians + * @return degrees between 0 and 360 degrees + */ + static double radiansToDegrees(double radians) { + double degrees = radians % (2 * Math.PI); + return degrees * 180 / Math.PI; + } + + /** + * Get the absolute distance, in degrees, between the west and + * east boundaries of this LatLngBounds + * + * @return Span distance + */ + static double getLongitudeSpan(double east, double west) { + double longSpan = Math.abs(east - west); + if (east > west) { + return longSpan; + } + + // shortest span contains antimeridian + return GeometryConstants.LONGITUDE_SPAN - longSpan; + } + + /** + * Returns a screen location that corresponds to a geographical coordinate (LatLng). + * The screen location is in screen pixels (not display pixels) relative to the top left + * of the map (not of the whole screen). + * + * @param location A LatLng on the map to convert to a screen location. + * @return A Point representing the screen location in screen pixels. + */ + @NonNull + public PointF toScreenLocation(@NonNull LatLng location) { + return nativeMapView.pixelForLatLng(location); + } + + float getHeight() { + return mapView.getHeight(); + } + + float getWidth() { + return mapView.getWidth(); + } + + /** + * Calculates a zoom level based on minimum scale and current scale from MapView + * + * @param minScale The minimum scale to calculate the zoom level. + * @return zoom level that fits the MapView. + */ + public double calculateZoom(float minScale) { + return nativeMapView.getZoom() + Math.log(minScale) / Math.log(2); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotationContainer.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotationContainer.java new file mode 100644 index 000000000..9c2b97b6a --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotationContainer.java @@ -0,0 +1,41 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.support.v4.util.LongSparseArray; + +import com.mapbox.mapboxsdk.annotations.Annotation; + +import java.util.ArrayList; +import java.util.List; + +class ShapeAnnotationContainer implements ShapeAnnotations { + + private final NativeMap nativeMapView; + private final LongSparseArray annotations; + + ShapeAnnotationContainer(NativeMap nativeMapView, LongSparseArray annotations) { + this.nativeMapView = nativeMapView; + this.annotations = annotations; + } + + @NonNull + @Override + public List obtainAllIn(@NonNull RectF rectangle) { + RectF rect = nativeMapView.getDensityDependantRectangle(rectangle); + long[] annotationIds = nativeMapView.queryShapeAnnotations(rect); + return getAnnotationsFromIds(annotationIds); + } + + @NonNull + private List getAnnotationsFromIds(long[] annotationIds) { + List shapeAnnotations = new ArrayList<>(); + for (long annotationId : annotationIds) { + Annotation annotation = annotations.get(annotationId); + if (annotation != null) { + shapeAnnotations.add(annotation); + } + } + return shapeAnnotations; + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotations.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotations.java new file mode 100644 index 000000000..f9b2ca10d --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotations.java @@ -0,0 +1,13 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.RectF; + +import com.mapbox.mapboxsdk.annotations.Annotation; + +import java.util.List; + +interface ShapeAnnotations { + + List obtainAllIn(RectF rectF); + +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Style.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Style.java new file mode 100644 index 000000000..b21186050 --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Style.java @@ -0,0 +1,1150 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringDef; +import android.util.DisplayMetrics; +import android.util.Pair; +import com.mapbox.mapboxsdk.constants.MapboxConstants; +import com.mapbox.mapboxsdk.style.layers.Layer; +import com.mapbox.mapboxsdk.style.layers.TransitionOptions; +import com.mapbox.mapboxsdk.style.light.Light; +import com.mapbox.mapboxsdk.style.sources.Source; +import com.mapbox.mapboxsdk.utils.BitmapUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The proxy object for current map style. + *

+ * To create new instances of this object, create a new instance using a {@link Builder} and load the style with + * MapboxMap. This object is returned from {@link MapboxMap#getStyle()} once the style + * has been loaded by underlying map. + *

+ */ +@SuppressWarnings("unchecked") +public class Style { + + static final String EMPTY_JSON = "{\"version\": 8,\"sources\": {},\"layers\": []}"; + + private final NativeMap nativeMap; + private final HashMap sources = new HashMap<>(); + private final HashMap layers = new HashMap<>(); + private final HashMap images = new HashMap<>(); + private final Builder builder; + private boolean fullyLoaded; + + /** + * Private constructor to build a style object. + * + * @param builder the builder used for creating this style + * @param nativeMap the map object used to load this style + */ + private Style(@NonNull Builder builder, @NonNull NativeMap nativeMap) { + this.builder = builder; + this.nativeMap = nativeMap; + } + + /** + * Returns the current style url. + * + * @return the style url + * @deprecated use {@link #getUri()} instead + */ + @NonNull + @Deprecated + public String getUrl() { + validateState("getUrl"); + return nativeMap.getStyleUri(); + } + + /** + * Returns the current style uri. + * + * @return the style uri + */ + @NonNull + public String getUri() { + validateState("getUri"); + return nativeMap.getStyleUri(); + } + + /** + * Returns the current style json. + * + * @return the style json + */ + @NonNull + public String getJson() { + validateState("getJson"); + return nativeMap.getStyleJson(); + } + + // + // Source + // + + /** + * Retrieve all the sources in the style + * + * @return all the sources in the current style + */ + @NonNull + public List getSources() { + validateState("getSources"); + return nativeMap.getSources(); + } + + /** + * Adds the source to the map. The source must be newly created and not added to the map before + * + * @param source the source to add + */ + public void addSource(@NonNull Source source) { + validateState("addSource"); + nativeMap.addSource(source); + sources.put(source.getId(), source); + } + + /** + * Retrieve a source by id + * + * @param id the source's id + * @return the source if present in the current style + */ + @Nullable + public Source getSource(String id) { + validateState("getSource"); + Source source = sources.get(id); + if (source == null) { + source = nativeMap.getSource(id); + } + return source; + } + + /** + * Tries to cast the Source to T, throws ClassCastException if it's another type. + * + * @param sourceId the id used to look up a layer + * @param the generic type of a Source + * @return the casted Source, null if another type + */ + @Nullable + public T getSourceAs(@NonNull String sourceId) { + validateState("getSourceAs"); + // noinspection unchecked + if (sources.containsKey(sourceId)) { + return (T) sources.get(sourceId); + } + return (T) nativeMap.getSource(sourceId); + } + + /** + * Removes the source from the style. + * + * @param sourceId the source to remove + * @return the source handle or null if the source was not present + */ + public boolean removeSource(@NonNull String sourceId) { + validateState("removeSource"); + sources.remove(sourceId); + return nativeMap.removeSource(sourceId); + } + + /** + * Removes the source, preserving the reference for re-use + * + * @param source the source to remove + * @return the source + */ + public boolean removeSource(@NonNull Source source) { + validateState("removeSource"); + sources.remove(source.getId()); + return nativeMap.removeSource(source); + } + + // + // Layer + // + + /** + * Adds the layer to the map. The layer must be newly created and not added to the map before + * + * @param layer the layer to add + */ + public void addLayer(@NonNull Layer layer) { + validateState("addLayer"); + nativeMap.addLayer(layer); + layers.put(layer.getId(), layer); + } + + /** + * Adds the layer to the map. The layer must be newly created and not added to the map before + * + * @param layer the layer to add + * @param below the layer id to add this layer before + */ + public void addLayerBelow(@NonNull Layer layer, @NonNull String below) { + validateState("addLayerBelow"); + nativeMap.addLayerBelow(layer, below); + layers.put(layer.getId(), layer); + } + + /** + * Adds the layer to the map. The layer must be newly created and not added to the map before + * + * @param layer the layer to add + * @param above the layer id to add this layer above + */ + public void addLayerAbove(@NonNull Layer layer, @NonNull String above) { + validateState("addLayerAbove"); + nativeMap.addLayerAbove(layer, above); + layers.put(layer.getId(), layer); + } + + /** + * Adds the layer to the map at the specified index. The layer must be newly + * created and not added to the map before + * + * @param layer the layer to add + * @param index the index to insert the layer at + */ + public void addLayerAt(@NonNull Layer layer, @IntRange(from = 0) int index) { + validateState("addLayerAbove"); + nativeMap.addLayerAt(layer, index); + layers.put(layer.getId(), layer); + } + + /** + * Get the layer by id + * + * @param id the layer's id + * @return the layer, if present in the style + */ + @Nullable + public Layer getLayer(@NonNull String id) { + validateState("getLayer"); + Layer layer = layers.get(id); + if (layer == null) { + layer = nativeMap.getLayer(id); + } + return layer; + } + + /** + * Tries to cast the Layer to T, throws ClassCastException if it's another type. + * + * @param layerId the layer id used to look up a layer + * @param the generic attribute of a Layer + * @return the casted Layer, null if another type + */ + @Nullable + public T getLayerAs(@NonNull String layerId) { + validateState("getLayerAs"); + // noinspection unchecked + return (T) nativeMap.getLayer(layerId); + } + + /** + * Retrieve all the layers in the style + * + * @return all the layers in the current style + */ + @NonNull + public List getLayers() { + validateState("getLayers"); + return nativeMap.getLayers(); + } + + /** + * Removes the layer. Any references to the layer become invalid and should not be used anymore + * + * @param layerId the layer to remove + * @return the removed layer or null if not found + */ + public boolean removeLayer(@NonNull String layerId) { + validateState("removeLayer"); + layers.remove(layerId); + return nativeMap.removeLayer(layerId); + } + + /** + * Removes the layer. The reference is re-usable after this and can be re-added + * + * @param layer the layer to remove + * @return the layer + */ + public boolean removeLayer(@NonNull Layer layer) { + validateState("removeLayer"); + layers.remove(layer.getId()); + return nativeMap.removeLayer(layer); + } + + /** + * Removes the layer. Any other references to the layer become invalid and should not be used anymore + * + * @param index the layer index + * @return the removed layer or null if not found + */ + public boolean removeLayerAt(@IntRange(from = 0) int index) { + validateState("removeLayerAt"); + return nativeMap.removeLayerAt(index); + } + + // + // Image + // + + /** + * Adds an image to be used in the map's style + * + * @param name the name of the image + * @param image the pre-multiplied Bitmap + */ + public void addImage(@NonNull String name, @NonNull Bitmap image) { + addImage(name, image, false); + } + + /** + * Adds an drawable to be converted into a bitmap to be used in the map's style + * + * @param name the name of the image + * @param drawable the drawable instance to convert + */ + public void addImage(@NonNull String name, @NonNull Drawable drawable) { + Bitmap bitmap = BitmapUtils.getBitmapFromDrawable(drawable); + if (bitmap == null) { + throw new IllegalArgumentException("Provided drawable couldn't be converted to a Bitmap."); + } + addImage(name, bitmap, false); + } + + /** + * Adds an image to be used in the map's style + * + * @param name the name of the image + * @param bitmap the pre-multiplied Bitmap + * @param sdf the flag indicating image is an SDF or template image + */ + public void addImage(@NonNull final String name, @NonNull Bitmap bitmap, boolean sdf) { + validateState("addImage"); + nativeMap.addImages(new Image[]{toImage(new Builder.ImageWrapper(name, bitmap, sdf))}); + } + + /** + * Adds an image asynchronously, to be used in the map's style. + * + * @param name the name of the image + * @param image the pre-multiplied Bitmap + */ + public void addImageAsync(@NonNull String name, @NonNull Bitmap image) { + addImageAsync(name, image, false); + } + + /** + * Adds an drawable asynchronously, to be converted into a bitmap to be used in the map's style. + * + * @param name the name of the image + * @param drawable the drawable instance to convert + */ + public void addImageAsync(@NonNull String name, @NonNull Drawable drawable) { + Bitmap bitmap = BitmapUtils.getBitmapFromDrawable(drawable); + if (bitmap == null) { + throw new IllegalArgumentException("Provided drawable couldn't be converted to a Bitmap."); + } + addImageAsync(name, bitmap, false); + } + + /** + * Adds an image asynchronously, to be used in the map's style. + * + * @param name the name of the image + * @param bitmap the pre-multiplied Bitmap + * @param sdf the flag indicating image is an SDF or template image + */ + public void addImageAsync(@NonNull final String name, @NonNull Bitmap bitmap, boolean sdf) { + validateState("addImage"); + new BitmapImageConversionTask(nativeMap).execute(new Builder.ImageWrapper(name, bitmap, sdf)); + } + + /** + * Adds images to be used in the map's style. + * + * @param images the map of images to add + */ + public void addImages(@NonNull HashMap images) { + addImages(images, false); + } + + /** + * Adds images to be used in the map's style. + * + * @param images the map of images to add + * @param sdf the flag indicating image is an SDF or template image + */ + public void addImages(@NonNull HashMap images, boolean sdf) { + validateState("addImage"); + Image[] convertedImages = new Image[images.size()]; + int index = 0; + for (Builder.ImageWrapper imageWrapper : Builder.ImageWrapper.convertToImageArray(images, sdf)) { + convertedImages[index] = toImage(imageWrapper); + index++; + } + + nativeMap.addImages(convertedImages); + } + + /** + * Adds images asynchronously, to be used in the map's style. + * + * @param images the map of images to add + */ + public void addImagesAsync(@NonNull HashMap images) { + addImagesAsync(images, false); + } + + /** + * Adds images asynchronously, to be used in the map's style. + * + * @param images the map of images to add + * @param sdf the flag indicating image is an SDF or template image + */ + public void addImagesAsync(@NonNull HashMap images, boolean sdf) { + validateState("addImages"); + new BitmapImageConversionTask(nativeMap).execute(Builder.ImageWrapper.convertToImageArray(images, sdf)); + } + + /** + * Removes an image from the map's style. + * + * @param name the name of the image to remove + */ + public void removeImage(@NonNull String name) { + validateState("removeImage"); + nativeMap.removeImage(name); + } + + /** + * Get an image from the map's style using an id. + * + * @param id the id of the image + * @return the image bitmap + */ + @Nullable + public Bitmap getImage(@NonNull String id) { + validateState("getImage"); + return nativeMap.getImage(id); + } + + // + // Transition + // + + /** + *

+ * Set the transition options for style changes. + *

+ * If not set, any changes take effect without animation, besides symbols, + * which will fade in/out with a default duration after symbol collision detection. + *

+ * To disable symbols fade in/out animation, + * pass transition options with {@link TransitionOptions#enablePlacementTransitions} equal to false. + *

+ * Both {@link TransitionOptions#duration} and {@link TransitionOptions#delay} + * will also change the behavior of the symbols fade in/out animation if the placement transition is enabled. + * + * @param transitionOptions the transition options + */ + public void setTransition(@NonNull TransitionOptions transitionOptions) { + validateState("setTransition"); + nativeMap.setTransitionOptions(transitionOptions); + } + + /** + *

+ * Get the transition options for style changes. + *

+ * By default, any changes take effect without animation, besides symbols, + * which will fade in/out with a default duration after symbol collision detection. + *

+ * To disable symbols fade in/out animation, + * pass transition options with {@link TransitionOptions#enablePlacementTransitions} equal to false + * into {@link #setTransition(TransitionOptions)}. + *

+ * Both {@link TransitionOptions#duration} and {@link TransitionOptions#delay} + * will also change the behavior of the symbols fade in/out animation if the placement transition is enabled. + * + * @return TransitionOptions the transition options + */ + @NonNull + public TransitionOptions getTransition() { + validateState("getTransition"); + return nativeMap.getTransitionOptions(); + } + + // + // Light + // + + /** + * Get the light source used to change lighting conditions on extruded fill layers. + * + * @return the global light source + */ + @Nullable + public Light getLight() { + validateState("getLight"); + return nativeMap.getLight(); + } + + // + // State + // + + /** + * Called when the underlying map will start loading a new style or the map is destroyed. + * This method will clean up this style by setting the java sources and layers + * in a detached state and removing them from core. + */ + void clear() { + fullyLoaded = false; + for (Layer layer : layers.values()) { + if (layer != null) { + layer.setDetached(); + } + } + + for (Source source : sources.values()) { + if (source != null) { + source.setDetached(); + } + } + + for (Map.Entry bitmapEntry : images.entrySet()) { + nativeMap.removeImage(bitmapEntry.getKey()); + bitmapEntry.getValue().recycle(); + } + + sources.clear(); + layers.clear(); + images.clear(); + } + + /** + * Called when the underlying map has finished loading this style. + * This method will add all components added to the builder that were defined with the 'with' prefix. + */ + void onDidFinishLoadingStyle() { + if (!fullyLoaded) { + fullyLoaded = true; + for (Source source : builder.sources) { + addSource(source); + } + + for (Builder.LayerWrapper layerWrapper : builder.layers) { + if (layerWrapper instanceof Builder.LayerAtWrapper) { + addLayerAt(layerWrapper.layer, ((Builder.LayerAtWrapper) layerWrapper).index); + } else if (layerWrapper instanceof Builder.LayerAboveWrapper) { + addLayerAbove(layerWrapper.layer, ((Builder.LayerAboveWrapper) layerWrapper).aboveLayer); + } else if (layerWrapper instanceof Builder.LayerBelowWrapper) { + addLayerBelow(layerWrapper.layer, ((Builder.LayerBelowWrapper) layerWrapper).belowLayer); + } else { + // just add layer to map, but below annotations + addLayerBelow(layerWrapper.layer, MapboxConstants.LAYER_ID_ANNOTATIONS); + } + } + + for (Builder.ImageWrapper image : builder.images) { + addImage(image.id, image.bitmap, image.sdf); + } + + if (builder.transitionOptions != null) { + setTransition(builder.transitionOptions); + } + } + } + + /** + * Returns true if the style is fully loaded. Returns false if style hasn't been fully loaded or a new style is + * underway of being loaded. + * + * @return True if fully loaded, false otherwise + */ + public boolean isFullyLoaded() { + return fullyLoaded; + } + + /** + * Validates the style state, throw an IllegalArgumentException on invalid state. + * + * @param methodCall the calling method name + */ + private void validateState(String methodCall) { + if (!fullyLoaded) { + throw new IllegalStateException( + String.format("Calling %s when a newer style is loading/has loaded.", methodCall) + ); + } + } + + // + // Builder + // + + /** + * Builder for composing a style object. + */ + public static class Builder { + + private final List sources = new ArrayList<>(); + private final List layers = new ArrayList<>(); + private final List images = new ArrayList<>(); + + private TransitionOptions transitionOptions; + private String styleUri; + private String styleJson; + + /** + *

+ * Will loads a new map style asynchronous from the specified URL. + *

+ * {@code url} can take the following forms: + *
    + *
  • {@code Style#StyleUrl}: load one of the bundled styles in {@link Style}.
  • + *
  • {@code mapbox://styles// + \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/AccountsManagerTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/AccountsManagerTest.java new file mode 100644 index 000000000..5ce71ceac --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/AccountsManagerTest.java @@ -0,0 +1,59 @@ +package com.mapbox.mapboxsdk; + +import android.content.SharedPreferences; +import android.text.format.DateUtils; + +import org.junit.Test; + +import static com.mapbox.mapboxsdk.constants.MapboxConstants.KEY_PREFERENCE_SKU_TOKEN; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AccountsManagerTest { + @Test + public void testIsExpired() { + long now = AccountsManager.getNow(); + + long defaultValue = 0L; + long tooOld = now - DateUtils.HOUR_IN_MILLIS - 1; + long futureValue = now + 1; + long immediatePast = now - 1; + + assertTrue(AccountsManager.isExpired(now, defaultValue)); + assertTrue(AccountsManager.isExpired(now, tooOld)); + + assertFalse(AccountsManager.isExpired(now, futureValue)); + assertFalse(AccountsManager.isExpired(now, immediatePast)); + } + + @Test + public void checksSkuTokenExternalManagement() { + SharedPreferences mockedSharedPreferences = mock(SharedPreferences.class); + when(mockedSharedPreferences.getString(KEY_PREFERENCE_SKU_TOKEN, "")).thenReturn("external-sku-token"); + boolean isNotManaged = false; + AccountsManager theAccountsManager = new AccountsManager(mockedSharedPreferences, isNotManaged); + + String skuToken = theAccountsManager.getSkuToken(); + + assertEquals("external-sku-token", skuToken); + } + + @Test + public void checksSkuTokenInternalManagement() { + SharedPreferences mockedSharedPreferences = mock(SharedPreferences.class, RETURNS_DEEP_STUBS); + when(mockedSharedPreferences.getString("com.mapbox.mapboxsdk.accounts.userid", "")) + .thenReturn("any-user-id"); + boolean isManaged = true; + AccountsManager theAccountsManager = new AccountsManager(mockedSharedPreferences, isManaged); + + String skuToken = theAccountsManager.getSkuToken(); + + assertFalse(skuToken.isEmpty()); + assertTrue(skuToken.startsWith("100")); + assertTrue(skuToken.endsWith("any-user-id")); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/MapboxInjector.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/MapboxInjector.java new file mode 100644 index 000000000..966c75631 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/MapboxInjector.java @@ -0,0 +1,48 @@ +package com.mapbox.mapboxsdk; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.lang.reflect.Field; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MapboxInjector { + + private static final String FIELD_INSTANCE = "INSTANCE"; + private static final String FIELD_ACCOUNTS = "accounts"; + + public static void inject(@NonNull Context context, @NonNull String accessToken) { + inject(context, accessToken, null); + } + + public static void inject(@NonNull Context context, @NonNull String accessToken, @Nullable String skuToken) { + Mapbox mapbox = new Mapbox(context, accessToken); + try { + Field instance = Mapbox.class.getDeclaredField(FIELD_INSTANCE); + instance.setAccessible(true); + instance.set(mapbox, mapbox); + + Field accounts = Mapbox.class.getDeclaredField(FIELD_ACCOUNTS); + accounts.setAccessible(true); + + AccountsManager manager = mock(AccountsManager.class); + when(manager.getSkuToken()).thenReturn(skuToken); + accounts.set(mapbox, manager); + } catch (Exception exception) { + throw new AssertionError(); + } + } + + public static void clear() { + try { + Field field = Mapbox.class.getDeclaredField(FIELD_INSTANCE); + field.setAccessible(true); + field.set(field, null); + } catch (Exception exception) { + throw new AssertionError(); + } + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/MapboxTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/MapboxTest.java new file mode 100644 index 000000000..a4b573672 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/MapboxTest.java @@ -0,0 +1,109 @@ +package com.mapbox.mapboxsdk; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.DisplayMetrics; + +import com.mapbox.mapboxsdk.exceptions.MapboxConfigurationException; +import com.mapbox.mapboxsdk.maps.MapView; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertSame; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MapboxTest { + + private Context context; + private Context appContext; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Before + public void before() { + context = mock(Context.class); + appContext = mock(Context.class); + when(context.getApplicationContext()).thenReturn(appContext); + } + + @Test + public void testGetAccessToken() { + final String accessToken = "pk.0000000001"; + MapboxInjector.inject(context, accessToken); + assertSame(accessToken, Mapbox.getAccessToken()); + } + + @Test + public void testApplicationContext() { + MapboxInjector.inject(context, "pk.0000000001"); + assertNotNull(Mapbox.getApplicationContext()); + assertNotEquals(context, appContext); + assertEquals(appContext, appContext); + } + + @Test + public void testPkTokenValid() { + assertTrue(Mapbox.isAccessTokenValid("pk.0000000001")); + } + + @Test + public void testSkTokenValid() { + assertTrue(Mapbox.isAccessTokenValid("sk.0000000001")); + } + + @Test + public void testEmptyToken() { + assertFalse(Mapbox.isAccessTokenValid("")); + } + + @Test + public void testNullToken() { + assertFalse(Mapbox.isAccessTokenValid(null)); + } + + @Test + public void testBlaBlaToken() { + assertFalse(Mapbox.isAccessTokenValid("blabla")); + } + + @Test + public void testNoInstance() { + DisplayMetrics displayMetrics = mock(DisplayMetrics.class); + Resources resources = mock(Resources.class); + when(resources.getDisplayMetrics()).thenReturn(displayMetrics); + when(context.getResources()).thenReturn(resources); + TypedArray typedArray = mock(TypedArray.class); + when(context.obtainStyledAttributes(nullable(AttributeSet.class), any(int[].class), anyInt(), anyInt())) + .thenReturn(typedArray); + + expectedException.expect(MapboxConfigurationException.class); + expectedException.expectMessage( + "\nUsing MapView requires calling Mapbox.getInstance(Context context, String accessToken) before " + + "inflating or creating the view. The access token parameter is required when using a Mapbox service." + + "\nPlease see https://www.mapbox.com/help/create-api-access-token/ to learn how to create one." + + "\nMore information in this guide https://www.mapbox.com/help/first-steps-android-sdk/#access-tokens." + ); + new MapView(context); + } + + @After + public void after() { + MapboxInjector.clear(); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/AnnotationTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/AnnotationTest.java new file mode 100644 index 000000000..0c61b01be --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/AnnotationTest.java @@ -0,0 +1,93 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.support.annotation.NonNull; +import com.mapbox.mapboxsdk.maps.MapboxMap; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class AnnotationTest { + + @InjectMocks + private MapboxMap mapboxMap = mock(MapboxMap.class); + private Annotation annotation; + @NonNull + private Annotation compare = new Annotation() { + @Override + public long getId() { + return 1; + } + }; + + @Before + public void beforeTest() { + annotation = new Annotation() { + // empty child + }; + } + + @Test + public void testSanity() { + assertNotNull("markerOptions should not be null", annotation); + } + + @Test + public void testRemove() { + annotation.setId(1); + annotation.setMapboxMap(mapboxMap); + annotation.remove(); + verify(mapboxMap, times(1)).removeAnnotation(annotation); + } + + @Test + public void testRemoveUnboundMapboxMap() { + annotation.setId(1); + annotation.remove(); + verify(mapboxMap, times(0)).removeAnnotation(annotation); + } + + @Test + public void testCompareToEqual() { + annotation.setId(1); + assertEquals("conparable equal", 0, annotation.compareTo(compare)); + } + + @Test + public void testCompareToHigher() { + annotation.setId(3); + assertEquals("conparable higher", -1, annotation.compareTo(compare)); + } + + @Test + public void testCompareTolower() { + annotation.setId(0); + assertEquals("conparable lower", 1, annotation.compareTo(compare)); + } + + @Test + public void testEquals() { + Annotation holder = null; + assertFalse(annotation.equals(holder)); + holder = annotation; + assertTrue(annotation.equals(holder)); + assertFalse(annotation.equals(new Object())); + } + + @Test + public void testHashcode() { + int id = 1; + annotation.setId(id); + assertSame("hashcode should match", annotation.hashCode(), id); + } + +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/IconTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/IconTest.java new file mode 100644 index 000000000..e56ca559d --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/IconTest.java @@ -0,0 +1,56 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.graphics.Bitmap; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotSame; +import static org.mockito.Mockito.when; + +public class IconTest { + + @Mock + Bitmap bitmap; + + @Before + public void beforeTest() { + MockitoAnnotations.initMocks(this); + when(bitmap.getConfig()).thenReturn(Bitmap.Config.ARGB_8888); + } + + @Test + public void testId() { + String id = "test"; + Icon icon = IconFactory.recreate(id, Bitmap.createBitmap(0, 0, Bitmap.Config.ALPHA_8)); + assertEquals("id should match", id, icon.getId()); + } + + @Test + public void testBitmap() { + Icon icon = IconFactory.recreate("test", bitmap); + assertEquals("bitmap should match", bitmap, icon.getBitmap()); + } + + @Test + public void testEquals() { + Icon icon1 = IconFactory.recreate("test", bitmap); + Icon icon2 = IconFactory.recreate("test", bitmap); + assertEquals("icons should not match", icon1, icon2); + } + + @Test + public void testEqualsObject() { + Icon icon = IconFactory.recreate("test", Bitmap.createBitmap(0, 0, Bitmap.Config.ALPHA_8)); + assertNotSame("icon should not match", new Object(), icon); + } + + @Test + public void testHashcode() { + Icon icon = IconFactory.recreate("test", bitmap); + long expectedHashcode = 31 * bitmap.hashCode() + "test".hashCode(); + assertEquals("hashcode should match", expectedHashcode, icon.hashCode()); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/InfoWindowTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/InfoWindowTest.java new file mode 100644 index 000000000..94b629860 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/InfoWindowTest.java @@ -0,0 +1,86 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.graphics.PointF; + +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.Projection; + +import org.junit.Test; +import org.mockito.InjectMocks; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class InfoWindowTest { + + @InjectMocks + MapView mMapView = mock(MapView.class); + + @InjectMocks + MapboxMap mMapboxMap = mock(MapboxMap.class); + + @Test + public void testSanity() { + InfoWindow infoWindow = new InfoWindow(mMapView, mMapboxMap); + assertNotNull("infoWindow should exist", infoWindow); + } + + @Test + public void testBoundMarker() { + MarkerOptions markerOptions = new MarkerOptions(); + Marker marker = markerOptions.position(new LatLng()).getMarker(); + InfoWindow infoWindow = new InfoWindow(mMapView, mMapboxMap).setBoundMarker(marker); + assertEquals("marker should match", marker, infoWindow.getBoundMarker()); + } + + @Test + public void testClose() { + InfoWindow infoWindow = new InfoWindow(mMapView, mMapboxMap); + infoWindow.close(); + assertEquals("infowindow should not be visible", false, infoWindow.isVisible()); + } + + + @Test + public void testOpen() { + LatLng latLng = new LatLng(0, 0); + Projection projection = mock(Projection.class); + when(mMapboxMap.getProjection()).thenReturn(projection); + when(projection.toScreenLocation(latLng)).thenReturn(new PointF(0, 0)); + + InfoWindow infoWindow = new InfoWindow(mMapView, mMapboxMap); + infoWindow.open(mMapView, new MarkerOptions().position(new LatLng()).getMarker(), latLng, 0, 0); + assertEquals("infowindow should not be visible", true, infoWindow.isVisible()); + } + + @Test + public void testOpenClose() { + LatLng latLng = new LatLng(0, 0); + Projection projection = mock(Projection.class); + when(mMapboxMap.getProjection()).thenReturn(projection); + when(projection.toScreenLocation(latLng)).thenReturn(new PointF(0, 0)); + + InfoWindow infoWindow = new InfoWindow(mMapView, mMapboxMap); + infoWindow.open(mMapView, new MarkerOptions().position(new LatLng()).getMarker(), latLng, 0, 0); + infoWindow.close(); + assertEquals("infowindow should not be visible", false, infoWindow.isVisible()); + } + + + @Test + public void testUpdate() { + LatLng latLng = new LatLng(0, 0); + Projection projection = mock(Projection.class); + when(mMapboxMap.getProjection()).thenReturn(projection); + when(projection.toScreenLocation(latLng)).thenReturn(new PointF(0, 0)); + + InfoWindow infoWindow = new InfoWindow(mMapView, mMapboxMap); + infoWindow.open(mMapView, new MarkerOptions().position(latLng).getMarker(), latLng, 0, 0); + infoWindow.update(); + } + +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/MarkerTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/MarkerTest.java new file mode 100644 index 000000000..fa571e06b --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/MarkerTest.java @@ -0,0 +1,160 @@ +package com.mapbox.mapboxsdk.annotations; + +import android.graphics.Bitmap; +import android.os.Parcelable; + +import com.mapbox.mapboxsdk.exceptions.InvalidMarkerPositionException; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.utils.MockParcel; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; + +public class MarkerTest { + + @Test + public void testSanity() { + MarkerOptions markerOptions = new MarkerOptions(); + assertNotNull("markerOptions should not be null", markerOptions); + } + + @Test + public void testMarker() { + MarkerOptions markerOptions = new MarkerOptions().position(new LatLng()); + assertNotNull("marker should not be null", markerOptions.getMarker()); + } + + @Test(expected = InvalidMarkerPositionException.class) + public void testInvalidMarker() { + new MarkerOptions().getMarker(); + } + + @Test + public void testPosition() { + MarkerOptions markerOptions = new MarkerOptions().position(new LatLng(10, 12)); + Marker marker = markerOptions.getMarker(); + assertEquals(marker.getPosition(), new LatLng(10, 12)); + assertEquals(markerOptions.getPosition(), new LatLng(10, 12)); + } + + @Test + public void testTitle() { + MarkerOptions markerOptions = new MarkerOptions().title("Mapbox").position(new LatLng()); + Marker marker = markerOptions.getMarker(); + assertEquals(marker.getTitle(), "Mapbox"); + assertEquals(markerOptions.getTitle(), "Mapbox"); + } + + @Test + public void testSnippet() { + MarkerOptions markerOptions = new MarkerOptions().snippet("Mapbox").position(new LatLng()); + Marker marker = markerOptions.getMarker(); + assertEquals(marker.getSnippet(), "Mapbox"); + } + + @Test + public void testBuilder() { + Marker marker = new MarkerOptions().title("title").snippet("snippet").position(new LatLng(10, 12)).getMarker(); + assertEquals(marker.getSnippet(), "snippet"); + + assertEquals(marker.getPosition(), new LatLng(10, 12)); + } + + @Test + public void testIcon() { + Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_4444); + Icon icon = IconFactory.recreate("test", bitmap); + MarkerOptions markerOptions = new MarkerOptions().position(new LatLng()).icon(icon); + Marker marker = markerOptions.getMarker(); + assertEquals("Icon should match", icon, marker.getIcon()); + assertEquals("Icon should match", icon, markerOptions.getIcon()); + } + + @Test + public void testHashCode() { + Marker marker = new MarkerOptions().position(new LatLng()).getMarker(); + assertEquals("hash code should match", marker.hashCode(), 0); + } + + @Test + public void testHashCodeBuilder() { + MarkerOptions markerOptions = new MarkerOptions().position(new LatLng(10, 12)); + assertEquals("hash code should match", markerOptions.hashCode(), 579999617); + } + + @Test + public void testEquals() { + Marker markerOne = new MarkerOptions().position(new LatLng(0, 0)).getMarker(); + Marker markerTwo = new MarkerOptions().position(new LatLng(0, 0)).getMarker(); + assertEquals(markerOne, markerTwo); + } + + @Test + public void testEqualityDifferentLocation() { + MarkerOptions marker = new MarkerOptions().position(new LatLng(0, 0)); + MarkerOptions other = new MarkerOptions().position(new LatLng(1, 0)); + assertNotEquals("Should not match", other, marker); + } + + + @Test + public void testEqualityDifferentSnippet() { + MarkerOptions marker = new MarkerOptions().snippet("s"); + MarkerOptions other = new MarkerOptions(); + assertNotEquals("Should not match", other, marker); + } + + @Test + public void testEqualityDifferentIcon() { + MarkerOptions marker = new MarkerOptions().icon(mock(Icon.class)); + MarkerOptions other = new MarkerOptions(); + assertNotEquals("Should not match", other, marker); + } + + @Test + public void testEqualityDifferentTitle() { + MarkerOptions marker = new MarkerOptions().title("t"); + MarkerOptions other = new MarkerOptions(); + assertNotEquals("Should not match", other, marker); + } + + @Test + public void testEqualsItself() { + MarkerOptions markerOptions = new MarkerOptions().position(new LatLng(0, 0)); + Marker marker = markerOptions.getMarker(); + assertEquals("Marker should match", marker, marker); + assertEquals("MarkerOptions should match", markerOptions, markerOptions); + } + + @Test + public void testNotEquals() { + MarkerOptions markerOptions = new MarkerOptions().position(new LatLng(0, 0)); + Marker marker = markerOptions.getMarker(); + assertNotEquals("MarkerOptions should match", markerOptions, new Object()); + assertNotEquals("Marker should match", marker, new Object()); + } + + @Test + public void testEqualityBuilder() { + MarkerOptions markerOne = new MarkerOptions().position(new LatLng(0, 0)); + MarkerOptions markerTwo = new MarkerOptions().position(new LatLng(0, 0)); + assertEquals(markerOne, markerTwo); + } + + @Test + public void testToString() { + Marker marker = new MarkerOptions().position(new LatLng(0, 0)).getMarker(); + assertEquals(marker.toString(), "Marker [position[" + "LatLng [latitude=0.0, longitude=0.0, altitude=0.0]" + "]]"); + } + + @Test + public void testParcelable() { + MarkerOptions markerOptions = new MarkerOptions().position(new LatLng()).title("t").snippet("s"); + Parcelable parcelable = MockParcel.obtain(markerOptions); + assertEquals("Parcel should match original object", parcelable, markerOptions); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/PolygonTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/PolygonTest.java new file mode 100644 index 000000000..3933c6888 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/PolygonTest.java @@ -0,0 +1,75 @@ +package com.mapbox.mapboxsdk.annotations; + +import com.mapbox.mapboxsdk.geometry.LatLng; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class PolygonTest { + + @Test + public void testSanity() { + PolygonOptions polygonOptions = new PolygonOptions(); + assertNotNull("polygonOptions should not be null", polygonOptions); + } + + @Test + public void testPolygon() { + Polygon polygon = new PolygonOptions().getPolygon(); + assertNotNull("polyline should not be null", polygon); + } + + @Test + public void testAlpha() { + Polygon polygon = new PolygonOptions().alpha(0.5f).getPolygon(); + assertEquals(0.5f, polygon.getAlpha(), 0.0f); + } + + @Test + public void testStrokeColor() { + Polygon polygon = new PolygonOptions().strokeColor(1).getPolygon(); + assertEquals(1, polygon.getStrokeColor()); + } + + @Test + public void testFillColor() { + Polygon polygon = new PolygonOptions().fillColor(1).getPolygon(); + assertEquals(1, polygon.getFillColor()); + } + + @Test + public void testLatLng() { + Polygon polygon = new PolygonOptions().add(new LatLng(0, 0)).getPolygon(); + assertNotNull("points should not be null", polygon.getPoints()); + assertEquals(new LatLng(0, 0), polygon.getPoints().get(0)); + } + + @Test + public void testAddAllLatLng() { + List coordinates = new ArrayList<>(); + coordinates.add(new LatLng(0, 0)); + Polygon polygon = new PolygonOptions().addAll(coordinates).getPolygon(); + assertNotNull(polygon.getPoints()); + assertEquals(new LatLng(0, 0), polygon.getPoints().get(0)); + } + + @Test + public void testBuilder() { + PolylineOptions polylineOptions = new PolylineOptions(); + polylineOptions.width(1.0f); + polylineOptions.color(2); + polylineOptions.add(new LatLng(0, 0)); + + Polyline polyline = polylineOptions.getPolyline(); + assertEquals(1.0f, polyline.getWidth(), 0); + assertEquals(2, polyline.getColor()); + assertNotNull("Points should not be null", polyline.getPoints()); + assertEquals(new LatLng(0, 0), polyline.getPoints().get(0)); + } + +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/PolylineTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/PolylineTest.java new file mode 100644 index 000000000..54bb0e8cf --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/annotations/PolylineTest.java @@ -0,0 +1,75 @@ +package com.mapbox.mapboxsdk.annotations; + +import com.mapbox.mapboxsdk.geometry.LatLng; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class PolylineTest { + + @Test + public void testSanity() { + PolylineOptions polylineOptions = new PolylineOptions(); + assertNotNull("polylineOptions should not be null", polylineOptions); + } + + @Test + public void testPolyline() { + Polyline polyline = new PolylineOptions().getPolyline(); + assertNotNull("polyline should not be null", polyline); + } + + @Test + public void testAlpha() { + Polyline polyline = new PolylineOptions().alpha(0.2f).getPolyline(); + assertEquals(0.2f, polyline.getAlpha(), 0.0f); + } + + @Test + public void testWidth() { + Polyline polyline = new PolylineOptions().width(1).getPolyline(); + assertEquals(1.0f, polyline.getWidth(), 0); + } + + @Test + public void testColor() { + Polyline polyline = new PolylineOptions().color(1).getPolyline(); + assertEquals(1, polyline.getColor()); + } + + @Test + public void testAddLatLng() { + Polyline polyline = new PolylineOptions().add(new LatLng(0, 0)).getPolyline(); + assertNotNull("Points should not be null", polyline.getPoints()); + assertEquals(new LatLng(0, 0), polyline.getPoints().get(0)); + } + + @Test + public void testAddAllLatLng() { + List coordinates = new ArrayList<>(); + coordinates.add(new LatLng(0, 0)); + Polyline polyline = new PolylineOptions().addAll(coordinates).getPolyline(); + assertNotNull(polyline.getPoints()); + assertEquals(new LatLng(0, 0), polyline.getPoints().get(0)); + } + + @Test + public void testBuilder() { + PolylineOptions polylineOptions = new PolylineOptions(); + polylineOptions.width(1.0f); + polylineOptions.color(2); + polylineOptions.add(new LatLng(0, 0)); + + Polyline polyline = polylineOptions.getPolyline(); + assertEquals(1.0f, polyline.getWidth(), 0); + assertEquals(2, polyline.getColor()); + assertNotNull("Points should not be null", polyline.getPoints()); + assertEquals(new LatLng(0, 0), polyline.getPoints().get(0)); + } + +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/attribution/AttributionParseTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/attribution/AttributionParseTest.java new file mode 100644 index 000000000..b72e85270 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/attribution/AttributionParseTest.java @@ -0,0 +1,322 @@ +package com.mapbox.mapboxsdk.attribution; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.Set; + +import static junit.framework.Assert.assertEquals; + +@RunWith(RobolectricTestRunner.class) +public class AttributionParseTest { + + private static final String STREETS_ATTRIBUTION = "© Mapbox © OpenStreetMap Improve this map\n"; + private static final String SATELLITE_ATTRIBUTION = "© Mapbox © OpenStreetMap Improve this map © DigitalGlobe\n"; + + @Test + public void testParseAttributionStringSatellite() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(SATELLITE_ATTRIBUTION) + .build(); + + Set attributionList = attributionParser.getAttributions(); + assertEquals("Size of list should match", 4, attributionList.size()); + + int counter = 0; + for (Attribution attribution : attributionList) { + switch (counter) { + case 0: + assertEquals("URL mapbox should match", "https://www.mapbox.com/about/maps/", attribution.getUrl()); + assertEquals("Title mapbox should match", "© Mapbox", attribution.getTitle()); + break; + case 1: + assertEquals("URL openstreetmap should match", "http://www.openstreetmap.org/about/", attribution.getUrl()); + assertEquals("Title openstreetmap should match", "© OpenStreetMap", attribution.getTitle()); + break; + case 2: + assertEquals("URL improve map should match", "https://apps.mapbox.com/feedback/", attribution.getUrl()); + assertEquals("Title improve map should match", "Improve This Map", attribution.getTitle()); + break; + case 3: + assertEquals("URL digital globe should match", "https://www.digitalglobe.com/", attribution.getUrl()); + assertEquals("Title digital globe should match", "© DigitalGlobe", attribution.getTitle()); + break; + } + counter++; + } + } + + @Test + public void testParseAttributionStringStreets() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(STREETS_ATTRIBUTION) + .build(); + + Set attributionList = attributionParser.getAttributions(); + assertEquals("Size of list should match", 3, attributionList.size()); + + int counter = 0; + for (Attribution attribution : attributionList) { + switch (counter) { + case 0: + assertEquals("URL mapbox should match", "https://www.mapbox.com/about/maps/", attribution.getUrl()); + assertEquals("Title mapbox should match", "© Mapbox", attribution.getTitle()); + break; + case 1: + assertEquals("URL openstreetmap should match", "http://www.openstreetmap.org/about/", attribution.getUrl()); + assertEquals("Title openstreetmap should match", "© OpenStreetMap", attribution.getTitle()); + break; + case 2: + assertEquals("URL improve map should match", "https://apps.mapbox.com/feedback/", attribution.getUrl()); + assertEquals("Title improve map should match", "Improve This Map", attribution.getTitle()); + break; + } + counter++; + } + } + + @Test + public void testParseAttributionWithoutMapbox() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(STREETS_ATTRIBUTION) + .withMapboxAttribution(false) + .build(); + + Set attributionList = attributionParser.getAttributions(); + assertEquals("Size of list should match", 2, attributionList.size()); + + int counter = 0; + for (Attribution attribution : attributionList) { + switch (counter) { + case 0: + assertEquals("URL openstreetmap should match", "http://www.openstreetmap.org/about/", attribution.getUrl()); + assertEquals("Title openstreetmap should match", "© OpenStreetMap", attribution.getTitle()); + break; + case 1: + assertEquals("URL improve map should match", "https://apps.mapbox.com/feedback/", attribution.getUrl()); + assertEquals("Title improve map should match", "Improve This Map", attribution.getTitle()); + break; + } + counter++; + } + } + + @Test + public void testParseAttributionArrayString() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(new String[] {STREETS_ATTRIBUTION, "", SATELLITE_ATTRIBUTION}) + .build(); + Set attributionList = attributionParser.getAttributions(); + assertEquals("Size of list should match", 4, attributionList.size()); + + int counter = 0; + for (Attribution attribution : attributionList) { + switch (counter) { + case 0: + assertEquals("URL mapbox should match", "https://www.mapbox.com/about/maps/", attribution.getUrl()); + assertEquals("Title mapbox should match", "© Mapbox", attribution.getTitle()); + break; + case 1: + assertEquals("URL openstreetmap should match", "http://www.openstreetmap.org/about/", attribution.getUrl()); + assertEquals("Title openstreetmap should match", "© OpenStreetMap", attribution.getTitle()); + break; + case 2: + assertEquals("URL improve map should match", "https://apps.mapbox.com/feedback/", attribution.getUrl()); + assertEquals("Title improve map should match", "Improve This Map", attribution.getTitle()); + break; + case 3: + assertEquals("URL digital globe should match", "https://www.digitalglobe.com/", attribution.getUrl()); + assertEquals("Title digital globe should match", "© DigitalGlobe", attribution.getTitle()); + break; + } + counter++; + } + } + + @Test + public void testHideImproveThisMapAttributionArrayString() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(SATELLITE_ATTRIBUTION) + .withImproveMap(false) + .build(); + Set attributionList = attributionParser.getAttributions(); + assertEquals("Size of list should match", 3, attributionList.size()); + + int counter = 0; + for (Attribution attribution : attributionList) { + switch (counter) { + case 0: + assertEquals("URL mapbox should match", "https://www.mapbox.com/about/maps/", attribution.getUrl()); + assertEquals("Title mapbox should match", "© Mapbox", attribution.getTitle()); + break; + case 1: + assertEquals("URL openstreetmap should match", "http://www.openstreetmap.org/about/", attribution.getUrl()); + assertEquals("Title openstreetmap should match", "© OpenStreetMap", attribution.getTitle()); + break; + case 2: + assertEquals("URL digital globe should match", "https://www.digitalglobe.com/", attribution.getUrl()); + assertEquals("Title digital globe should match", "© DigitalGlobe", attribution.getTitle()); + break; + } + counter++; + } + } + + @Test + public void testParseHideCopyrightAttributionArrayString() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(STREETS_ATTRIBUTION, "", SATELLITE_ATTRIBUTION) + .withCopyrightSign(false) + .build(); + Set attributionList = attributionParser.getAttributions(); + assertEquals("Size of list should match", 4, attributionList.size()); + + int counter = 0; + for (Attribution attribution : attributionList) { + switch (counter) { + case 0: + assertEquals("URL mapbox should match", "https://www.mapbox.com/about/maps/", attribution.getUrl()); + assertEquals("Title mapbox should match", "Mapbox", attribution.getTitle()); + break; + case 1: + assertEquals("URL openstreetmap should match", "http://www.openstreetmap.org/about/", attribution.getUrl()); + assertEquals("Title openstreetmap should match", "OpenStreetMap", attribution.getTitle()); + break; + case 2: + assertEquals("URL improve map should match", "https://apps.mapbox.com/feedback/", attribution.getUrl()); + assertEquals("Title improve map should match", "Improve This Map", attribution.getTitle()); + break; + case 3: + assertEquals("URL digital globe should match", "https://www.digitalglobe.com/", attribution.getUrl()); + assertEquals("Title digital globe should match", "DigitalGlobe", attribution.getTitle()); + break; + } + counter++; + } + } + + @Test + public void testOutputWithoutCopyRightString() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(STREETS_ATTRIBUTION) + .withCopyrightSign(false) + .withImproveMap(false) + .build(); + + assertEquals( + "Attribution string should match", + "© Mapbox / OpenStreetMap", + attributionParser.createAttributionString() + ); + } + + + @Test + public void testOutputWithCopyRightString() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(STREETS_ATTRIBUTION) + .withImproveMap(false) + .build(); + + assertEquals( + "Attribution string should match", + "© Mapbox / © OpenStreetMap", + attributionParser.createAttributionString() + ); + } + + @Test + public void testOutputWithoutCopyRightWithoutMapboxString() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(STREETS_ATTRIBUTION) + .withCopyrightSign(false) + .withImproveMap(false) + .withMapboxAttribution(false) + .build(); + + assertEquals( + "Attribution string should match", + "© OpenStreetMap", + attributionParser.createAttributionString() + ); + } + + @Test + public void testOutputWithCopyRightWithoutMapboxString() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(STREETS_ATTRIBUTION) + .withImproveMap(false) + .withMapboxAttribution(false) + .build(); + + assertEquals( + "Attribution string should match", + "© OpenStreetMap", + attributionParser.createAttributionString() + ); + } + + @Test + public void testOutputSatelliteString() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(STREETS_ATTRIBUTION, SATELLITE_ATTRIBUTION, "blabla", "") + .withImproveMap(false) + .withCopyrightSign(false) + .withMapboxAttribution(false) + .build(); + + assertEquals( + "Attribution string should match", + "© OpenStreetMap / DigitalGlobe", + attributionParser.createAttributionString() + ); + } + + @Test + public void testShortOpenStreetMapString() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(STREETS_ATTRIBUTION, SATELLITE_ATTRIBUTION, "blabla", "") + .withImproveMap(false) + .withCopyrightSign(false) + .withMapboxAttribution(false) + .build(); + + assertEquals( + "Attribution string should match", + "© OSM / DigitalGlobe", + attributionParser.createAttributionString(true) + ); + } + + @Test + public void testShortOpenStreetMapWithoutCopyrightString() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(STREETS_ATTRIBUTION, SATELLITE_ATTRIBUTION, "blabla", "") + .withImproveMap(false) + .withCopyrightSign(false) + .build(); + + assertEquals( + "Attribution string should match", + "© Mapbox / OSM / DigitalGlobe", + attributionParser.createAttributionString(true) + ); + } + + @Test + public void testWithImproveThisMapString() throws Exception { + AttributionParser attributionParser = new AttributionParser.Options(RuntimeEnvironment.application) + .withAttributionData(STREETS_ATTRIBUTION, "blabla", "") + .withImproveMap(true) + .withCopyrightSign(false) + .build(); + + assertEquals( + "Attribution string should match", + "© Mapbox / OSM / Improve This Map", + attributionParser.createAttributionString(true) + ); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/camera/CameraPositionTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/camera/CameraPositionTest.java new file mode 100644 index 000000000..6974705fa --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/camera/CameraPositionTest.java @@ -0,0 +1,120 @@ +package com.mapbox.mapboxsdk.camera; + +import android.content.res.TypedArray; +import android.os.Parcel; + +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.geometry.LatLng; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class CameraPositionTest { + + private static final double DELTA = 1e-15; + + @Test + public void testSanity() { + LatLng latLng = new LatLng(1, 2); + CameraPosition cameraPosition = new CameraPosition(latLng, 3, 4, 5, new double[] {0, 500, 0, 0}); + assertNotNull("cameraPosition should not be null", cameraPosition); + } + + @Test + public void testDefaultTypedArrayBuilder() { + TypedArray typedArray = null; + CameraPosition cameraPosition = new CameraPosition.Builder(typedArray).build(); + assertEquals("bearing should match", -1, cameraPosition.bearing, DELTA); + assertNull("latlng should be null", cameraPosition.target); + assertEquals("tilt should match", -1, cameraPosition.tilt, DELTA); + assertEquals("zoom should match", -1, cameraPosition.zoom, DELTA); + assertNull("padding should be null", cameraPosition.padding); + } + + @Test + public void testTypedArrayBuilder() { + float bearing = 180; + float zoom = 12; + float latitude = 10; + float longitude = 11; + float tilt = 44; + + TypedArray typedArray = mock(TypedArray.class); + when(typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_cameraBearing, 0.0f)).thenReturn(bearing); + when(typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_cameraTargetLat, 0.0f)).thenReturn(latitude); + when(typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_cameraTargetLng, 0.0f)).thenReturn(longitude); + when(typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_cameraZoom, 0.0f)).thenReturn(zoom); + when(typedArray.getFloat(R.styleable.mapbox_MapView_mapbox_cameraTilt, 0.0f)).thenReturn(tilt); + doNothing().when(typedArray).recycle(); + + CameraPosition cameraPosition = new CameraPosition.Builder(typedArray).build(); + assertEquals("bearing should match", bearing, cameraPosition.bearing, DELTA); + assertEquals("latlng should match", new LatLng(latitude, longitude), cameraPosition.target); + assertEquals("tilt should match", tilt, cameraPosition.tilt, DELTA); + assertEquals("zoom should match", zoom, cameraPosition.zoom, DELTA); + } + + @Test + public void testToString() { + LatLng latLng = new LatLng(1, 2); + CameraPosition cameraPosition = new CameraPosition(latLng, 3, 4, 5, new double[] {0, 500, 0, 0}); + assertEquals("toString should match", "Target: LatLng [latitude=1.0, longitude=2.0, altitude=0.0], Zoom:3.0, " + + "Bearing:5.0, Tilt:4.0, Padding:[0.0, 500.0, 0.0, 0.0]", cameraPosition.toString()); + } + + @Test + public void testHashcode() { + LatLng latLng = new LatLng(1, 2); + CameraPosition cameraPosition = new CameraPosition(latLng, 3, 4, 5, new double[] {0, 500, 0, 0}); + assertEquals("hashCode should match", -420915327, cameraPosition.hashCode()); + } + + @Test + public void testZoomUpdateBuilder() { + float zoomLevel = 5; + CameraPosition.Builder builder = new CameraPosition.Builder( + (CameraUpdateFactory.ZoomUpdate) CameraUpdateFactory.zoomTo(zoomLevel)); + assertEquals("zoom should match", zoomLevel, builder.build().zoom, 0); + } + + @Test + public void testEquals() { + LatLng latLng = new LatLng(1, 2); + CameraPosition cameraPosition = new CameraPosition(latLng, 3, 4, 5, new double[] {0, 500, 0, 0}); + CameraPosition cameraPositionBearing = new CameraPosition(latLng, 3, 4, 9, new double[] {0, 500, 0, 0}); + CameraPosition cameraPositionTilt = new CameraPosition(latLng, 3, 9, 5, new double[] {0, 500, 0, 0}); + CameraPosition cameraPositionZoom = new CameraPosition(latLng, 9, 4, 5, new double[] {0, 500, 0, 0}); + CameraPosition cameraPositionTarget = new CameraPosition(new LatLng(), 3, 4, 5, new double[] {0, 500, 0, 0}); + CameraPosition cameraPositionPadding = new CameraPosition(new LatLng(), 3, 4, 5, new double[] {0, 501, 0, 0}); + + assertEquals("cameraPosition should match itself", cameraPosition, cameraPosition); + assertNotEquals("cameraPosition should not match null", null, cameraPosition); + assertNotEquals("cameraPosition should not match object", new Object(), cameraPosition); + assertNotEquals("cameraPosition should not match for bearing", cameraPositionBearing, cameraPosition); + assertNotEquals("cameraPosition should not match for tilt", cameraPositionTilt, cameraPosition); + assertNotEquals("cameraPosition should not match for zoom", cameraPositionZoom, cameraPosition); + assertNotEquals("cameraPosition should not match for target", cameraPositionTarget, cameraPosition); + assertNotEquals("cameraPosition should not match for padding", cameraPositionPadding, cameraPosition); + } + + @Test + public void testParcelable() { + CameraPosition cameraPosition1 = new CameraPosition(new LatLng(1, 2), 3, 4, 5, new double[] {0, 500, 0, 0}); + Parcel parcel = Parcel.obtain(); + cameraPosition1.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + CameraPosition cameraPosition2 = CameraPosition.CREATOR.createFromParcel(parcel); + assertEquals("Parcel should match original object", cameraPosition1, cameraPosition2); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/constants/AppConstant.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/constants/AppConstant.java new file mode 100644 index 000000000..cb654aa55 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/constants/AppConstant.java @@ -0,0 +1,6 @@ +package com.mapbox.mapboxsdk.constants; + +public class AppConstant { + + public static final int STYLE_VERSION = 9; +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java new file mode 100644 index 000000000..6c44c29e3 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java @@ -0,0 +1,813 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.os.Parcelable; +import com.mapbox.mapboxsdk.constants.GeometryConstants; +import com.mapbox.mapboxsdk.exceptions.InvalidLatLngBoundsException; +import com.mapbox.mapboxsdk.utils.MockParcel; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.Assert.assertNotNull; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class LatLngBoundsTest { + + private static final double DELTA = 1e-15; + + private LatLngBounds latLngBounds; + private static final LatLng LAT_LNG_NULL_ISLAND = new LatLng(0, 0); + private static final LatLng LAT_LNG_NOT_NULL_ISLAND = new LatLng(2, 2); + + @Before + public void beforeTest() { + latLngBounds = new LatLngBounds.Builder() + .include(LAT_LNG_NULL_ISLAND) + .include(LAT_LNG_NOT_NULL_ISLAND) + .build(); + } + + @Test + public void testSanity() { + LatLngBounds.Builder latLngBoundsBuilder = new LatLngBounds.Builder(); + latLngBoundsBuilder.include(LAT_LNG_NULL_ISLAND).include(LAT_LNG_NOT_NULL_ISLAND); + assertNotNull("latLng should not be null", latLngBoundsBuilder.build()); + } + + @Test(expected = InvalidLatLngBoundsException.class) + public void noLatLngs() { + new LatLngBounds.Builder().build(); + } + + @Test(expected = InvalidLatLngBoundsException.class) + public void oneLatLngs() { + new LatLngBounds.Builder().include(LAT_LNG_NULL_ISLAND).build(); + } + + @Test + public void latitiudeSpan() { + assertEquals("Span should be the same", 2, latLngBounds.getLatitudeSpan(), DELTA); + } + + @Test + public void longitudeSpan() { + assertEquals("Span should be the same", 2, latLngBounds.getLongitudeSpan(), DELTA); + } + + @Test + public void coordinateSpan() { + LatLngSpan latLngSpan = latLngBounds.getSpan(); + assertEquals("LatLngSpan should be the same", new LatLngSpan(2, 2), latLngSpan); + } + + @Test + public void dateLineSpanBuilder1() { + latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(10, -170)) + .include(new LatLng(-10, 170)) + .build(); + + LatLngSpan latLngSpan = latLngBounds.getSpan(); + assertEquals("LatLngSpan should be the same", new LatLngSpan(20, 340), + latLngSpan); + } + + @Test + public void dateLineSpanBuilder2() { + latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(-10, -170)) + .include(new LatLng(10, 170)) + .build(); + + LatLngSpan latLngSpan = latLngBounds.getSpan(); + assertEquals("LatLngSpan should be the same", new LatLngSpan(20, 340), + latLngSpan); + } + + @Test + public void dateLineSpanFrom1() { + latLngBounds = LatLngBounds.from(10, -170, -10, -190); + LatLngSpan latLngSpan = latLngBounds.getSpan(); + assertEquals("LatLngSpan should be the same", new LatLngSpan(20, 20), + latLngSpan); + } + + @Test + public void dateLineSpanFrom2() { + latLngBounds = LatLngBounds.from(10, 170, -10, -170); + LatLngSpan latLngSpan = latLngBounds.getSpan(); + assertEquals("LatLngSpan should be the same", new LatLngSpan(20, 340), + latLngSpan); + } + + @Test + public void zeroLongitudeSpan() { + latLngBounds = LatLngBounds.from(10, 10, -10, 10); + LatLngSpan latLngSpan = latLngBounds.getSpan(); + assertEquals("LatLngSpan should be shortest distance", new LatLngSpan(20, 0), + latLngSpan); + } + + @Test + public void nearDateLineCenter1() { + latLngBounds = LatLngBounds.from(10, -175, -10, -195); + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(0, -185), center); + } + + @Test + public void nearDateLineCenter2() { + latLngBounds = LatLngBounds.from(10, 195, -10, 175); + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(0, 185), center); + } + + @Test + public void nearDateLineCenter3() { + latLngBounds = LatLngBounds.from(10, -170, -10, -190); + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(0, -180), center); + } + + @Test + public void nearDateLineCenter4() { + latLngBounds = LatLngBounds.from(10, 180, -10, 0); + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(0, 90), center); + } + + @Test + public void nearDateLineCenter5() { + latLngBounds = LatLngBounds.from(10, 180, -10, 0); + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(0, 90), center); + } + + @Test + public void centerForBoundsWithSameLongitude() { + latLngBounds = LatLngBounds.from(10, 10, -10, 10); + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(0, 10), center); + } + + @Test + public void centerForBoundsWithSameLatitude() { + latLngBounds = LatLngBounds.from(10, 10, 10, -10); + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(10, 0), center); + } + + @Test + public void center() { + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(1, 1), center); + } + + @Test + public void notEmptySpan() { + latLngBounds = new LatLngBounds.Builder() + .include(LAT_LNG_NOT_NULL_ISLAND) + .include(LAT_LNG_NULL_ISLAND) + .build(); + assertFalse("Should not be empty", latLngBounds.isEmptySpan()); + } + + @Test + public void includeSameLatLngs() { + latLngBounds = new LatLngBounds.Builder() + .include(LAT_LNG_NOT_NULL_ISLAND) + .include(LAT_LNG_NOT_NULL_ISLAND) + .include(LAT_LNG_NULL_ISLAND) + .include(LAT_LNG_NULL_ISLAND) + .build(); + assertEquals(latLngBounds.getNorthEast(), LAT_LNG_NOT_NULL_ISLAND); + assertEquals(latLngBounds.getSouthWest(), LAT_LNG_NULL_ISLAND); + } + + @Test + public void toLatLngs() { + latLngBounds = new LatLngBounds.Builder() + .include(LAT_LNG_NOT_NULL_ISLAND) + .include(LAT_LNG_NULL_ISLAND) + .build(); + + assertArrayEquals("LatLngs should match", + new LatLng[] {LAT_LNG_NOT_NULL_ISLAND, LAT_LNG_NULL_ISLAND}, + latLngBounds.toLatLngs()); + } + + @Test + public void include() { + assertTrue("LatLng should be included", latLngBounds.contains(new LatLng(1, 1))); + } + + @Test + public void includes() { + List points = new ArrayList<>(); + points.add(LAT_LNG_NULL_ISLAND); + points.add(LAT_LNG_NOT_NULL_ISLAND); + + LatLngBounds latLngBounds1 = new LatLngBounds.Builder() + .includes(points) + .build(); + + LatLngBounds latLngBounds2 = new LatLngBounds.Builder() + .include(LAT_LNG_NULL_ISLAND) + .include(LAT_LNG_NOT_NULL_ISLAND) + .build(); + + assertEquals("LatLngBounds should match", latLngBounds1, latLngBounds2); + } + + @Test + public void includesOrderDoesNotMatter() { + LatLngBounds sameLongitudeFirst = new LatLngBounds.Builder() + .include(new LatLng(50, 10)) // southWest + .include(new LatLng(60, 10)) + .include(new LatLng(60, 20)) // northEast + .include(new LatLng(50, 20)) + .include(new LatLng(50, 10)) // southWest again + .build(); + + LatLngBounds sameLatitudeFirst = new LatLngBounds.Builder() + .include(new LatLng(50, 20)) + .include(new LatLng(50, 10)) // southWest + .include(new LatLng(60, 10)) + .include(new LatLng(60, 20)) // northEast + .include(new LatLng(50, 20)) + .build(); + + assertEquals(sameLatitudeFirst, sameLongitudeFirst); + } + + @Test + public void includesOverDateline1() { + + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(10, -170)) + .include(new LatLng(-10, -175)) + .include(new LatLng(0, -190)) + .build(); + + LatLngSpan latLngSpan = latLngBounds.getSpan(); + assertEquals("LatLngSpan should be the same", + new LatLngSpan(20, 20), latLngSpan); + } + + @Test + public void includesOverDateline2() { + + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(10, 170)) + .include(new LatLng(-10, 175)) + .include(new LatLng(0, 190)) + .build(); + + assertEquals("LatLngSpan should be the same", + new LatLngSpan(20, 20), latLngBounds.getSpan()); + } + + @Test + public void includesOverDateline3() { + + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(10, -190)) + .include(new LatLng(-10, -170)) + .include(new LatLng(0, -180)) + .include(new LatLng(5, -180)) + .build(); + + assertEquals("LatLngSpan should be the same", + new LatLngSpan(20, 20), latLngBounds.getSpan()); + } + + @Test + public void containsNot() { + assertFalse("LatLng should not be included", + latLngBounds.contains(new LatLng(3, 1))); + } + + @Test + public void containsBoundsInWorld() { + assertTrue("LatLngBounds should be contained in the world", + LatLngBounds.world().contains(latLngBounds)); + } + + @Test + public void worldSpan() { + assertEquals("LatLngBounds world span should be 180, 360", + GeometryConstants.LATITUDE_SPAN, LatLngBounds.world().getLatitudeSpan(), DELTA); + assertEquals("LatLngBounds world span should be 180, 360", + GeometryConstants.LONGITUDE_SPAN, LatLngBounds.world().getLongitudeSpan(), DELTA); + } + + @Test + public void emptySpan() { + LatLngBounds latLngBounds = LatLngBounds.from( + GeometryConstants.MIN_LATITUDE, GeometryConstants.MAX_LONGITUDE, + GeometryConstants.MIN_LATITUDE, GeometryConstants.MAX_LONGITUDE); + assertTrue("LatLngBounds empty span", latLngBounds.isEmptySpan()); + } + + @Test + public void containsBounds() { + LatLngBounds inner = new LatLngBounds.Builder() + .include(new LatLng(-5, -5)) + .include(new LatLng(5, 5)) + .build(); + LatLngBounds outer = new LatLngBounds.Builder() + .include(new LatLng(-10, -10)) + .include(new LatLng(10, 10)) + .build(); + assertTrue(outer.contains(inner)); + assertFalse(inner.contains(outer)); + } + + @Test + public void testHashCode() { + assertEquals(2147483647, latLngBounds.hashCode(), -1946419200); + } + + @Test + public void equality() { + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(LAT_LNG_NULL_ISLAND) + .include(LAT_LNG_NOT_NULL_ISLAND) + .build(); + assertEquals("equality should match", this.latLngBounds, latLngBounds); + assertEquals("not equal to a different object type", + this.latLngBounds.equals(LAT_LNG_NOT_NULL_ISLAND), false); + } + + @Test + public void testToString() { + assertEquals(latLngBounds.toString(), "N:2.0; E:2.0; S:0.0; W:0.0"); + } + + @Test + public void intersect() { + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(1, 1)) + .include(LAT_LNG_NULL_ISLAND) + .build(); + assertEquals("intersect should match", latLngBounds, + latLngBounds.intersect(this.latLngBounds.getLatNorth(), + this.latLngBounds.getLonEast(), + this.latLngBounds.getLatSouth(), + this.latLngBounds.getLonWest())); + } + + @Test + public void intersectNot() { + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(10, 10)) + .include(new LatLng(9, 8)) + .build(); + assertNull(latLngBounds.intersect(this.latLngBounds)); + } + + @Test + public void intersectNorthCheck() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must be between -90 and 90"); + LatLngBounds intersectLatLngBounds = + LatLngBounds.from(10, 10, 0, 0) + .intersect(200, 200, 0, 0); + } + + @Test + public void intersectSouthCheck() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must be between -90 and 90"); + LatLngBounds intersectLatLngBounds = + LatLngBounds.from(0, 0, -10, -10) + .intersect(0, 0, -200, -200); + } + + @Test + public void intersectSouthLessThanNorthCheck() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latNorth cannot be less than latSouth"); + LatLngBounds intersectLatLngBounds = + LatLngBounds.from(10, 10, 0, 0) + .intersect(0, 200, 20, 0); + } + + @Test + public void intersectEastLessThanWestCheck() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("lonEast cannot be less than lonWest"); + LatLngBounds intersectLatLngBounds = + LatLngBounds.from(10, -10, 0, 0) + .intersect(0, 200, 20, 0); + } + + public void intersectEastDoesNotWrapCheck() { + + LatLngBounds latLngBounds1 = LatLngBounds.from(10, 210, 0, 0); + LatLngBounds latLngBounds2 = LatLngBounds.from(90, 200, 0, 0); + + LatLngBounds intersectLatLngBounds = LatLngBounds.from(10, 200, 0, 0); + + assertEquals(latLngBounds1.intersect(latLngBounds2), intersectLatLngBounds); + assertEquals(latLngBounds2.intersect(latLngBounds1), intersectLatLngBounds); + } + + @Test + public void intersectDoesNotWestWrapCheck() { + LatLngBounds latLngBounds1 = LatLngBounds.from(0, 0, -10, -210); + LatLngBounds latLngBounds2 = LatLngBounds.from(0, 0, -90, -200); + + LatLngBounds intersectLatLngBounds = LatLngBounds.from(0, 0, -10, -200); + + assertEquals(latLngBounds1.intersect(latLngBounds2), intersectLatLngBounds); + assertEquals(latLngBounds2.intersect(latLngBounds1), intersectLatLngBounds); + } + + @Test + public void innerUnion() { + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(1, 1)) + .include(LAT_LNG_NULL_ISLAND) + .build(); + assertEquals("union should match", latLngBounds, latLngBounds.intersect(this.latLngBounds)); + } + + @Test + public void outerUnion() { + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(10, 10)) + .include(new LatLng(9, 8)) + .build(); + assertEquals("outer union should match", + latLngBounds.union(this.latLngBounds), + new LatLngBounds.Builder() + .include(new LatLng(10, 10)) + .include(LAT_LNG_NULL_ISLAND) + .build()); + } + + @Test + public void unionOverDateLine() { + LatLngBounds latLngBounds1 = new LatLngBounds.Builder() + .include(new LatLng(10, 170)) + .include(new LatLng(0, 160)) + .build(); + + LatLngBounds latLngBounds2 = new LatLngBounds.Builder() + .include(new LatLng(0, 190)) + .include(new LatLng(-10, 200)) + .build(); + + LatLngBounds union1 = latLngBounds1.union(latLngBounds2); + LatLngBounds union2 = latLngBounds2.union(latLngBounds1); + + assertEquals(union1, + new LatLngBounds.Builder() + .include(new LatLng(10, 160)) + .include(new LatLng(-10, 200)) + .build()); + + assertEquals(union1, union2); + } + + @Test + public void unionOverDateLine2() { + LatLngBounds latLngBounds1 = new LatLngBounds.Builder() + .include(new LatLng(10, 170)) + .include(new LatLng(0, 160)) + .build(); + + LatLngBounds latLngBounds2 = new LatLngBounds.Builder() + .include(new LatLng(0, 165)) + .include(new LatLng(-10, 200)) + .build(); + + LatLngBounds union1 = latLngBounds1.union(latLngBounds2); + LatLngBounds union2 = latLngBounds2.union(latLngBounds1); + + assertEquals(union1, + new LatLngBounds.Builder() + .include(new LatLng(10, 160)) + .include(new LatLng(-10, 200)) + .build()); + + assertEquals(union1, union2); + } + + @Test + public void unionOverDateLine3() { + LatLngBounds latLngBounds1 = new LatLngBounds.Builder() + .include(new LatLng(10, 195)) + .include(new LatLng(0, 160)) + .build(); + + LatLngBounds latLngBounds2 = new LatLngBounds.Builder() + .include(new LatLng(0, 190)) + .include(new LatLng(-10, 200)) + .build(); + + LatLngBounds union1 = latLngBounds1.union(latLngBounds2); + LatLngBounds union2 = latLngBounds2.union(latLngBounds1); + + assertEquals(union1, + new LatLngBounds.Builder() + .include(new LatLng(10, 160)) + .include(new LatLng(-10, 200)) + .build()); + + assertEquals(union1, union2); + } + + @Test + public void unionOverDateLine4() { + LatLngBounds latLngBounds1 = new LatLngBounds.Builder() + .include(new LatLng(10, -160)) + .include(new LatLng(0, -200)) + .build(); + + LatLngBounds latLngBounds2 = new LatLngBounds.Builder() + .include(new LatLng(0, -170)) + .include(new LatLng(-10, -175)) + .build(); + + LatLngBounds union1 = latLngBounds1.union(latLngBounds2); + LatLngBounds union2 = latLngBounds2.union(latLngBounds1); + + assertEquals(union1, + new LatLngBounds.Builder() + .include(new LatLng(10, -200)) + .include(new LatLng(-10, -160)) + .build()); + + assertEquals(union1, union2); + } + + @Test + public void unionOverDateLine5() { + LatLngBounds latLngBounds1 = new LatLngBounds.Builder() + .include(new LatLng(10, 200)) + .include(new LatLng(0, 160)) + .build(); + + LatLngBounds latLngBounds2 = new LatLngBounds.Builder() + .include(new LatLng(0, 170)) + .include(new LatLng(-10, 175)) + .build(); + + LatLngBounds union1 = latLngBounds1.union(latLngBounds2); + LatLngBounds union2 = latLngBounds2.union(latLngBounds1); + + assertEquals(union1, + new LatLngBounds.Builder() + .include(new LatLng(10, 160)) + .include(new LatLng(-10, 200)) + .build()); + + assertEquals(union1, union2); + } + + @Test + public void unionOverDateLineReturnLongerThanWorldLonSpan() { + LatLngBounds latLngBounds1 = LatLngBounds.from(10, 200, -10, -10); + LatLngBounds latLngBounds2 = LatLngBounds.from(10, 10, -10, -200); + + LatLngBounds union1 = latLngBounds1.union(latLngBounds2); + LatLngBounds union2 = latLngBounds2.union(latLngBounds1); + + assertEquals(union1, union2); + assertEquals(union1, LatLngBounds.from(10, 200, -10, -200)); + } + + @Test + public void unionNorthCheck() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must be between -90 and 90"); + LatLngBounds unionLatLngBounds = + LatLngBounds.from(10, 10, 0, 0) + .union(200, 200, 0, 0); + } + + @Test + public void unionSouthCheck() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must be between -90 and 90"); + LatLngBounds unionLatLngBounds = + LatLngBounds.from(0, 0, -10, -10) + .union(0, 0, -200, -200); + } + + @Test + public void unionSouthLessThanNorthCheck() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latNorth cannot be less than latSouth"); + + LatLngBounds unionLatLngBounds = + LatLngBounds.from(10, 10, 0, 0) + .union(0, 200, 20, 0); + } + + + @Test + public void unionEastDoesNotWrapCheck() { + + LatLngBounds latLngBounds1 = LatLngBounds.from(10, 10, 0, 0); + LatLngBounds latLngBounds2 = LatLngBounds.from(90, 200, 0, 0); + LatLngBounds unionLatLngBounds = LatLngBounds.from(90, 200, 0, 0); + + assertEquals(latLngBounds1.union(latLngBounds2), unionLatLngBounds); + assertEquals(latLngBounds2.union(latLngBounds1), unionLatLngBounds); + } + + @Test + public void unionWestDoesNotWrapCheck() { + LatLngBounds latLngBounds1 = LatLngBounds.from(0, 0, -10, -10); + LatLngBounds latLngBounds2 = LatLngBounds.from(0, 0, -90, -200); + + LatLngBounds unionLatLngBounds = LatLngBounds.from(0, 0, -90, -200); + + assertEquals(latLngBounds1.union(latLngBounds2), unionLatLngBounds); + assertEquals(latLngBounds2.union(latLngBounds1), unionLatLngBounds); + } + + @Test + public void northWest() { + double minLat = 5; + double minLon = 6; + double maxLat = 20; + double maxLon = 21; + + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(minLat, minLon)) + .include(new LatLng(maxLat, maxLon)) + .build(); + + assertEquals("NorthWest should match", latLngBounds.getNorthWest(), new LatLng(maxLat, minLon)); + } + + @Test + public void southWest() { + double minLat = 5; + double minLon = 6; + double maxLat = 20; + double maxLon = 21; + + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(minLat, minLon)) + .include(new LatLng(maxLat, maxLon)) + .build(); + + assertEquals("SouthWest should match", latLngBounds.getSouthWest(), new LatLng(minLat, minLon)); + } + + @Test + public void northEast() { + double minLat = 5; + double minLon = 6; + double maxLat = 20; + double maxLon = 21; + + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(minLat, minLon)) + .include(new LatLng(maxLat, maxLon)) + .build(); + + assertEquals("NorthEast should match", latLngBounds.getNorthEast(), new LatLng(maxLat, maxLon)); + } + + @Test + public void southEast() { + double minLat = 5; + double minLon = 6; + double maxLat = 20; + double maxLon = 21; + + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(minLat, minLon)) + .include(new LatLng(maxLat, maxLon)) + .build(); + + assertEquals("SouthEast should match", latLngBounds.getSouthEast(), new LatLng(minLat, maxLon)); + } + + @Test + public void testParcelable() { + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(10, 10)) + .include(new LatLng(9, 8)) + .build(); + Parcelable parcel = MockParcel.obtain(latLngBounds); + assertEquals("Parcel should match original object", parcel, latLngBounds); + } + + @Test + public void fromTileID() { + LatLngBounds bounds = LatLngBounds.from(0, 0, 0); + assertEquals(GeometryConstants.MIN_WRAP_LONGITUDE, bounds.getLonWest(), DELTA); + assertEquals(GeometryConstants.MIN_MERCATOR_LATITUDE, bounds.getLatSouth(), DELTA); + assertEquals(GeometryConstants.MAX_WRAP_LONGITUDE, bounds.getLonEast(), DELTA); + assertEquals(GeometryConstants.MAX_MERCATOR_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); + + } + + @Rule + public final ExpectedException exception = ExpectedException.none(); + + @Test + public void testConstructorChecksNorthLatitudeNaN() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must not be NaN"); + LatLngBounds.from(Double.NaN, 0, -20, -20); + } + + @Test + public void testConstructorChecksEastLongitudeNaN() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("longitude must not be NaN"); + LatLngBounds.from(0, Double.NaN, -20, -20); + } + + @Test + public void testConstructorChecksNorthLatitudeGreaterThan90() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must be between -90 and 90"); + LatLngBounds.from(95, 0, -20, -20); + } + + @Test + public void testConstructorChecksNorthLatitudeLessThanThanNegative90() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must be between -90 and 90"); + LatLngBounds.from(-95, 0, -20, -20); + } + + @Test + public void testConstructorChecksEastLongitudeInfinity() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("longitude must not be infinite"); + LatLngBounds.from(0, Double.POSITIVE_INFINITY, -20, -20); + } + + @Test + public void testConstructorChecksSouthLatitudeNaN() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must not be NaN"); + LatLngBounds.from(20, 20, Double.NaN, 0); + } + + @Test + public void testConstructorChecksWesttLongitudeNaN() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("longitude must not be NaN"); + LatLngBounds.from(20, 20, 0, Double.NaN); + } + + @Test + public void testConstructorChecksSouthLatitudeGreaterThan90() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must be between -90 and 90"); + LatLngBounds.from(20, 20, 95, 0); + } + + @Test + public void testConstructorChecksSouthLatitudeLessThanThanNegative90() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must be between -90 and 90"); + LatLngBounds.from(20, 20, -95, 0); + } + + @Test + public void testConstructorChecksWestLongitudeInfinity() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("longitude must not be infinite"); + LatLngBounds.from(20, 20, 0, Double.POSITIVE_INFINITY); + } + + @Test + public void testConstructorCheckLatSouthGreaterLatNorth() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latNorth cannot be less than latSouth"); + LatLngBounds.from(0, 20, 20, 0); + } + + @Test + public void testConstructorCheckLonWestGreaterLonEast() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("lonEast cannot be less than lonWest"); + LatLngBounds.from(20, 0, 0, 20); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngSpanTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngSpanTest.java new file mode 100644 index 000000000..12297247c --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngSpanTest.java @@ -0,0 +1,65 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.os.Parcelable; + +import com.mapbox.mapboxsdk.utils.MockParcel; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class LatLngSpanTest { + + private static final double DELTA = 1e-15; + private static final LatLng LAT_LNG_NULL_ISLAND = new LatLng(0, 0); + + @Test + public void testSanity() { + LatLngSpan latLngSpan = new LatLngSpan(0.0, 0.0); + assertNotNull("latLngSpan should not be null", latLngSpan); + } + + @Test + public void testEquality() { + LatLngSpan latLngSpan = new LatLngSpan(0.0, 0.0); + assertEquals("latLngSpan is not equal to a LatLng", latLngSpan.equals(LAT_LNG_NULL_ISLAND), false); + } + + @Test + public void testLatitudeConstructor() { + double latitude = 1.23; + LatLngSpan latLngSpan = new LatLngSpan(latitude, 0.0); + assertEquals("latitude in constructor", latLngSpan.getLatitudeSpan(), latitude, DELTA); + } + + @Test + public void testLongitudeConstructor() { + double longitude = 1.23; + LatLngSpan latLngSpan = new LatLngSpan(0.0, longitude); + assertEquals("latitude in constructor", latLngSpan.getLongitudeSpan(), longitude, DELTA); + } + + @Test + public void testLatitudeMethod() { + double latitude = 1.23; + LatLngSpan latLngSpan = new LatLngSpan(0.0, 0.0); + latLngSpan.setLatitudeSpan(latitude); + assertEquals("latitude in constructor", latLngSpan.getLatitudeSpan(), latitude, DELTA); + } + + @Test + public void testLongitudeMethod() { + double longitude = 1.23; + LatLngSpan latLngSpan = new LatLngSpan(0.0, 0.0); + latLngSpan.setLongitudeSpan(longitude); + assertEquals("latitude in constructor", latLngSpan.getLongitudeSpan(), longitude, DELTA); + } + + @Test + public void testParcelable() { + LatLngSpan object = new LatLngSpan(1, 2); + Parcelable parcel = MockParcel.obtain(object); + assertEquals("parcel should match initial object", object, parcel); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngTest.java new file mode 100644 index 000000000..862c56a52 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngTest.java @@ -0,0 +1,286 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.location.Location; +import android.os.Parcelable; + +import com.mapbox.mapboxsdk.utils.MockParcel; + +import org.junit.Test; +import org.junit.Rule; +import org.junit.rules.ExpectedException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class LatLngTest { + + private static final double DELTA = 1e-15; + + @Test + public void testSanity() { + LatLng latLng = new LatLng(0.0, 0.0); + assertNotNull("latLng should not be null", latLng); + } + + @Test + public void testLatitudeEmptyConstructor() { + LatLng latLng = new LatLng(); + assertEquals("latitude default value", latLng.getLatitude(), 0, DELTA); + } + + @Test + public void testLongitudeEmptyConstructor() { + LatLng latLng = new LatLng(); + assertEquals("longitude default value", latLng.getLongitude(), 0, DELTA); + } + + @Test + public void testAltitudeEmptyConstructor() { + LatLng latLng1 = new LatLng(); + assertEquals("altitude default value", latLng1.getAltitude(), 0.0, DELTA); + } + + @Test + public void testLatitudeConstructor() { + double latitude = 1.2; + LatLng latLng = new LatLng(latitude, 3.4); + assertEquals("latitude should match", latLng.getLatitude(), latitude, DELTA); + } + + @Test + public void testLongitudeConstructor() { + double longitude = 3.4; + LatLng latLng = new LatLng(1.2, longitude); + assertEquals("longitude should match", latLng.getLongitude(), longitude, DELTA); + } + + @Test + public void testAltitudeConstructor() { + LatLng latLng1 = new LatLng(1.2, 3.4); + assertEquals("altitude default value", latLng1.getAltitude(), 0.0, DELTA); + + double altitude = 5.6; + LatLng latLng2 = new LatLng(1.2, 3.4, altitude); + assertEquals("altitude default value", latLng2.getAltitude(), altitude, DELTA); + } + + @Test + public void testLatitudeSetter() { + LatLng latLng = new LatLng(1.2, 3.4); + latLng.setLatitude(3); + assertEquals("latitude should match", 3, latLng.getLatitude(), DELTA); + } + + @Test + public void testLongitudeSetter() { + LatLng latLng = new LatLng(1.2, 3.4); + latLng.setLongitude(3); + assertEquals("longitude should match", 3, latLng.getLongitude(), DELTA); + } + + @Rule + public final ExpectedException exception = ExpectedException.none(); + + @Test + public void testConstructorChecksLatitudeNaN() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must not be NaN"); + new LatLng(Double.NaN, 0); + } + + @Test + public void testConstructorChecksLongitudeNaN() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("longitude must not be NaN"); + new LatLng(0, Double.NaN); + } + + @Test + public void testConstructorChecksLatitudeGreaterThan90() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must be between -90 and 90"); + new LatLng(95, 0); + } + + @Test + public void testConstructorChecksLatitudeLessThanThanNegative90() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must be between -90 and 90"); + new LatLng(-95, 0); + } + + @Test + public void testConstructorChecksLongitudeInfinity() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("longitude must not be infinite"); + new LatLng(0, Double.POSITIVE_INFINITY); + } + + @Test + public void testLatitudeSetterChecksNaN() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must not be NaN"); + new LatLng().setLatitude(Double.NaN); + } + + @Test + public void testLongitudeSetterChecksNaN() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("longitude must not be NaN"); + new LatLng().setLongitude(Double.NaN); + } + + @Test + public void testLatitudeSetterChecksGreaterThan90() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must be between -90 and 90"); + new LatLng().setLatitude(95); + } + + @Test + public void testLatitudeSetterChecksLessThanThanNegative90() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("latitude must be between -90 and 90"); + new LatLng().setLatitude(-95); + } + + @Test + public void testLongitudeSetterChecksInfinity() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("longitude must not be infinite"); + new LatLng().setLongitude(Double.NEGATIVE_INFINITY); + } + + @Test + public void testAltitudeSetter() { + LatLng latLng = new LatLng(1.2, 3.4); + latLng.setAltitude(3); + assertEquals("altitude should match", 3, latLng.getAltitude(), DELTA); + } + + @Test + public void testLatLngConstructor() { + LatLng latLng1 = new LatLng(1.2, 3.4); + LatLng latLng2 = new LatLng(latLng1); + assertEquals("latLng should match", latLng1, latLng2); + } + + @Test + public void testDistanceTo() { + LatLng latLng1 = new LatLng(0.0, 0.0); + LatLng latLng2 = new LatLng(1.0, 1.0); + assertEquals("distances should match", + latLng1.distanceTo(latLng2), + 157298.7453847275, DELTA); + } + + @Test + public void testDistanceToSamePoint() { + LatLng latLng1 = new LatLng(40.71199035644531, -74.0081); + LatLng latLng2 = new LatLng(40.71199035644531, -74.0081); + double distance = latLng1.distanceTo(latLng2); + assertEquals("distance should match", 0.0, distance, DELTA); + } + + // Regression test for #14216 + @Test + public void testDistanceToClosePointNotNaN() { + LatLng latLng = new LatLng(40.00599, -105.29261); + LatLng other = new LatLng(40.005990000000025, -105.29260999999997); + double distance = latLng.distanceTo(other); + assertNotEquals(distance, Double.NaN); + } + + @Test + public void testLocationProvider() { + double latitude = 1.2; + double longitude = 3.4; + double altitude = 5.6; + + // Mock the location class + Location locationMocked = mock(Location.class); + when(locationMocked.getLatitude()).thenReturn(latitude); + when(locationMocked.getLongitude()).thenReturn(longitude); + when(locationMocked.getAltitude()).thenReturn(altitude); + + // Test the constructor + LatLng latLng = new LatLng(locationMocked); + assertEquals("latitude should match", latLng.getLatitude(), latitude, DELTA); + assertEquals("longitude should match", latLng.getLongitude(), longitude, DELTA); + assertEquals("altitude should match", latLng.getAltitude(), altitude, DELTA); + } + + @Test + public void testHashCode() { + double latitude = 1.2; + double longitude = 3.4; + double altitude = 5.6; + LatLng latLng = new LatLng(latitude, longitude, altitude); + assertEquals("hash code should match", latLng.hashCode(), -151519232); + } + + @Test + public void testToString() { + double latitude = 1.2; + double longitude = 3.4; + double altitude = 5.6; + LatLng latLng = new LatLng(latitude, longitude, altitude); + assertEquals("string should match", + latLng.toString(), + "LatLng [latitude=1.2, longitude=3.4, altitude=5.6]"); + } + + @Test + public void testEqualsOther() { + double latitude = 1.2; + double longitude = 3.4; + double altitude = 5.6; + LatLng latLng1 = new LatLng(latitude, longitude, altitude); + LatLng latLng2 = new LatLng(latitude, longitude, altitude); + assertEquals("LatLng should match", latLng1, latLng2); + } + + @Test + public void testEqualsItself() { + LatLng latLng = new LatLng(1, 2, 3); + assertEquals("LatLng should match", latLng, latLng); + } + + @Test + public void testNotEquals() { + LatLng latLng = new LatLng(1, 2); + assertNotEquals("LatLng should match", latLng, new Object()); + } + + @Test + public void testParcelable() { + LatLng latLng = new LatLng(45.0, -185.0); + Parcelable parcel = MockParcel.obtain(latLng); + assertEquals("parcel should match initial object", latLng, parcel); + } + + @Test + public void testWrapped() { + LatLng originalLatLng = new LatLng(45.0, -185.0); + LatLng newLatlng = originalLatLng.wrap(); + assertNotSame(" new wrapped LatLng is created", originalLatLng, newLatlng); + assertEquals("longitude wrapped value", originalLatLng.getLongitude(), -185.0, DELTA); + assertEquals("longitude wrapped value", newLatlng.getLongitude(), 175.0, DELTA); + + newLatlng = new LatLng(45.0, 180.0).wrap(); + assertEquals("longitude wrapped max value", newLatlng.getLongitude(), 180.0, DELTA); + + newLatlng = new LatLng(45.0, -180.0).wrap(); + assertEquals("longitude wrapped min value", newLatlng.getLongitude(), -180.0, DELTA); + } + + @Test + public void testUnnecessaryWrapped() { + LatLng latLng = new LatLng(45.0, 50.0).wrap(); + assertEquals("longitude wrapped value", latLng.getLongitude(), 50.0, DELTA); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/ProjectedMetersTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/ProjectedMetersTest.java new file mode 100644 index 000000000..00fd125a1 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/ProjectedMetersTest.java @@ -0,0 +1,66 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.os.Parcelable; + +import com.mapbox.mapboxsdk.utils.MockParcel; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class ProjectedMetersTest { + + private static final LatLng LAT_LNG_NULL_ISLAND = new LatLng(0, 0); + + @Test + public void testSanity() { + ProjectedMeters projectedMeters = new ProjectedMeters(0.0, 0.0); + assertNotNull("projectedMeters should not be null", projectedMeters); + } + + @Test + public void testEquality() { + ProjectedMeters projectedMeters = new ProjectedMeters(0.0, 0.0); + assertEquals("projectedMeters is not equal to a LatLng", projectedMeters.equals(LAT_LNG_NULL_ISLAND), false); + assertEquals("projectedMeters is equal to itself", projectedMeters.equals(projectedMeters), true); + } + + @Test + public void testNorthing() { + ProjectedMeters projectedMeters = new ProjectedMeters(1.0, 0.0); + assertEquals("northing should be 1", 1, projectedMeters.getNorthing(), 0); + } + + @Test + public void testEasting() { + ProjectedMeters projectedMeters = new ProjectedMeters(0.0, 1.0); + assertEquals("easting should be 1", 1, projectedMeters.getEasting(), 0); + } + + @Test + public void testConstructor() { + ProjectedMeters projectedMeters1 = new ProjectedMeters(1, 2); + ProjectedMeters projectedMeters2 = new ProjectedMeters(projectedMeters1); + assertEquals("projectedmeters should match", projectedMeters1, projectedMeters2); + } + + @Test + public void testHashcode() { + ProjectedMeters meters = new ProjectedMeters(1, 2); + assertEquals("hashcode should match", -1048576, meters.hashCode()); + } + + @Test + public void testToString() { + ProjectedMeters meters = new ProjectedMeters(1, 1); + assertEquals("toString should match", "ProjectedMeters [northing=1.0, easting=1.0]", meters.toString()); + } + + @Test + public void testParcelable() { + ProjectedMeters meters = new ProjectedMeters(1, 1); + Parcelable parcel = MockParcel.obtain(meters); + assertEquals("parcel should match initial object", meters, parcel); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/VisibleRegionTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/VisibleRegionTest.java new file mode 100644 index 000000000..12b779de5 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/VisibleRegionTest.java @@ -0,0 +1,95 @@ +package com.mapbox.mapboxsdk.geometry; + +import android.os.Parcelable; + +import com.mapbox.mapboxsdk.utils.MockParcel; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class VisibleRegionTest { + + private static final LatLng FAR_LEFT = new LatLng(52, -12); + private static final LatLng NEAR_LEFT = new LatLng(34, -12); + private static final LatLng FAR_RIGHT = new LatLng(52, 26); + private static final LatLng NEAR_RIGHT = new LatLng(34, 26); + private static final LatLngBounds BOUNDS = + new LatLngBounds.Builder().include(FAR_LEFT).include(FAR_RIGHT).include(NEAR_LEFT).include(NEAR_RIGHT).build(); + + @Test + public void testSanity() { + VisibleRegion region = new VisibleRegion(FAR_LEFT, FAR_RIGHT, NEAR_LEFT, NEAR_RIGHT, BOUNDS); + assertNotNull("region should not be null", region); + } + + @Test + public void testEquality() { + VisibleRegion region = new VisibleRegion(FAR_LEFT, FAR_RIGHT, NEAR_LEFT, NEAR_RIGHT, BOUNDS); + assertEquals("visibleRegion is not equal to a LatLng", region.equals(FAR_LEFT), false); + assertEquals("visibleRegion is equal to itself", region.equals(region), true); + } + + @Test + public void testFarLeftConstructor() { + VisibleRegion region = new VisibleRegion(FAR_LEFT, FAR_RIGHT, NEAR_LEFT, NEAR_RIGHT, BOUNDS); + assertEquals("LatLng should match", region.farLeft, FAR_LEFT); + } + + @Test + public void testNearLeftConstructor() { + VisibleRegion region = new VisibleRegion(FAR_LEFT, FAR_RIGHT, NEAR_LEFT, NEAR_RIGHT, BOUNDS); + assertEquals("LatLng should match", region.nearLeft, NEAR_LEFT); + } + + @Test + public void testFarRightConstructor() { + VisibleRegion region = new VisibleRegion(FAR_LEFT, FAR_RIGHT, NEAR_LEFT, NEAR_RIGHT, BOUNDS); + assertEquals("LatLng should match", region.farRight, FAR_RIGHT); + } + + @Test + public void testNearRightConstructor() { + VisibleRegion region = new VisibleRegion(FAR_LEFT, FAR_RIGHT, NEAR_LEFT, NEAR_RIGHT, BOUNDS); + assertEquals("LatLng should match", region.nearRight, NEAR_RIGHT); + } + + @Test + public void testLatLngBoundsConstructor() { + VisibleRegion region = new VisibleRegion(FAR_LEFT, FAR_RIGHT, NEAR_LEFT, NEAR_RIGHT, BOUNDS); + assertEquals("LatLngBounds should match", region.latLngBounds, BOUNDS); + } + + @Test + public void testEquals() { + VisibleRegion regionLeft = new VisibleRegion(FAR_LEFT, FAR_RIGHT, NEAR_LEFT, NEAR_RIGHT, BOUNDS); + VisibleRegion regionRight = new VisibleRegion(FAR_LEFT, FAR_RIGHT, NEAR_LEFT, NEAR_RIGHT, BOUNDS); + assertEquals("VisibleRegions should match", regionLeft, regionRight); + } + + @Test + public void testHashcode() { + VisibleRegion region = new VisibleRegion(FAR_LEFT, FAR_RIGHT, NEAR_LEFT, NEAR_RIGHT, BOUNDS); + assertEquals("hashcode should match", -923534102, region.hashCode()); + } + + @Test + public void testToString() { + VisibleRegion region = new VisibleRegion(FAR_LEFT, FAR_RIGHT, NEAR_LEFT, NEAR_RIGHT, BOUNDS); + assertEquals("string should match", + "[farLeft [LatLng [latitude=52.0, longitude=-12.0, altitude=0.0]], " + + "farRight [LatLng [latitude=52.0, longitude=26.0, altitude=0.0]], " + + "nearLeft [LatLng [latitude=34.0, longitude=-12.0, altitude=0.0]], " + + "nearRight [LatLng [latitude=34.0, longitude=26.0, altitude=0.0]], " + + "latLngBounds [N:52.0; E:26.0; S:34.0; W:-12.0]]", + region.toString()); + } + + @Test + public void testParcelable() { + VisibleRegion region = new VisibleRegion(FAR_LEFT, FAR_RIGHT, NEAR_LEFT, NEAR_RIGHT, BOUNDS); + Parcelable parcel = MockParcel.obtain(region); + assertEquals("parcel should match initial object", region, parcel); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/CompassEngineTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/CompassEngineTest.java new file mode 100644 index 000000000..bebd82811 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/CompassEngineTest.java @@ -0,0 +1,89 @@ +package com.mapbox.mapboxsdk.location; + +import android.hardware.Sensor; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.view.WindowManager; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static com.mapbox.mapboxsdk.location.LocationComponentCompassEngine.SENSOR_DELAY_MICROS; +import static junit.framework.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CompassEngineTest { + + private LocationComponentCompassEngine compassEngine; + + @Mock + private WindowManager windowManager; + + @Mock + private SensorManager sensorManager; + + @Mock + private Sensor compassSensor; + + @Mock + private CompassListener compassListener; + + @Before + public void setUp() throws Exception { + when(sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)).thenReturn(compassSensor); + compassEngine = new LocationComponentCompassEngine(windowManager, sensorManager); + } + + @Test + public void lastKnownCompassBearingAccuracyDefault() { + assertEquals("Last accuracy should match", compassEngine.getLastAccuracySensorStatus(), 0); + } + + @Test + public void lastKnownCompassAccuracyStatusValue() { + Sensor sensor = mock(Sensor.class); + compassEngine.onAccuracyChanged(sensor, 2); + assertEquals("Last accuracy should match", compassEngine.getLastAccuracySensorStatus(), 2); + } + + @Test + public void whenGyroscopeIsNull_fallbackToGravity() { + SensorManager sensorManager = mock(SensorManager.class); + when(sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)).thenReturn(null); + new LocationComponentCompassEngine(windowManager, sensorManager); + + verify(sensorManager, times(1)).getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + + @Test + public void whenGyroscopeIsNull_fallbackToMagneticField() { + SensorManager sensorManager = mock(SensorManager.class); + when(sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)).thenReturn(null); + new LocationComponentCompassEngine(windowManager, sensorManager); + + verify(sensorManager, times(1)).getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + } + + @Test + public void listener_registerOnAdd() { + compassEngine.addCompassListener(compassListener); + verify(sensorManager) + .registerListener(any(SensorEventListener.class), eq(compassSensor), eq(SENSOR_DELAY_MICROS)); + } + + @Test + public void listener_unregisterOnRemove() { + compassEngine.addCompassListener(compassListener); + compassEngine.removeCompassListener(compassListener); + verify(sensorManager).unregisterListener(any(SensorEventListener.class), eq(compassSensor)); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt new file mode 100644 index 000000000..1b927d213 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt @@ -0,0 +1,532 @@ +package com.mapbox.mapboxsdk.location + +import android.animation.Animator +import android.location.Location +import android.util.SparseArray +import android.view.animation.LinearInterpolator +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.geometry.LatLng +import com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_TRACKING_TILT_ANIM_DURATION +import com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_TRACKING_ZOOM_ANIM_DURATION +import com.mapbox.mapboxsdk.location.MapboxAnimator.* +import com.mapbox.mapboxsdk.location.modes.CameraMode +import com.mapbox.mapboxsdk.location.modes.RenderMode +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.Projection +import io.mockk.* +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class LocationAnimatorCoordinatorTest { + + private lateinit var locationAnimatorCoordinator: LocationAnimatorCoordinator + private val cameraPosition: CameraPosition = CameraPosition.DEFAULT + + private val animatorProvider: MapboxAnimatorProvider = mockk() + private val animatorSetProvider: MapboxAnimatorSetProvider = mockk() + + private val projection: Projection = mockk() + + @Before + fun setUp() { + locationAnimatorCoordinator = LocationAnimatorCoordinator(projection, animatorSetProvider, animatorProvider) + configureAnimatorProvider() + every { projection.getMetersPerPixelAtLatitude(any()) } answers { 1.0 } + every { animatorSetProvider.startAnimation(any(), any(), any()) } answers {} + locationAnimatorCoordinator.updateAnimatorListenerHolders(getListenerHoldersSet( + ANIMATOR_LAYER_LATLNG, + ANIMATOR_CAMERA_LATLNG, + ANIMATOR_LAYER_GPS_BEARING, + ANIMATOR_LAYER_COMPASS_BEARING, + ANIMATOR_CAMERA_GPS_BEARING, + ANIMATOR_CAMERA_COMPASS_BEARING, + ANIMATOR_LAYER_ACCURACY, + ANIMATOR_ZOOM, + ANIMATOR_TILT + )) + } + + private fun configureAnimatorProvider() { + // workaround https://github.com/mockk/mockk/issues/229#issuecomment-457816131 + registerInstanceFactory { AnimationsValueChangeListener {} } + registerInstanceFactory { AnimationsValueChangeListener {} } + val previousFloatSlot = slot() + val targetFloatSlot = slot() + val listenerSlot = slot>() + val maxFpsSlot = slot() + every { + animatorProvider.floatAnimator(capture(previousFloatSlot), capture(targetFloatSlot), capture(listenerSlot), capture(maxFpsSlot)) + } answers { + MapboxFloatAnimator(previousFloatSlot.captured, targetFloatSlot.captured, listenerSlot.captured, maxFpsSlot.captured) + } + + val previousLatLngSlot = slot() + val targetLatLngSlot = slot() + every { + animatorProvider.latLngAnimator(capture(previousLatLngSlot), capture(targetLatLngSlot), capture(listenerSlot), capture(maxFpsSlot)) + } answers { + MapboxLatLngAnimator(previousLatLngSlot.captured, targetLatLngSlot.captured, listenerSlot.captured, maxFpsSlot.captured) + } + + val callback = slot() + every { + animatorProvider.cameraAnimator(capture(previousFloatSlot), capture(targetFloatSlot), capture(listenerSlot), capture(callback)) + } answers { + MapboxCameraAnimatorAdapter(previousFloatSlot.captured, targetFloatSlot.captured, listenerSlot.captured, callback.captured) + } + every { + animatorProvider.cameraAnimator(capture(previousFloatSlot), capture(targetFloatSlot), capture(listenerSlot), null) + } answers { + MapboxCameraAnimatorAdapter(previousFloatSlot.captured, targetFloatSlot.captured, listenerSlot.captured, null) + } + } + + @Test + fun feedNewLocation_animatorsAreCreated() { + locationAnimatorCoordinator.feedNewLocation(Location(""), cameraPosition, false) + + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG] != null) + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_GPS_BEARING] != null) + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_LATLNG] != null) + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING] != null) + } + + @Test + fun feedNewLocation_animatorValue() { + val location = Location("") + location.latitude = 51.0 + location.longitude = 17.0 + location.bearing = 35f + locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + + val cameraLatLngTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG]?.target as LatLng + assertEquals(cameraLatLngTarget.latitude, cameraLatLngTarget.latitude) + + val layerLatLngTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_LATLNG]?.target as LatLng + assertEquals(layerLatLngTarget.latitude, layerLatLngTarget.latitude) + + val cameraBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_GPS_BEARING]?.target as Float + assertEquals(location.bearing, cameraBearingTarget) + + val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float + assertEquals(location.bearing, layerBearingTarget) + } + + @Test + fun feedNewLocation_animatorValue_correctRotation_1() { + val location = Location("") + location.latitude = 51.0 + location.longitude = 17.0 + location.bearing = 0f + + val animator = mockk(relaxed = true) + every { animator.animatedValue } returns 270f + locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) + + locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + + val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float + assertEquals(360f, layerBearingTarget) + } + + @Test + fun feedNewLocation_animatorValue_correctRotation_2() { + val location = Location("") + location.latitude = 51.0 + location.longitude = 17.0 + location.bearing = 90f + + val animator = mockk(relaxed = true) + every { animator.animatedValue } returns 280f + locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) + + locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + + val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float + assertEquals(450f, layerBearingTarget) + } + + @Test + fun feedNewLocation_animatorValue_correctRotation_3() { + val location = Location("") + location.latitude = 51.0 + location.longitude = 17.0 + location.bearing = 300f + + val animator = mockk(relaxed = true) + every { animator.animatedValue } returns 450f + locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) + + locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + + val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float + assertEquals(-60f, layerBearingTarget) + } + + @Test + fun feedNewLocation_animatorValue_correctRotation_4() { + val location = Location("") + location.latitude = 51.0 + location.longitude = 17.0 + location.bearing = 350f + + val animator = mockk(relaxed = true) + every { animator.animatedValue } returns 10f + locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) + + locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + + val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float + assertEquals(-10f, layerBearingTarget) + } + + @Test + fun feedNewLocation_animatorValue_correctRotation_5() { + val location = Location("") + location.latitude = 51.0 + location.longitude = 17.0 + location.bearing = 90f + + val animator = mockk(relaxed = true) + every { animator.animatedValue } returns -280f + locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) + + locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + + val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float + assertEquals(90f, layerBearingTarget) + } + + @Test + fun feedNewLocation_animatorValue_correctRotation_6() { + val location = Location("") + location.latitude = 51.0 + location.longitude = 17.0 + location.bearing = 270f + + val animator = mockk(relaxed = true) + every { animator.animatedValue } returns -350f + locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) + + locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + + val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float + assertEquals(-90f, layerBearingTarget) + } + + @Test + fun feedNewLocation_isNorth_animatorsAreCreated() { + val location = Location("") + location.latitude = 51.0 + location.longitude = 17.0 + location.bearing = 35f + locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG] != null) + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_GPS_BEARING] != null) + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_LATLNG] != null) + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING] != null) + } + + @Test + fun feedNewLocation_isNorth_animatorValue() { + val location = Location("") + location.latitude = 51.0 + location.longitude = 17.0 + location.bearing = 35f + locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, true) + + val cameraLatLngTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG]?.target as LatLng + assertEquals(cameraLatLngTarget.latitude, cameraLatLngTarget.latitude) + + val layerLatLngTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_LATLNG]?.target as LatLng + assertEquals(layerLatLngTarget.latitude, layerLatLngTarget.latitude) + + val cameraBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_GPS_BEARING]?.target as Float + assertEquals(0f, cameraBearingTarget) + + val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float + assertEquals(location.bearing, layerBearingTarget) + } + + @Test + fun feedNewCompassBearing_animatorsAreCreated() { + locationAnimatorCoordinator.feedNewCompassBearing(77f, cameraPosition) + + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_COMPASS_BEARING] != null) + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_COMPASS_BEARING] != null) + } + + @Test + fun feedNewCompassBearing_animatorValue() { + val bearing = 77f + locationAnimatorCoordinator.feedNewCompassBearing(bearing, cameraPosition) + + val cameraBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_COMPASS_BEARING]?.target as Float + assertEquals(bearing, cameraBearingTarget) + + val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_COMPASS_BEARING]?.target as Float + assertEquals(bearing, layerBearingTarget) + } + + @Test + fun feedNewAccuracyRadius_animatorsCreated() { + locationAnimatorCoordinator.feedNewAccuracyRadius(150f, false) + + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_ACCURACY] != null) + } + + @Test + fun feedNewAccuracyRadius_animatorValue() { + val accuracy = 150f + locationAnimatorCoordinator.feedNewAccuracyRadius(accuracy, false) + + val layerAccuracy = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_ACCURACY]?.target as Float + assertEquals(layerAccuracy, accuracy) + } + + @Test + fun feedNewAccuracyRadius_noAnimation_animatorsCreated() { + locationAnimatorCoordinator.feedNewAccuracyRadius(150f, true) + + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_ACCURACY] != null) + } + + @Test + fun feedNewAccuracyRadius_noAnimation_animatorValue() { + val accuracy = 150f + locationAnimatorCoordinator.feedNewAccuracyRadius(accuracy, true) + + val layerAccuracy = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_ACCURACY]?.target as Float + assertEquals(layerAccuracy, accuracy) + } + + @Test + fun feedNewZoomLevel_animatorsCreated() { + locationAnimatorCoordinator.feedNewZoomLevel( + 15.0, + cameraPosition, + DEFAULT_TRACKING_ZOOM_ANIM_DURATION, + null + ) + + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_ZOOM] != null) + } + + @Test + fun feedNewZoomLevel_animatorValue() { + val zoom = 15.0f + locationAnimatorCoordinator.feedNewZoomLevel( + zoom.toDouble(), + cameraPosition, + DEFAULT_TRACKING_ZOOM_ANIM_DURATION, + null + ) + + val animator = locationAnimatorCoordinator.animatorArray[ANIMATOR_ZOOM] + assertEquals(zoom, animator.target) + verify { animatorSetProvider.startAnimation(eq(listOf(animator)), any(), DEFAULT_TRACKING_ZOOM_ANIM_DURATION) } + } + + @Test + fun feedNewTiltLevel_animatorsCreated() { + locationAnimatorCoordinator.feedNewTilt( + 30.0, + cameraPosition, + DEFAULT_TRACKING_TILT_ANIM_DURATION, + null + ) + + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_TILT] != null) + } + + @Test + fun feedNewTiltLevel_animatorValue() { + val tilt = 30.0f + locationAnimatorCoordinator.feedNewTilt( + tilt.toDouble(), + cameraPosition, + DEFAULT_TRACKING_TILT_ANIM_DURATION, + null + ) + + val animator = locationAnimatorCoordinator.animatorArray[ANIMATOR_TILT] + assertEquals(tilt, animator.target) + verify { animatorSetProvider.startAnimation(eq(listOf(animator)), any(), DEFAULT_TRACKING_TILT_ANIM_DURATION) } + } + + @Test + fun cancelAllAnimators() { + locationAnimatorCoordinator.feedNewLocation(Location(""), cameraPosition, true) + locationAnimatorCoordinator.cancelAllAnimations() + + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG] == null) + } + + @Test + fun cancelZoomAnimators() { + locationAnimatorCoordinator.feedNewZoomLevel( + 15.0, + cameraPosition, + DEFAULT_TRACKING_ZOOM_ANIM_DURATION, + null + ) + locationAnimatorCoordinator.cancelZoomAnimation() + + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_ZOOM] == null) + } + + @Test + fun cancelTiltAnimation() { + locationAnimatorCoordinator.feedNewTilt( + 30.0, + cameraPosition, + DEFAULT_TRACKING_TILT_ANIM_DURATION, + null + ) + + locationAnimatorCoordinator.cancelTiltAnimation() + + assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_TILT] == null) + } + + @Test + fun resetAllCameraAnimations_empty() { + locationAnimatorCoordinator.resetAllCameraAnimations(cameraPosition, false) + assertTrue(locationAnimatorCoordinator.animatorArray.size() == 0) + } + + @Test + fun resetAllLayerAnimations_empty() { + locationAnimatorCoordinator.resetAllLayerAnimations() + assertTrue(locationAnimatorCoordinator.animatorArray.size() == 0) + } + + @Test + fun addNewListener() { + val listener = Mockito.mock(AnimationsValueChangeListener::class.java) + val holder = AnimatorListenerHolder(RenderMode.NORMAL, listener) + val set = HashSet().also { + it.add(holder) + } + locationAnimatorCoordinator.updateAnimatorListenerHolders(set) + + assertTrue(locationAnimatorCoordinator.listeners.contains(listener)) + } + + @Test + fun updateListeners() { + val listener = Mockito.mock(AnimationsValueChangeListener::class.java) + val holder = AnimatorListenerHolder(RenderMode.NORMAL, listener) + val set = HashSet().also { + it.add(holder) + } + locationAnimatorCoordinator.updateAnimatorListenerHolders(set) + + val listener2 = Mockito.mock(AnimationsValueChangeListener::class.java) + val holder2 = AnimatorListenerHolder(RenderMode.NORMAL, listener2) + val listener3 = Mockito.mock(AnimationsValueChangeListener::class.java) + val holder3 = AnimatorListenerHolder(CameraMode.TRACKING_GPS, listener3) + val set2 = HashSet().also { + it.add(holder2) + it.add(holder3) + } + locationAnimatorCoordinator.updateAnimatorListenerHolders(set2) + + assertTrue(locationAnimatorCoordinator.listeners.size() == 2) + assertTrue(locationAnimatorCoordinator.listeners.contains(listener2)) + assertTrue(locationAnimatorCoordinator.listeners.contains(listener3)) + } + + @Test + fun feedNewCompassBearing_withAnimation() { + locationAnimatorCoordinator.setCompassAnimationEnabled(true) + locationAnimatorCoordinator.feedNewCompassBearing(77f, cameraPosition) + + val animators = mutableListOf( + locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_COMPASS_BEARING], + locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_COMPASS_BEARING]) + + verify(exactly = 1) { animatorSetProvider.startAnimation(eq(animators), ofType(LinearInterpolator::class), eq(LocationComponentConstants.COMPASS_UPDATE_RATE_MS)) } + } + + @Test + fun feedNewCompassBearing_withoutAnimation() { + locationAnimatorCoordinator.setCompassAnimationEnabled(false) + locationAnimatorCoordinator.feedNewCompassBearing(77f, cameraPosition) + + val animators = mutableListOf( + locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_COMPASS_BEARING], + locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_COMPASS_BEARING]) + + verify(exactly = 1) { animatorSetProvider.startAnimation(eq(animators), ofType(LinearInterpolator::class), eq(0)) } + } + + @Test + fun feedNewAccuracy_withAnimation() { + locationAnimatorCoordinator.setAccuracyAnimationEnabled(true) + locationAnimatorCoordinator.feedNewAccuracyRadius(150f, false) + + val animators = mutableListOf( + locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_ACCURACY]) + + verify(exactly = 1) { animatorSetProvider.startAnimation(eq(animators), ofType(LinearInterpolator::class), eq(LocationComponentConstants.ACCURACY_RADIUS_ANIMATION_DURATION)) } + } + + @Test + fun feedNewAccuracy_withoutAnimation() { + locationAnimatorCoordinator.setAccuracyAnimationEnabled(false) + locationAnimatorCoordinator.feedNewAccuracyRadius(150f, false) + + val animators = mutableListOf( + locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_ACCURACY]) + + verify(exactly = 1) { animatorSetProvider.startAnimation(eq(animators), ofType(LinearInterpolator::class), eq(0)) } + } + + @Test + fun maxFps_setter() { + locationAnimatorCoordinator.setMaxAnimationFps(5) + assertEquals(5, locationAnimatorCoordinator.maxAnimationFps) + } + + @Test + fun maxFps_moreThanZeroRequired() { + locationAnimatorCoordinator.setMaxAnimationFps(0) + assertEquals(Int.MAX_VALUE, locationAnimatorCoordinator.maxAnimationFps) + locationAnimatorCoordinator.setMaxAnimationFps(-1) + assertEquals(Int.MAX_VALUE, locationAnimatorCoordinator.maxAnimationFps) + } + + @Test + fun maxFps_givenToAnimator() { + locationAnimatorCoordinator.setMaxAnimationFps(5) + locationAnimatorCoordinator.feedNewLocation(Location(""), cameraPosition, false) + verify { animatorProvider.latLngAnimator(any(), any(), any(), 5) } + verify { animatorProvider.floatAnimator(any(), any(), any(), 5) } + } + + private fun getListenerHoldersSet(vararg animatorTypes: Int): Set { + return HashSet().also { + for (type in animatorTypes) { + it.add(AnimatorListenerHolder(type, mockk())) + } + } + } +} + +private fun SparseArray.contains(listener: AnimationsValueChangeListener<*>?): Boolean { + for (i in 0 until this.size()) { + val element = this.get(this.keyAt(i)) + if (element == listener) { + return true + } + } + return false +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationCameraControllerTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationCameraControllerTest.java new file mode 100644 index 000000000..d1b8642c3 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationCameraControllerTest.java @@ -0,0 +1,1001 @@ +package com.mapbox.mapboxsdk.location; + +import android.graphics.PointF; +import android.location.Location; + +import com.mapbox.android.gestures.AndroidGesturesManager; +import com.mapbox.android.gestures.MoveGestureDetector; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.camera.CameraUpdate; +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.Projection; +import com.mapbox.mapboxsdk.maps.Transform; +import com.mapbox.mapboxsdk.maps.UiSettings; + +import junit.framework.Assert; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.Set; + +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.TRANSITION_ANIMATION_DURATION_MS; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_CAMERA_COMPASS_BEARING; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_CAMERA_GPS_BEARING; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_CAMERA_LATLNG; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_TILT; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_ZOOM; +import static com.mapbox.mapboxsdk.location.modes.CameraMode.NONE; +import static com.mapbox.mapboxsdk.location.modes.CameraMode.NONE_COMPASS; +import static com.mapbox.mapboxsdk.location.modes.CameraMode.NONE_GPS; +import static com.mapbox.mapboxsdk.location.modes.CameraMode.TRACKING; +import static com.mapbox.mapboxsdk.location.modes.CameraMode.TRACKING_COMPASS; +import static com.mapbox.mapboxsdk.location.modes.CameraMode.TRACKING_GPS; +import static com.mapbox.mapboxsdk.location.modes.CameraMode.TRACKING_GPS_NORTH; +import static junit.framework.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class LocationCameraControllerTest { + + @Test + public void setCameraMode_mapTransitionsAreCancelled() { + MapboxMap mapboxMap = mock(MapboxMap.class); + LocationCameraController camera = buildCamera(mapboxMap); + camera.initializeOptions(mock(LocationComponentOptions.class)); + + camera.setCameraMode(TRACKING_GPS); + + verify(mapboxMap).cancelTransitions(); + } + + @Test + public void setCameraMode_gestureThresholdIsAdjusted() { + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + LocationCameraController camera = buildCamera(moveGestureDetector); + LocationComponentOptions options = mock(LocationComponentOptions.class); + float moveThreshold = 5f; + when(options.trackingInitialMoveThreshold()).thenReturn(moveThreshold); + when(options.trackingGesturesManagement()).thenReturn(true); + camera.initializeOptions(options); + + camera.setCameraMode(TRACKING_GPS); + + verify(moveGestureDetector).setMoveThreshold(moveThreshold); + } + + @Test + public void setCameraMode_gestureThresholdNotAdjustedWhenDisabled() { + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + LocationCameraController camera = buildCamera(moveGestureDetector); + LocationComponentOptions options = mock(LocationComponentOptions.class); + float moveThreshold = 5f; + when(options.trackingInitialMoveThreshold()).thenReturn(moveThreshold); + when(options.trackingGesturesManagement()).thenReturn(false); + camera.initializeOptions(options); + + camera.setCameraMode(TRACKING_GPS); + + verify(moveGestureDetector, times(0)).setMoveThreshold(moveThreshold); + verify(moveGestureDetector, times(0)).setMoveThreshold(0f); + } + + @Test + public void setCameraMode_gestureThresholdIsResetWhenNotTracking() { + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + LocationCameraController camera = buildCamera(moveGestureDetector); + LocationComponentOptions options = mock(LocationComponentOptions.class); + when(options.trackingGesturesManagement()).thenReturn(true); + camera.initializeOptions(options); + + camera.setCameraMode(NONE); + + verify(moveGestureDetector, times(2)).setMoveThreshold(0f); // one for initialization + } + + @Test + public void setCameraMode_notTrackingAdjustsFocalPoint() { + MapboxMap mapboxMap = mock(MapboxMap.class); + when(mapboxMap.getUiSettings()).thenReturn(mock(UiSettings.class)); + LocationCameraController camera = buildCamera(mapboxMap); + camera.initializeOptions(mock(LocationComponentOptions.class)); + + camera.setCameraMode(TRACKING_GPS); + camera.setCameraMode(NONE); + + verify(mapboxMap.getUiSettings()).setFocalPoint(null); + } + + @Test + public void setCameraMode_trackingChangeListenerCameraDismissedIsCalled() { + OnCameraTrackingChangedListener internalTrackingChangedListener = mock(OnCameraTrackingChangedListener.class); + LocationCameraController camera = buildCamera(internalTrackingChangedListener); + camera.initializeOptions(mock(LocationComponentOptions.class)); + + camera.setCameraMode(TRACKING_GPS); + camera.setCameraMode(NONE); + + verify(internalTrackingChangedListener).onCameraTrackingDismissed(); + } + + @Test + public void setCameraMode_internalCameraTrackingChangeListenerIsCalled() { + OnCameraTrackingChangedListener internalTrackingChangedListener = mock(OnCameraTrackingChangedListener.class); + LocationCameraController camera = buildCamera(internalTrackingChangedListener); + camera.initializeOptions(mock(LocationComponentOptions.class)); + int cameraMode = NONE; + + camera.setCameraMode(cameraMode); + + verify(internalTrackingChangedListener).onCameraTrackingChanged(cameraMode); + } + + @Test + public void setCameraMode_doNotNotifyAboutDuplicates_NONE() { + OnCameraTrackingChangedListener internalTrackingChangedListener = mock(OnCameraTrackingChangedListener.class); + LocationCameraController camera = buildCamera(internalTrackingChangedListener); + camera.initializeOptions(mock(LocationComponentOptions.class)); + int cameraMode = NONE; + + camera.setCameraMode(cameraMode); + camera.setCameraMode(cameraMode); + + verify(internalTrackingChangedListener, times(1)).onCameraTrackingChanged(cameraMode); + } + + @Test + public void setCameraMode_doNotNotifyAboutDuplicates_TRACKING_GPS() { + OnCameraTrackingChangedListener internalTrackingChangedListener = mock(OnCameraTrackingChangedListener.class); + LocationCameraController camera = buildCamera(internalTrackingChangedListener); + camera.initializeOptions(mock(LocationComponentOptions.class)); + int cameraMode = TRACKING_GPS; + + camera.setCameraMode(cameraMode); + camera.setCameraMode(cameraMode); + + verify(internalTrackingChangedListener, times(1)).onCameraTrackingChanged(cameraMode); + } + + @Test + public void setCameraMode_cancelTransitionsWhenSet() { + MapboxMap mapboxMap = mock(MapboxMap.class); + when(mapboxMap.getUiSettings()).thenReturn(mock(UiSettings.class)); + when(mapboxMap.getProjection()).thenReturn(mock(Projection.class)); + LocationCameraController camera = buildCamera(mapboxMap); + camera.initializeOptions(mock(LocationComponentOptions.class)); + + camera.setCameraMode(NONE_COMPASS); + verify(mapboxMap, times(1)).cancelTransitions(); + + camera.setCameraMode(NONE_GPS); + verify(mapboxMap, times(2)).cancelTransitions(); + + camera.setCameraMode(TRACKING); + verify(mapboxMap, times(3)).cancelTransitions(); + + camera.setCameraMode(TRACKING_COMPASS); + verify(mapboxMap, times(4)).cancelTransitions(); + + camera.setCameraMode(TRACKING_GPS); + verify(mapboxMap, times(5)).cancelTransitions(); + + camera.setCameraMode(TRACKING_GPS_NORTH); + verify(mapboxMap, times(6)).cancelTransitions(); + } + + @Test + public void setCameraMode_dontCancelTransitionsWhenNoneSet() { + MapboxMap mapboxMap = mock(MapboxMap.class); + when(mapboxMap.getUiSettings()).thenReturn(mock(UiSettings.class)); + when(mapboxMap.getProjection()).thenReturn(mock(Projection.class)); + LocationCameraController camera = buildCamera(mapboxMap); + camera.initializeOptions(mock(LocationComponentOptions.class)); + + camera.setCameraMode(NONE); + verify(mapboxMap, never()).cancelTransitions(); + } + + @Test + public void onNewLatLngValue_cameraModeTrackingUpdatesLatLng() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(TRACKING); + LatLng latLng = mock(LatLng.class); + + getAnimationListener(ANIMATOR_CAMERA_LATLNG, camera.getAnimationListeners()).onNewAnimationValue(latLng); + + verify(transform).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewLatLngValue_cameraModeTrackingGpsNorthUpdatesLatLng() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(TRACKING_GPS_NORTH); + LatLng latLng = mock(LatLng.class); + + getAnimationListener(ANIMATOR_CAMERA_LATLNG, camera.getAnimationListeners()).onNewAnimationValue(latLng); + + verify(transform).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewLatLngValue_cameraModeTrackingGpsUpdatesLatLng() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(TRACKING_GPS); + LatLng latLng = mock(LatLng.class); + + getAnimationListener(ANIMATOR_CAMERA_LATLNG, camera.getAnimationListeners()).onNewAnimationValue(latLng); + + verify(transform).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewLatLngValue_cameraModeTrackingCompassUpdatesLatLng() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(TRACKING_COMPASS); + LatLng latLng = mock(LatLng.class); + + getAnimationListener(ANIMATOR_CAMERA_LATLNG, camera.getAnimationListeners()).onNewAnimationValue(latLng); + + verify(transform).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewLatLngValue_cameraModeNoneIgnored() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(NONE); + + assertNull(getAnimationListener(ANIMATOR_CAMERA_LATLNG, camera.getAnimationListeners())); + verify(transform, times(0)).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewLatLngValue_focalPointIsAdjusted() { + MapboxMap mapboxMap = mock(MapboxMap.class); + UiSettings uiSettings = mock(UiSettings.class); + when(mapboxMap.getUiSettings()).thenReturn(uiSettings); + Projection projection = mock(Projection.class); + PointF pointF = mock(PointF.class); + when(projection.toScreenLocation(any(LatLng.class))).thenReturn(pointF); + when(mapboxMap.getProjection()).thenReturn(projection); + LocationCameraController camera = buildCamera(mapboxMap); + LocationComponentOptions options = mock(LocationComponentOptions.class); + when(options.trackingGesturesManagement()).thenReturn(true); + camera.initializeOptions(options); + camera.setCameraMode(TRACKING); + LatLng latLng = mock(LatLng.class); + + getAnimationListener(ANIMATOR_CAMERA_LATLNG, camera.getAnimationListeners()).onNewAnimationValue(latLng); + + verify(uiSettings).setFocalPoint(pointF); + } + + @Test + public void onNewGpsBearingValue_cameraModeTrackingGpsUpdatesBearing() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(TRACKING_GPS); + float gpsBearing = 5f; + + getAnimationListener(ANIMATOR_CAMERA_GPS_BEARING, camera.getAnimationListeners()).onNewAnimationValue(gpsBearing); + + verify(transform).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewGpsBearingValue_cameraModeNoneGpsUpdatesBearing() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(NONE_GPS); + float gpsBearing = 5f; + + getAnimationListener(ANIMATOR_CAMERA_GPS_BEARING, camera.getAnimationListeners()).onNewAnimationValue(gpsBearing); + + verify(transform).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewGpsBearingValue_cameraModeTrackingNorthUpdatesBearing() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(mapboxMap, transform); + CameraPosition cameraPosition = new CameraPosition.Builder().bearing(7d).build(); + when(mapboxMap.getCameraPosition()).thenReturn(cameraPosition); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(TRACKING_GPS_NORTH); + float gpsBearing = 5f; + + getAnimationListener(ANIMATOR_CAMERA_GPS_BEARING, camera.getAnimationListeners()).onNewAnimationValue(gpsBearing); + + verify(transform).moveCamera(eq(mapboxMap), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewGpsBearingValue_cameraModeTrackingNorthBearingZeroIgnored() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(mapboxMap, transform); + CameraPosition cameraPosition = new CameraPosition.Builder().bearing(0d).build(); + when(mapboxMap.getCameraPosition()).thenReturn(cameraPosition); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(TRACKING_GPS_NORTH); + float gpsBearing = 5f; + + getAnimationListener(ANIMATOR_CAMERA_GPS_BEARING, camera.getAnimationListeners()).onNewAnimationValue(gpsBearing); + + verify(transform, times(0)).moveCamera(eq(mapboxMap), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewGpsBearingValue_cameraModeNoneIgnored() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(NONE); + + assertNull(getAnimationListener(ANIMATOR_CAMERA_GPS_BEARING, camera.getAnimationListeners())); + verify(transform, times(0)).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewCompassBearingValue_cameraModeTrackingCompassUpdatesBearing() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(TRACKING_COMPASS); + float compassBearing = 5f; + + getAnimationListener(ANIMATOR_CAMERA_COMPASS_BEARING, camera.getAnimationListeners()) + .onNewAnimationValue(compassBearing); + + verify(transform).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewCompassBearingValue_cameraModeNoneCompassUpdatesBearing() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(NONE_COMPASS); + float compassBearing = 5f; + + getAnimationListener(ANIMATOR_CAMERA_COMPASS_BEARING, camera.getAnimationListeners()) + .onNewAnimationValue(compassBearing); + + verify(transform).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewCompassBearingValue_cameraModeNoneIgnored() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(NONE); + + assertNull(getAnimationListener(ANIMATOR_CAMERA_COMPASS_BEARING, camera.getAnimationListeners())); + verify(transform, times(0)).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNewZoomValue_cameraIsUpdated() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(TRACKING); + float zoom = 5f; + + getAnimationListener(ANIMATOR_ZOOM, camera.getAnimationListeners()).onNewAnimationValue(zoom); + + verify(transform).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void onNeTiltValue_cameraIsUpdated() { + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + camera.setCameraMode(TRACKING); + float tilt = 5f; + + getAnimationListener(ANIMATOR_TILT, camera.getAnimationListeners()).onNewAnimationValue(tilt); + + verify(transform).moveCamera(any(MapboxMap.class), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void gesturesManagement_enabled() { + MapboxMap mapboxMap = mock(MapboxMap.class); + AndroidGesturesManager initialGesturesManager = mock(AndroidGesturesManager.class); + AndroidGesturesManager internalGesturesManager = mock(AndroidGesturesManager.class); + when(mapboxMap.getGesturesManager()).thenReturn(initialGesturesManager); + LocationCameraController camera = buildCamera(mapboxMap, initialGesturesManager, internalGesturesManager); + LocationComponentOptions options = mock(LocationComponentOptions.class); + when(options.trackingGesturesManagement()).thenReturn(true); + camera.initializeOptions(options); + + verify(mapboxMap).setGesturesManager(internalGesturesManager, true, true); + } + + @Test + public void gesturesManagement_disabled() { + MapboxMap mapboxMap = mock(MapboxMap.class); + AndroidGesturesManager initialGesturesManager = mock(AndroidGesturesManager.class); + AndroidGesturesManager internalGesturesManager = mock(AndroidGesturesManager.class); + when(mapboxMap.getGesturesManager()).thenReturn(internalGesturesManager); + LocationCameraController camera = buildCamera(mapboxMap, initialGesturesManager, internalGesturesManager); + LocationComponentOptions options = mock(LocationComponentOptions.class); + when(options.trackingGesturesManagement()).thenReturn(false); + camera.initializeOptions(options); + + verify(mapboxMap).setGesturesManager(initialGesturesManager, true, true); + } + + @Test + public void gesturesManagement_optionNotChangedInitial() { + MapboxMap mapboxMap = mock(MapboxMap.class); + AndroidGesturesManager initialGesturesManager = mock(AndroidGesturesManager.class); + AndroidGesturesManager internalGesturesManager = mock(AndroidGesturesManager.class); + when(mapboxMap.getGesturesManager()).thenReturn(initialGesturesManager); + LocationCameraController camera = buildCamera(mapboxMap, initialGesturesManager, internalGesturesManager); + LocationComponentOptions options = mock(LocationComponentOptions.class); + when(options.trackingGesturesManagement()).thenReturn(false); + camera.initializeOptions(options); + + verify(mapboxMap, times(0)).setGesturesManager(initialGesturesManager, true, true); + } + + @Test + public void gesturesManagement_optionNotChangedInternal() { + MapboxMap mapboxMap = mock(MapboxMap.class); + AndroidGesturesManager initialGesturesManager = mock(AndroidGesturesManager.class); + AndroidGesturesManager internalGesturesManager = mock(AndroidGesturesManager.class); + when(mapboxMap.getGesturesManager()).thenReturn(internalGesturesManager); + LocationCameraController camera = buildCamera(mapboxMap, initialGesturesManager, internalGesturesManager); + LocationComponentOptions options = mock(LocationComponentOptions.class); + when(options.trackingGesturesManagement()).thenReturn(true); + camera.initializeOptions(options); + + verify(mapboxMap, times(0)).setGesturesManager(internalGesturesManager, true, true); + } + + @Test + public void onMove_notCancellingTransitionWhileNone() { + MapboxMap mapboxMap = mock(MapboxMap.class); + when(mapboxMap.getUiSettings()).thenReturn(mock(UiSettings.class)); + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + LocationCameraController camera = buildCamera(mapboxMap); + camera.initializeOptions(mock(LocationComponentOptions.class)); + + camera.setCameraMode(NONE); + camera.onMoveListener.onMove(moveGestureDetector); + verify(mapboxMap, times(0)).cancelTransitions(); + verify(moveGestureDetector, times(0)).interrupt(); + + // testing subsequent calls + camera.onMoveListener.onMove(moveGestureDetector); + verify(mapboxMap, times(0)).cancelTransitions(); + verify(moveGestureDetector, times(0)).interrupt(); + } + + @Test + public void onMove_cancellingTransitionWhileGps() { + MapboxMap mapboxMap = mock(MapboxMap.class); + when(mapboxMap.getUiSettings()).thenReturn(mock(UiSettings.class)); + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + LocationCameraController camera = buildCamera(mapboxMap); + camera.initializeOptions(mock(LocationComponentOptions.class)); + + camera.setCameraMode(TRACKING); + camera.onMoveListener.onMove(moveGestureDetector); + verify(mapboxMap, times(1)).cancelTransitions(); + verify(moveGestureDetector, times(1)).interrupt(); + + // testing subsequent calls + camera.onMoveListener.onMove(moveGestureDetector); + verify(mapboxMap, times(1)).cancelTransitions(); + verify(moveGestureDetector, times(1)).interrupt(); + } + + @Test + public void onMove_cancellingTransitionWhileBearing() { + MapboxMap mapboxMap = mock(MapboxMap.class); + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + LocationCameraController camera = buildCamera(mapboxMap); + camera.initializeOptions(mock(LocationComponentOptions.class)); + + camera.setCameraMode(NONE_COMPASS); + camera.onMoveListener.onMove(moveGestureDetector); + verify(mapboxMap, times(1)).cancelTransitions(); + verify(moveGestureDetector, times(1)).interrupt(); + + // testing subsequent calls + camera.onMoveListener.onMove(moveGestureDetector); + verify(mapboxMap, times(1)).cancelTransitions(); + verify(moveGestureDetector, times(1)).interrupt(); + } + + @Test + public void transition_locationIsNull() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(mapboxMap, transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + OnLocationCameraTransitionListener listener = mock(OnLocationCameraTransitionListener.class); + + camera.setCameraMode(TRACKING, null, TRANSITION_ANIMATION_DURATION_MS, null, null, null, listener); + Assert.assertEquals(TRACKING, camera.getCameraMode()); + verify(listener).onLocationCameraTransitionFinished(TRACKING); + verify(transform, times(0)) + .animateCamera(eq(mapboxMap), any(CameraUpdate.class), any(Integer.class), + any(MapboxMap.CancelableCallback.class)); + } + + @Test + public void transition_notTracking() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + LocationCameraController camera = buildCamera(mapboxMap, transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + OnLocationCameraTransitionListener listener = mock(OnLocationCameraTransitionListener.class); + Location location = mock(Location.class); + + camera.setCameraMode(NONE, location, TRANSITION_ANIMATION_DURATION_MS, null, null, null, listener); + verify(listener, times(1)).onLocationCameraTransitionFinished(NONE); + verify(transform, times(0)) + .animateCamera(eq(mapboxMap), any(CameraUpdate.class), any(Integer.class), + any(MapboxMap.CancelableCallback.class)); + } + + @Test + public void transition_trackingChanged() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + when(mapboxMap.getCameraPosition()).thenReturn(CameraPosition.DEFAULT); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1000)); + LocationCameraController camera = buildCamera(mapboxMap, transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + final OnLocationCameraTransitionListener listener = mock(OnLocationCameraTransitionListener.class); + Location location = mock(Location.class); + + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + listener.onLocationCameraTransitionFinished(TRACKING); + return null; + } + }).when(transform).animateCamera(eq(mapboxMap), any(CameraUpdate.class), any(Integer.class), + any(MapboxMap.CancelableCallback.class)); + + camera.setCameraMode(TRACKING, location, TRANSITION_ANIMATION_DURATION_MS, null, null, null, listener); + verify(listener).onLocationCameraTransitionFinished(TRACKING); + verify(transform) + .animateCamera(eq(mapboxMap), any(CameraUpdate.class), any(Integer.class), + any(MapboxMap.CancelableCallback.class)); + } + + @Test + public void transition_trackingNotChanged() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + when(mapboxMap.getCameraPosition()).thenReturn(CameraPosition.DEFAULT); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1000)); + LocationCameraController camera = buildCamera(mapboxMap, transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + final OnLocationCameraTransitionListener listener = mock(OnLocationCameraTransitionListener.class); + Location location = mock(Location.class); + + camera.setCameraMode(TRACKING, location, TRANSITION_ANIMATION_DURATION_MS, null, null, null, listener); + + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + listener.onLocationCameraTransitionFinished(TRACKING_GPS_NORTH); + return null; + } + }).when(transform).animateCamera(eq(mapboxMap), any(CameraUpdate.class), any(Integer.class), + any(MapboxMap.CancelableCallback.class)); + + camera.setCameraMode(TRACKING_GPS_NORTH, location, TRANSITION_ANIMATION_DURATION_MS, null, null, null, listener); + verify(listener, times(1)).onLocationCameraTransitionFinished(TRACKING_GPS_NORTH); + verify(transform, times(1)) + .animateCamera(eq(mapboxMap), any(CameraUpdate.class), any(Integer.class), + any(MapboxMap.CancelableCallback.class)); + } + + @Test + public void transition_canceled() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + when(mapboxMap.getCameraPosition()).thenReturn(CameraPosition.DEFAULT); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1000)); + LocationCameraController camera = buildCamera(mapboxMap, transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + final OnLocationCameraTransitionListener listener = mock(OnLocationCameraTransitionListener.class); + Location location = mock(Location.class); + + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + listener.onLocationCameraTransitionCanceled(TRACKING); + return null; + } + }).when(transform).animateCamera(eq(mapboxMap), any(CameraUpdate.class), any(Integer.class), + any(MapboxMap.CancelableCallback.class)); + + camera.setCameraMode(TRACKING, location, TRANSITION_ANIMATION_DURATION_MS, null, null, null, listener); + verify(listener).onLocationCameraTransitionCanceled(TRACKING); + verify(transform) + .animateCamera(eq(mapboxMap), any(CameraUpdate.class), any(Integer.class), + any(MapboxMap.CancelableCallback.class)); + } + + @Test + public void transition_mapboxCallbackFinished() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + when(mapboxMap.getCameraPosition()).thenReturn(CameraPosition.DEFAULT); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1000)); + LocationCameraController camera = buildCamera(mapboxMap, transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + final OnLocationCameraTransitionListener listener = mock(OnLocationCameraTransitionListener.class); + Location location = mock(Location.class); + when(location.getLatitude()).thenReturn(1.0); + when(location.getLongitude()).thenReturn(1.0); + when(location.getBearing()).thenReturn(30f); + when(location.getAltitude()).thenReturn(0.0); + + ArgumentCaptor callbackCaptor + = ArgumentCaptor.forClass(MapboxMap.CancelableCallback.class); + + camera.setCameraMode(TRACKING, location, TRANSITION_ANIMATION_DURATION_MS, null, null, null, listener); + + CameraPosition.Builder builder = new CameraPosition.Builder().target(new LatLng(location)); + verify(transform).animateCamera( + eq(mapboxMap), + eq(CameraUpdateFactory.newCameraPosition(builder.build())), + eq((int) TRANSITION_ANIMATION_DURATION_MS), + callbackCaptor.capture()); + + Assert.assertTrue(camera.isTransitioning()); + + callbackCaptor.getValue().onFinish(); + + Assert.assertFalse(camera.isTransitioning()); + + verify(listener).onLocationCameraTransitionFinished(TRACKING); + } + + @Test + public void transition_mapboxCallbackFinishedImmediately() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + when(mapboxMap.getCameraPosition()).thenReturn(CameraPosition.DEFAULT); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1)); + LocationCameraController camera = buildCamera(mapboxMap, transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + final OnLocationCameraTransitionListener listener = mock(OnLocationCameraTransitionListener.class); + Location location = mock(Location.class); + when(location.getLatitude()).thenReturn(1.0); + when(location.getLongitude()).thenReturn(1.0); + when(location.getBearing()).thenReturn(30f); + when(location.getAltitude()).thenReturn(0.0); + + ArgumentCaptor callbackCaptor + = ArgumentCaptor.forClass(MapboxMap.CancelableCallback.class); + + camera.setCameraMode(TRACKING, location, TRANSITION_ANIMATION_DURATION_MS, null, null, null, listener); + + CameraPosition.Builder builder = new CameraPosition.Builder().target(new LatLng(location)); + verify(transform).moveCamera( + eq(mapboxMap), + eq(CameraUpdateFactory.newCameraPosition(builder.build())), + callbackCaptor.capture()); + + Assert.assertTrue(camera.isTransitioning()); + + callbackCaptor.getValue().onFinish(); + + Assert.assertFalse(camera.isTransitioning()); + + verify(listener).onLocationCameraTransitionFinished(TRACKING); + } + + @Test + public void transition_mapboxCallbackCanceled() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + when(mapboxMap.getCameraPosition()).thenReturn(CameraPosition.DEFAULT); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1000)); + LocationCameraController camera = buildCamera(mapboxMap, transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + final OnLocationCameraTransitionListener listener = mock(OnLocationCameraTransitionListener.class); + Location location = mock(Location.class); + when(location.getLatitude()).thenReturn(1.0); + when(location.getLongitude()).thenReturn(1.0); + when(location.getBearing()).thenReturn(30f); + when(location.getAltitude()).thenReturn(0.0); + + ArgumentCaptor callbackCaptor + = ArgumentCaptor.forClass(MapboxMap.CancelableCallback.class); + + camera.setCameraMode(TRACKING, location, TRANSITION_ANIMATION_DURATION_MS, null, null, null, listener); + + CameraPosition.Builder builder = new CameraPosition.Builder().target(new LatLng(location)); + verify(transform).animateCamera( + eq(mapboxMap), + eq(CameraUpdateFactory.newCameraPosition(builder.build())), + eq((int) TRANSITION_ANIMATION_DURATION_MS), + callbackCaptor.capture()); + + Assert.assertTrue(camera.isTransitioning()); + + callbackCaptor.getValue().onCancel(); + + Assert.assertFalse(camera.isTransitioning()); + + verify(listener).onLocationCameraTransitionCanceled(TRACKING); + } + + @Test + public void transition_mapboxAnimateBearing() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + when(mapboxMap.getCameraPosition()).thenReturn(CameraPosition.DEFAULT); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1000)); + LocationCameraController camera = buildCamera(mapboxMap, transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + final OnLocationCameraTransitionListener listener = mock(OnLocationCameraTransitionListener.class); + Location location = mock(Location.class); + when(location.getLatitude()).thenReturn(1.0); + when(location.getLongitude()).thenReturn(1.0); + when(location.getBearing()).thenReturn(30f); + when(location.getAltitude()).thenReturn(0.0); + + camera.setCameraMode(TRACKING_GPS, location, TRANSITION_ANIMATION_DURATION_MS, null, null, null, listener); + + CameraPosition.Builder builder = new CameraPosition.Builder().target(new LatLng(location)).bearing(30); + verify(transform).animateCamera( + eq(mapboxMap), + eq(CameraUpdateFactory.newCameraPosition(builder.build())), + eq((int) TRANSITION_ANIMATION_DURATION_MS), + any(MapboxMap.CancelableCallback.class)); + } + + @Test + public void transition_mapboxAnimateNorth() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + when(mapboxMap.getCameraPosition()).thenReturn(CameraPosition.DEFAULT); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1000)); + LocationCameraController camera = buildCamera(mapboxMap, transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + final OnLocationCameraTransitionListener listener = mock(OnLocationCameraTransitionListener.class); + Location location = mock(Location.class); + when(location.getLatitude()).thenReturn(1.0); + when(location.getLongitude()).thenReturn(1.0); + when(location.getBearing()).thenReturn(30f); + when(location.getAltitude()).thenReturn(0.0); + + camera.setCameraMode(TRACKING_GPS_NORTH, location, TRANSITION_ANIMATION_DURATION_MS, null, null, null, listener); + + CameraPosition.Builder builder = new CameraPosition.Builder().target(new LatLng(location)).bearing(0); + verify(transform).animateCamera( + eq(mapboxMap), + eq(CameraUpdateFactory.newCameraPosition(builder.build())), + eq((int) TRANSITION_ANIMATION_DURATION_MS), + any(MapboxMap.CancelableCallback.class)); + } + + @Test + public void transition_animatorValuesDuringTransition() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + when(mapboxMap.getCameraPosition()).thenReturn(CameraPosition.DEFAULT); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1000)); + LocationCameraController camera = buildCamera(mapboxMap, transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + final OnLocationCameraTransitionListener listener = mock(OnLocationCameraTransitionListener.class); + Location location = mock(Location.class); + + ArgumentCaptor callbackCaptor + = ArgumentCaptor.forClass(MapboxMap.CancelableCallback.class); + + camera.setCameraMode(TRACKING_GPS, location, TRANSITION_ANIMATION_DURATION_MS, null, null, null, listener); + + verify(transform).animateCamera( + eq(mapboxMap), + any(CameraUpdate.class), + eq((int) TRANSITION_ANIMATION_DURATION_MS), + callbackCaptor.capture()); + + LatLng latLng = new LatLng(10, 10); + getAnimationListener(ANIMATOR_CAMERA_LATLNG, camera.getAnimationListeners()).onNewAnimationValue(latLng); + getAnimationListener(ANIMATOR_CAMERA_GPS_BEARING, camera.getAnimationListeners()).onNewAnimationValue(10f); + getAnimationListener(ANIMATOR_TILT, camera.getAnimationListeners()).onNewAnimationValue(10f); + getAnimationListener(ANIMATOR_ZOOM, camera.getAnimationListeners()).onNewAnimationValue(10f); + + verify(transform, times(0)).moveCamera(eq(mapboxMap), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + + callbackCaptor.getValue().onFinish(); + + getAnimationListener(ANIMATOR_CAMERA_LATLNG, camera.getAnimationListeners()).onNewAnimationValue(latLng); + getAnimationListener(ANIMATOR_CAMERA_GPS_BEARING, camera.getAnimationListeners()).onNewAnimationValue(10f); + getAnimationListener(ANIMATOR_TILT, camera.getAnimationListeners()).onNewAnimationValue(10f); + getAnimationListener(ANIMATOR_ZOOM, camera.getAnimationListeners()).onNewAnimationValue(10f); + + verify(transform, times(4)).moveCamera(eq(mapboxMap), any(CameraUpdate.class), + nullable(MapboxMap.CancelableCallback.class)); + } + + @Test + public void transition_customAnimation() { + MapboxMap mapboxMap = mock(MapboxMap.class); + Transform transform = mock(Transform.class); + when(mapboxMap.getCameraPosition()).thenReturn(CameraPosition.DEFAULT); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1000)); + LocationCameraController camera = buildCamera(mapboxMap, transform); + camera.initializeOptions(mock(LocationComponentOptions.class)); + Location location = mock(Location.class); + CameraUpdate cameraUpdate = CameraUpdateFactory.newCameraPosition( + new CameraPosition.Builder() + .target(new LatLng(location)) + .zoom(14.0) + .bearing(13.0) + .tilt(45.0) + .build() + ); + + camera.setCameraMode(TRACKING, location, 1200, 14.0, 13.0, 45.0, null); + verify(transform) + .animateCamera(eq(mapboxMap), eq(cameraUpdate), eq(1200), any(MapboxMap.CancelableCallback.class)); + } + + private LocationCameraController buildCamera(OnCameraTrackingChangedListener onCameraTrackingChangedListener) { + MapboxMap mapboxMap = mock(MapboxMap.class); + when(mapboxMap.getUiSettings()).thenReturn(mock(UiSettings.class)); + Transform transform = mock(Transform.class); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1000)); + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + OnCameraMoveInvalidateListener onCameraMoveInvalidateListener = mock(OnCameraMoveInvalidateListener.class); + AndroidGesturesManager initialGesturesManager = mock(AndroidGesturesManager.class); + AndroidGesturesManager internalGesturesManager = mock(AndroidGesturesManager.class); + return new LocationCameraController(mapboxMap, transform, moveGestureDetector, + onCameraTrackingChangedListener, onCameraMoveInvalidateListener, initialGesturesManager, internalGesturesManager); + } + + private LocationCameraController buildCamera(MoveGestureDetector moveGestureDetector) { + MapboxMap mapboxMap = mock(MapboxMap.class); + when(mapboxMap.getUiSettings()).thenReturn(mock(UiSettings.class)); + Transform transform = mock(Transform.class); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1000)); + OnCameraTrackingChangedListener onCameraTrackingChangedListener = mock(OnCameraTrackingChangedListener.class); + OnCameraMoveInvalidateListener onCameraMoveInvalidateListener = mock(OnCameraMoveInvalidateListener.class); + AndroidGesturesManager initialGesturesManager = mock(AndroidGesturesManager.class); + AndroidGesturesManager internalGesturesManager = mock(AndroidGesturesManager.class); + return new LocationCameraController(mapboxMap, transform, moveGestureDetector, + onCameraTrackingChangedListener, onCameraMoveInvalidateListener, initialGesturesManager, internalGesturesManager); + } + + private LocationCameraController buildCamera(MapboxMap mapboxMap) { + Transform transform = mock(Transform.class); + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + OnCameraTrackingChangedListener onCameraTrackingChangedListener = mock(OnCameraTrackingChangedListener.class); + OnCameraMoveInvalidateListener onCameraMoveInvalidateListener = mock(OnCameraMoveInvalidateListener.class); + AndroidGesturesManager initialGesturesManager = mock(AndroidGesturesManager.class); + AndroidGesturesManager internalGesturesManager = mock(AndroidGesturesManager.class); + return new LocationCameraController(mapboxMap, transform, moveGestureDetector, + onCameraTrackingChangedListener, onCameraMoveInvalidateListener, initialGesturesManager, internalGesturesManager); + } + + private LocationCameraController buildCamera(Transform transform) { + MapboxMap mapboxMap = mock(MapboxMap.class); + when(mapboxMap.getUiSettings()).thenReturn(mock(UiSettings.class)); + Projection projection = mock(Projection.class); + when(mapboxMap.getProjection()).thenReturn(projection); + when(projection.getMetersPerPixelAtLatitude(any(Double.class))).thenReturn(Double.valueOf(1000)); + when(mapboxMap.getUiSettings()).thenReturn(mock(UiSettings.class)); + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + OnCameraTrackingChangedListener onCameraTrackingChangedListener = mock(OnCameraTrackingChangedListener.class); + OnCameraMoveInvalidateListener onCameraMoveInvalidateListener = mock(OnCameraMoveInvalidateListener.class); + AndroidGesturesManager initialGesturesManager = mock(AndroidGesturesManager.class); + AndroidGesturesManager internalGesturesManager = mock(AndroidGesturesManager.class); + return new LocationCameraController(mapboxMap, transform, moveGestureDetector, + onCameraTrackingChangedListener, onCameraMoveInvalidateListener, initialGesturesManager, internalGesturesManager); + } + + private LocationCameraController buildCamera(MapboxMap mapboxMap, Transform transform) { + when(mapboxMap.getUiSettings()).thenReturn(mock(UiSettings.class)); + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + OnCameraTrackingChangedListener onCameraTrackingChangedListener = mock(OnCameraTrackingChangedListener.class); + OnCameraMoveInvalidateListener onCameraMoveInvalidateListener = mock(OnCameraMoveInvalidateListener.class); + AndroidGesturesManager initialGesturesManager = mock(AndroidGesturesManager.class); + AndroidGesturesManager internalGesturesManager = mock(AndroidGesturesManager.class); + return new LocationCameraController(mapboxMap, transform, moveGestureDetector, + onCameraTrackingChangedListener, onCameraMoveInvalidateListener, initialGesturesManager, internalGesturesManager); + } + + private LocationCameraController buildCamera(MapboxMap mapboxMap, AndroidGesturesManager initialGesturesManager, + AndroidGesturesManager internalGesturesManager) { + Transform transform = mock(Transform.class); + MoveGestureDetector moveGestureDetector = mock(MoveGestureDetector.class); + OnCameraTrackingChangedListener onCameraTrackingChangedListener = mock(OnCameraTrackingChangedListener.class); + OnCameraMoveInvalidateListener onCameraMoveInvalidateListener = mock(OnCameraMoveInvalidateListener.class); + return new LocationCameraController(mapboxMap, transform, moveGestureDetector, + onCameraTrackingChangedListener, onCameraMoveInvalidateListener, initialGesturesManager, internalGesturesManager); + } + + private MapboxAnimator.AnimationsValueChangeListener getAnimationListener( + @MapboxAnimator.Type int animatorType, + Set holders) { + for (AnimatorListenerHolder holder : holders) { + @MapboxAnimator.Type int type = holder.getAnimatorType(); + if (type == animatorType) { + return holder.getListener(); + } + } + + return null; + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptionsTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptionsTest.java new file mode 100644 index 000000000..2660c819d --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptionsTest.java @@ -0,0 +1,119 @@ +package com.mapbox.mapboxsdk.location; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.support.annotation.NonNull; + +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.maps.Style; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.MockitoRule; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class LocationComponentActivationOptionsTest { + + @Mock + private Context context; + @Mock + private TypedArray array; + @Mock + private Resources resources; + @Mock + private Style style; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @NonNull + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() throws Exception { + when(context.obtainStyledAttributes(R.style.mapbox_LocationComponent, R.styleable.mapbox_LocationComponent)) + .thenReturn(array); + when(array.getResourceId(R.styleable.mapbox_LocationComponent_mapbox_foregroundDrawable, -1)) + .thenReturn(R.drawable.mapbox_user_icon); + when(context.getResources()).thenReturn(resources); + } + + @Test + public void sanity() throws Exception { + when(style.isFullyLoaded()).thenReturn(true); + + LocationComponentOptions locationComponentOptions = LocationComponentOptions.builder(context) + .accuracyAlpha(0.5f) + .build(); + assertNotNull(locationComponentOptions); + + LocationComponentActivationOptions locationComponentActivationOptions = + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useDefaultLocationEngine(true) + .build(); + assertNotNull(locationComponentActivationOptions); + } + + @Test + public void includingBothStyleResAndComponentOptions_causesExceptionToBeThrown() throws Exception { + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("You've provided both a style resource and a LocationComponentOptions" + + " object to the LocationComponentActivationOptions builder. You can't use both and " + + "you must choose one of the two to style the LocationComponent."); + + LocationComponentOptions locationComponentOptions = LocationComponentOptions.builder(context) + .accuracyAlpha(0.5f) + .build(); + + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(locationComponentOptions) + .styleRes(R.style.mapbox_LocationComponent) + .build(); + } + + @Test + public void nullContext_causesExceptionToBeThrown() throws Exception { + thrown.expect(NullPointerException.class); + thrown.expectMessage("Context in LocationComponentActivationOptions is null."); + + LocationComponentActivationOptions.builder(null, style) + .build(); + } + + @Test + public void nullStyle_causesExceptionToBeThrown() throws Exception { + thrown.expect(NullPointerException.class); + thrown.expectMessage("Style in LocationComponentActivationOptions is null. Make sure the Style object isn't null." + + " Wait for the map to fully load before passing the Style object to LocationComponentActivationOptions."); + + LocationComponentActivationOptions.builder(context, null) + .build(); + } + + @Test + public void locationComponent_exceptionThrownWithDefaultLocationEngineButNotFullyLoadedStyle() throws Exception { + + when(style.isFullyLoaded()).thenReturn(false); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Style in LocationComponentActivationOptions isn't fully loaded. Wait for the " + + "map to fully load before passing the Style object to " + + "LocationComponentActivationOptions."); + + LocationComponentActivationOptions.builder(context, style) + .build(); + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentOptionsTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentOptionsTest.java new file mode 100644 index 000000000..b1e1f14cb --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentOptionsTest.java @@ -0,0 +1,81 @@ +package com.mapbox.mapboxsdk.location; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; + +import android.support.annotation.NonNull; +import com.mapbox.mapboxsdk.R; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class LocationComponentOptionsTest { + + @Mock + private Context context; + @Mock + private TypedArray array; + @Mock + private Resources resources; + + @NonNull + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() throws Exception { + when(context.obtainStyledAttributes(R.style.mapbox_LocationComponent, R.styleable.mapbox_LocationComponent)) + .thenReturn(array); + when(array.getResourceId(R.styleable.mapbox_LocationComponent_mapbox_foregroundDrawable, -1)) + .thenReturn(R.drawable.mapbox_user_icon); + when(context.getResources()).thenReturn(resources); + } + + @Test + public void sanity() throws Exception { + LocationComponentOptions locationComponentOptions = LocationComponentOptions.builder(context) + .accuracyAlpha(0.5f) + .build(); + assertNotNull(locationComponentOptions); + } + + @Test + public void passingOutOfRangeAccuracyAlpha_throwsException() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Accuracy alpha value must be between 0.0 and " + + "1.0."); + LocationComponentOptions.builder(context) + .accuracyAlpha(2f) + .build(); + } + + @Test + public void negativeElevation_causesExceptionToBeThrown() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Invalid shadow size -500.0. Must be >= 0"); + LocationComponentOptions.builder(context) + .elevation(-500) + .build(); + } + + @Test + public void passingBothLayerPositionOptions_throwsException() throws Exception { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("You cannot set both layerAbove and layerBelow options." + + "Choose one or the other."); + LocationComponentOptions.builder(context) + .layerAbove("above") + .layerBelow("below") + .build(); + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentPositionManagerTest.kt b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentPositionManagerTest.kt new file mode 100644 index 000000000..c8a75b670 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentPositionManagerTest.kt @@ -0,0 +1,130 @@ +package com.mapbox.mapboxsdk.location + +import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.mapboxsdk.style.layers.Layer +import io.mockk.mockk +import io.mockk.verify +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class LocationComponentPositionManagerTest { + + private lateinit var style: Style + private lateinit var layer: Layer + + @Before + fun setup() { + style = mockk(relaxed = true) + layer = mockk(relaxed = true) + } + + @Test + fun update_noChange_null() { + val positionManager = LocationComponentPositionManager(style, null, null) + val requiresUpdate = positionManager.update(null, null) + assertFalse(requiresUpdate) + } + + @Test + fun update_noChange_above() { + val positionManager = LocationComponentPositionManager(style, "above", null) + val requiresUpdate = positionManager.update("above", null) + assertFalse(requiresUpdate) + } + + @Test + fun update_noChange_below() { + val positionManager = LocationComponentPositionManager(style, null, "below") + val requiresUpdate = positionManager.update(null, "below") + assertFalse(requiresUpdate) + } + + @Test + fun update_fromNull_above() { + val positionManager = LocationComponentPositionManager(style, null, null) + val requiresUpdate = positionManager.update("above", null) + assertTrue(requiresUpdate) + } + + @Test + fun update_fromNull_below() { + val positionManager = LocationComponentPositionManager(style, null, null) + val requiresUpdate = positionManager.update(null, "below") + assertTrue(requiresUpdate) + } + + @Test + fun update_toNull_above() { + val positionManager = LocationComponentPositionManager(style, "above", null) + val requiresUpdate = positionManager.update(null, null) + assertTrue(requiresUpdate) + } + + @Test + fun update_toNull_below() { + val positionManager = LocationComponentPositionManager(style, null, "below") + val requiresUpdate = positionManager.update(null, null) + assertTrue(requiresUpdate) + } + + @Test + fun update_fromValue_above() { + val positionManager = LocationComponentPositionManager(style, "above1", null) + val requiresUpdate = positionManager.update("above2", null) + assertTrue(requiresUpdate) + } + + @Test + fun update_fromValue_below() { + val positionManager = LocationComponentPositionManager(style, null, "below1") + val requiresUpdate = positionManager.update(null, "below2") + assertTrue(requiresUpdate) + } + + @Test + fun addLayer_noModifier() { + val positionManager = LocationComponentPositionManager(style, null, null) + positionManager.addLayerToMap(layer) + + verify { style.addLayer(layer) } + } + + @Test + fun addLayer_above() { + val positionManager = LocationComponentPositionManager(style, "above", null) + positionManager.addLayerToMap(layer) + + verify { style.addLayerAbove(layer, "above") } + } + + @Test + fun addLayer_below() { + val positionManager = LocationComponentPositionManager(style, null, "below") + positionManager.addLayerToMap(layer) + + verify { style.addLayerBelow(layer, "below") } + } + + @Test + fun addLayer_afterUpdate_above() { + val positionManager = LocationComponentPositionManager(style, null, null) + positionManager.update("above", null) + positionManager.addLayerToMap(layer) + + verify { style.addLayerAbove(layer, "above") } + } + + @Test + fun addLayer_afterUpdate_below() { + val positionManager = LocationComponentPositionManager(style, null, null) + positionManager.update(null, "below") + positionManager.addLayerToMap(layer) + + verify { style.addLayerBelow(layer, "below") } + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt new file mode 100644 index 000000000..2994c29d5 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt @@ -0,0 +1,453 @@ +package com.mapbox.mapboxsdk.location + +import android.content.Context +import android.content.res.Resources +import android.content.res.TypedArray +import android.location.Location +import android.os.Looper +import com.mapbox.android.core.location.LocationEngine +import com.mapbox.android.core.location.LocationEngineRequest +import com.mapbox.mapboxsdk.R +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.location.LocationComponentConstants.TRANSITION_ANIMATION_DURATION_MS +import com.mapbox.mapboxsdk.location.modes.CameraMode +import com.mapbox.mapboxsdk.location.modes.RenderMode +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.mapboxsdk.maps.Transform +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.MockitoAnnotations +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class LocationComponentTest { + private lateinit var locationComponent: LocationComponent + + @Mock + private lateinit var locationComponentOptions: LocationComponentOptions + + @Mock + private lateinit var mapboxMap: MapboxMap + + @Mock + private lateinit var transform: Transform + + @Mock + private lateinit var context: Context + + @Mock + private lateinit var locationEngine: LocationEngine + + @Mock + private lateinit var locationEngineRequest: LocationEngineRequest + + @Mock + private lateinit var currentListener: LocationComponent.CurrentLocationEngineCallback + + @Mock + private lateinit var lastListener: LocationComponent.LastLocationEngineCallback + + @Mock + private lateinit var compassEngine: CompassEngine + + @Mock + private lateinit var locationLayerController: LocationLayerController + + @Mock + private lateinit var locationCameraController: LocationCameraController + + @Mock + private lateinit var locationAnimatorCoordinator: LocationAnimatorCoordinator + + @Mock + private lateinit var staleStateManager: StaleStateManager + + @Mock + private lateinit var locationEngineProvider: LocationComponent.InternalLocationEngineProvider + + @Mock + private lateinit var style: Style + + private lateinit var developerAnimationListeners: List + + @Before + fun before() { + MockitoAnnotations.initMocks(this) + developerAnimationListeners = mutableListOf() + locationComponent = LocationComponent(mapboxMap, transform, developerAnimationListeners, currentListener, lastListener, locationLayerController, locationCameraController, locationAnimatorCoordinator, staleStateManager, compassEngine, locationEngineProvider) + doReturn(locationEngine).`when`(locationEngineProvider).getBestLocationEngine(context, false) + doReturn(style).`when`(mapboxMap).style + } + + @Test + fun activateWithRequestTest() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + + Assert.assertEquals(locationEngineRequest, locationComponent.locationEngineRequest) + + doReturn(mock(TypedArray::class.java)).`when`(context) + .obtainStyledAttributes(R.style.mapbox_LocationComponent, R.styleable.mapbox_LocationComponent) + + val resources = mock(Resources::class.java) + + doReturn(resources).`when`(context).resources + doReturn(0f).`when`(resources) + .getDimension(R.dimen.mapbox_locationComponentTrackingMultiFingerMoveThreshold) + doReturn(0f).`when`(resources) + .getDimension(R.dimen.mapbox_locationComponentTrackingMultiFingerMoveThreshold) + locationComponent.activateLocationComponent(context, mock(Style::class.java), true, locationEngineRequest) + Assert.assertEquals(locationEngineRequest, locationComponent.locationEngineRequest) + } + + @Test + fun activateWithDefaultLocationEngineRequestAndOptionsTestDefaultLocationEngine() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), true, locationEngineRequest, locationComponentOptions) + Assert.assertEquals(locationEngineRequest, locationComponent.locationEngineRequest) + Assert.assertNotNull(locationComponent.locationEngine) + } + + @Test + fun activateWithDefaultLocationEngineRequestAndOptionsTestCustomLocationEngine() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), false, locationEngineRequest, locationComponentOptions) + Assert.assertEquals(locationEngineRequest, locationComponent.locationEngineRequest) + Assert.assertNull(locationComponent.locationEngine) + } + + @Test + fun locationUpdatesWhenEnabledDisableTest() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + verify(locationEngine, times(0)).removeLocationUpdates(currentListener) + verify(locationEngine, times(0)).requestLocationUpdates(eq(locationEngineRequest), eq(currentListener), any(Looper::class.java)) + + locationComponent.onStart() + verify(locationEngine, times(0)).removeLocationUpdates(currentListener) + verify(locationEngine, times(0)).requestLocationUpdates(eq(locationEngineRequest), eq(currentListener), any(Looper::class.java)) + + locationComponent.isLocationComponentEnabled = true + verify(locationEngine).requestLocationUpdates(eq(locationEngineRequest), eq(currentListener), any(Looper::class.java)) + + locationComponent.isLocationComponentEnabled = false + verify(locationEngine).requestLocationUpdates(eq(locationEngineRequest), eq(currentListener), any(Looper::class.java)) + verify(locationEngine).removeLocationUpdates(currentListener) + } + + @Test + fun locationUpdatesWhenStartedStoppedTest() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + + locationComponent.onStop() + verify(locationEngine).removeLocationUpdates(currentListener) + + locationComponent.onStart() + verify(locationEngine, times(2)).requestLocationUpdates(eq(locationEngineRequest), eq(currentListener), any(Looper::class.java)) + } + + @Test + fun locationUpdatesWhenNewRequestTest() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + + val newRequest = mock(LocationEngineRequest::class.java) + locationComponent.locationEngineRequest = newRequest + verify(locationEngine).removeLocationUpdates(currentListener) + verify(locationEngine).requestLocationUpdates(eq(newRequest), eq(currentListener), any(Looper::class.java)) + } + + @Test + fun lastLocationUpdateOnStartTest() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + + verify(locationEngine).getLastLocation(lastListener) + } + + @Test + fun transitionCallbackFinishedTest() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + val listener = mock(OnLocationCameraTransitionListener::class.java) + + val callback = ArgumentCaptor.forClass(OnLocationCameraTransitionListener::class.java) + locationComponent.setCameraMode(CameraMode.TRACKING, listener) + verify(locationCameraController).setCameraMode(eq(CameraMode.TRACKING), any(), eq(TRANSITION_ANIMATION_DURATION_MS), isNull(), isNull(), isNull(), callback.capture()) + callback.value.onLocationCameraTransitionFinished(CameraMode.TRACKING) + + verify(listener).onLocationCameraTransitionFinished(CameraMode.TRACKING) + verify(locationAnimatorCoordinator).resetAllCameraAnimations(CameraPosition.DEFAULT, false) + } + + @Test + fun transitionCallbackCanceledTest() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + val listener = mock(OnLocationCameraTransitionListener::class.java) + + val callback = ArgumentCaptor.forClass(OnLocationCameraTransitionListener::class.java) + locationComponent.setCameraMode(CameraMode.TRACKING, listener) + verify(locationCameraController).setCameraMode(eq(CameraMode.TRACKING), any(), eq(TRANSITION_ANIMATION_DURATION_MS), isNull(), isNull(), isNull(), callback.capture()) + callback.value.onLocationCameraTransitionCanceled(CameraMode.TRACKING) + + verify(listener).onLocationCameraTransitionCanceled(CameraMode.TRACKING) + verify(locationAnimatorCoordinator).resetAllCameraAnimations(CameraPosition.DEFAULT, false) + } + + @Test + fun transitionCustomFinishedTest() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + val listener = mock(OnLocationCameraTransitionListener::class.java) + + val callback = ArgumentCaptor.forClass(OnLocationCameraTransitionListener::class.java) + locationComponent.setCameraMode(CameraMode.TRACKING, 1200, 14.0, 13.0, 45.0, listener) + verify(locationCameraController).setCameraMode(eq(CameraMode.TRACKING), any(), eq(1200L), eq(14.0), eq(13.0), eq(45.0), callback.capture()) + callback.value.onLocationCameraTransitionFinished(CameraMode.TRACKING) + + verify(listener).onLocationCameraTransitionFinished(CameraMode.TRACKING) + verify(locationAnimatorCoordinator).resetAllCameraAnimations(CameraPosition.DEFAULT, false) + } + + @Test + fun compass_listenWhenConsumedByNoneCamera() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + `when`(locationCameraController.isConsumingCompass).thenReturn(true) + locationComponent.cameraMode = CameraMode.NONE_COMPASS + verify(compassEngine).addCompassListener(any(CompassListener::class.java)) + } + + @Test + fun compass_listenWhenConsumedByTrackingCamera() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + `when`(locationCameraController.isConsumingCompass).thenReturn(true) + locationComponent.cameraMode = CameraMode.TRACKING_COMPASS + verify(compassEngine).addCompassListener(any(CompassListener::class.java)) + } + + @Test + fun compass_listenWhenConsumedByLayer() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + `when`(locationLayerController.isConsumingCompass).thenReturn(true) + locationComponent.renderMode = RenderMode.COMPASS + verify(compassEngine).addCompassListener(any(CompassListener::class.java)) + } + + @Test + fun compass_notListenWhenNotConsumed() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + `when`(locationLayerController.isConsumingCompass).thenReturn(false) + `when`(locationCameraController.isConsumingCompass).thenReturn(false) + locationComponent.renderMode = RenderMode.GPS + locationComponent.renderMode = RenderMode.NORMAL + locationComponent.cameraMode = CameraMode.TRACKING + locationComponent.cameraMode = CameraMode.NONE + locationComponent.cameraMode = CameraMode.NONE_GPS + locationComponent.cameraMode = CameraMode.TRACKING_GPS + locationComponent.cameraMode = CameraMode.TRACKING_GPS_NORTH + verify(compassEngine, never()).addCompassListener(any(CompassListener::class.java)) + } + + @Test + fun compass_removeListenerOnChange() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + `when`(locationLayerController.isConsumingCompass).thenReturn(true) + locationComponent.renderMode = RenderMode.COMPASS + `when`(locationLayerController.isConsumingCompass).thenReturn(false) + locationComponent.renderMode = RenderMode.NORMAL + verify(compassEngine).removeCompassListener(any(CompassListener::class.java)) + } + + @Test + fun compass_removeListenerOnStop() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + `when`(locationLayerController.isConsumingCompass).thenReturn(true) + locationComponent.renderMode = RenderMode.COMPASS + locationComponent.onStop() + verify(compassEngine).removeCompassListener(any(CompassListener::class.java)) + } + + @Test + fun compass_reAddListenerOnStart() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + `when`(locationLayerController.isConsumingCompass).thenReturn(true) + locationComponent.renderMode = RenderMode.COMPASS + locationComponent.onStop() + locationComponent.onStart() + verify(compassEngine, times(2)).addCompassListener(any(CompassListener::class.java)) + } + + @Test + fun compass_removeListenerOnStyleStartLoad() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + `when`(locationLayerController.isConsumingCompass).thenReturn(true) + locationComponent.renderMode = RenderMode.COMPASS + locationComponent.onStartLoadingMap() + verify(compassEngine).removeCompassListener(any(CompassListener::class.java)) + } + + @Test + fun compass_reAddListenerOnStyleLoadFinished() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + `when`(locationLayerController.isConsumingCompass).thenReturn(true) + locationComponent.renderMode = RenderMode.COMPASS + locationComponent.onStartLoadingMap() + locationComponent.onFinishLoadingStyle() + verify(compassEngine, times(2)).addCompassListener(any(CompassListener::class.java)) + } + + @Test + fun compass_reAddListenerOnlyWhenEnabled() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + `when`(locationLayerController.isConsumingCompass).thenReturn(true) + locationComponent.renderMode = RenderMode.COMPASS + locationComponent.isLocationComponentEnabled = false + + locationComponent.onStartLoadingMap() + locationComponent.onFinishLoadingStyle() + verify(compassEngine).addCompassListener(any(CompassListener::class.java)) + } + + @Test + fun compass_notAdListenerWhenDisabled() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.onStart() + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + `when`(locationLayerController.isConsumingCompass).thenReturn(true) + locationComponent.renderMode = RenderMode.COMPASS + verify(compassEngine, never()).addCompassListener(any(CompassListener::class.java)) + } + + @Test + fun compass_notAdListenerWhenStopped() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.isLocationComponentEnabled = true + `when`(mapboxMap.cameraPosition).thenReturn(CameraPosition.DEFAULT) + + `when`(locationLayerController.isConsumingCompass).thenReturn(true) + locationComponent.renderMode = RenderMode.COMPASS + verify(compassEngine, never()).addCompassListener(any(CompassListener::class.java)) + } + + @Test + fun developerAnimationCalled() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.isLocationComponentEnabled = true + for (listener in developerAnimationListeners) { + listener.onDeveloperAnimationStarted() + } + verify(locationCameraController).setCameraMode(eq(CameraMode.NONE), isNull(), eq(TRANSITION_ANIMATION_DURATION_MS), isNull(), isNull(), isNull(), any()) + } + + @Test + fun internal_cameraTrackingChangedListener_onCameraTrackingDismissed() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.isLocationComponentEnabled = true + + val cameraChangeListener: OnCameraTrackingChangedListener = mock(OnCameraTrackingChangedListener::class.java) + locationComponent.addOnCameraTrackingChangedListener(cameraChangeListener) + + locationComponent.cameraTrackingChangedListener.onCameraTrackingDismissed() + + verify(cameraChangeListener).onCameraTrackingDismissed() + } + + @Test + fun internal_cameraTrackingChangedListener_onCameraTrackingChanged() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.isLocationComponentEnabled = true + + val cameraValueListener: AnimatorListenerHolder = mock(AnimatorListenerHolder::class.java) + val layerValueListener: AnimatorListenerHolder = mock(AnimatorListenerHolder::class.java) + `when`(locationCameraController.animationListeners).thenReturn(setOf(cameraValueListener)) + `when`(locationLayerController.animationListeners).thenReturn(setOf(layerValueListener)) + val cameraChangeListener: OnCameraTrackingChangedListener = mock(OnCameraTrackingChangedListener::class.java) + locationComponent.addOnCameraTrackingChangedListener(cameraChangeListener) + + locationComponent.cameraTrackingChangedListener.onCameraTrackingChanged(CameraMode.TRACKING_GPS) + + verify(locationAnimatorCoordinator).cancelZoomAnimation() + verify(locationAnimatorCoordinator).cancelTiltAnimation() + verify(locationAnimatorCoordinator).updateAnimatorListenerHolders(eq(setOf(cameraValueListener, layerValueListener))) + verify(locationAnimatorCoordinator).resetAllCameraAnimations(any(), anyBoolean()) + verify(locationAnimatorCoordinator).resetAllLayerAnimations() + verify(cameraChangeListener).onCameraTrackingChanged(CameraMode.TRACKING_GPS) + } + + @Test + fun internal_renderModeChangedListener_onRenderModeChanged() { + locationComponent.activateLocationComponent(context, mock(Style::class.java), locationEngine, locationEngineRequest, locationComponentOptions) + locationComponent.isLocationComponentEnabled = true + + val cameraListener: AnimatorListenerHolder = mock(AnimatorListenerHolder::class.java) + val layerListener: AnimatorListenerHolder = mock(AnimatorListenerHolder::class.java) + `when`(locationCameraController.animationListeners).thenReturn(setOf(cameraListener)) + `when`(locationLayerController.animationListeners).thenReturn(setOf(layerListener)) + val renderChangeListener: OnRenderModeChangedListener = mock(OnRenderModeChangedListener::class.java) + locationComponent.addOnRenderModeChangedListener(renderChangeListener) + + locationComponent.renderModeChangedListener.onRenderModeChanged(RenderMode.NORMAL) + + verify(locationAnimatorCoordinator).updateAnimatorListenerHolders(eq(setOf(cameraListener, layerListener))) + verify(locationAnimatorCoordinator).resetAllCameraAnimations(any(), anyBoolean()) + verify(locationAnimatorCoordinator).resetAllLayerAnimations() + verify(renderChangeListener).onRenderModeChanged(RenderMode.NORMAL) + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java new file mode 100644 index 000000000..ed2d015d8 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java @@ -0,0 +1,718 @@ +package com.mapbox.mapboxsdk.location; + +import android.graphics.Bitmap; +import android.support.annotation.NonNull; + +import com.google.gson.JsonElement; +import com.mapbox.geojson.Feature; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.location.modes.RenderMode; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.Style; +import com.mapbox.mapboxsdk.style.layers.Layer; +import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.Set; + +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.ACCURACY_LAYER; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_LAYER; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_STALE_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BEARING_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BEARING_LAYER; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_LAYER; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.FOREGROUND_STALE_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.LOCATION_SOURCE; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_ACCURACY_RADIUS; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_COMPASS_BEARING; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON_OFFSET; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_GPS_BEARING; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_SHADOW_ICON_OFFSET; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_ICON; +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_LAYER; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_ACCURACY; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_COMPASS_BEARING; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_GPS_BEARING; +import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_LATLNG; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class LocationLayerControllerTest { + + private MapboxMap mapboxMap = mock(MapboxMap.class); + private Style style = mock(Style.class); + + @Before + public void before() { + when(mapboxMap.getStyle()).thenReturn(style); + } + + @Test + public void onInitialization_locationSourceIsAdded() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addSource(locationSource); + } + + @Test + public void onInitialization_shadowLayerIsAdded() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + Layer shadowLayer = mock(Layer.class); + when(sourceProvider.generateLayer(SHADOW_LAYER)).thenReturn(shadowLayer); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addLayerBelow(shadowLayer, BACKGROUND_LAYER); + } + + @Test + public void onInitialization_backgroundLayerIsAdded() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + Layer backgroundLayer = mock(Layer.class); + when(sourceProvider.generateLayer(BACKGROUND_LAYER)).thenReturn(backgroundLayer); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addLayerBelow(backgroundLayer, FOREGROUND_LAYER); + } + + @Test + public void onInitialization_foregroundLayerIsAdded() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + Layer foregroundLayer = mock(Layer.class); + when(sourceProvider.generateLayer(FOREGROUND_LAYER)).thenReturn(foregroundLayer); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addLayerBelow(foregroundLayer, BEARING_LAYER); + } + + @Test + public void onInitialization_bearingLayerIsAdded() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + Layer bearingLayer = mock(Layer.class); + when(sourceProvider.generateLayer(BEARING_LAYER)).thenReturn(bearingLayer); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + String layerBelow = "layer-below"; + when(options.layerBelow()).thenReturn(layerBelow); + + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addLayerBelow(bearingLayer, layerBelow); + } + + @Test + public void onInitialization_accuracyLayerIsAdded() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + Layer accuracyLayer = mock(Layer.class); + when(sourceProvider.generateAccuracyLayer()).thenReturn(accuracyLayer); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addLayerBelow(accuracyLayer, BACKGROUND_LAYER); + } + + @Test + public void onInitialization_numberOfCachedLayerIdsIsConstant() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + + LocationLayerController controller = + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + controller.initializeComponents(mapboxMap.getStyle(), options); + + assertEquals(5, controller.layerSet.size()); + } + + @Test + public void applyStyle_styleShadowWithValidElevation() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(mock(GeoJsonSource.class)); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + Bitmap bitmap = mock(Bitmap.class); + when(bitmapProvider.generateShadowBitmap(any(LocationComponentOptions.class))).thenReturn(bitmap); + LocationComponentOptions options = mock(LocationComponentOptions.class); + when(options.elevation()).thenReturn(2f); + + // Style is applied on initialization + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addImage(SHADOW_ICON, bitmap); + } + + @Test + public void applyStyle_ignoreStyleShadowWithInvalidElevation() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(mock(GeoJsonSource.class)); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + Bitmap bitmap = mock(Bitmap.class); + when(bitmapProvider.generateShadowBitmap(any(LocationComponentOptions.class))).thenReturn(bitmap); + LocationComponentOptions options = mock(LocationComponentOptions.class); + when(options.elevation()).thenReturn(0f); + + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style, times(0)).addImage(SHADOW_ICON, bitmap); + } + + @Test + public void applyStyle_styleForegroundFromOptions() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(mock(GeoJsonSource.class)); + LocationComponentOptions options = mock(LocationComponentOptions.class); + int drawableResId = 123; + int tintColor = 456; + when(options.foregroundDrawable()).thenReturn(drawableResId); + when(options.foregroundTintColor()).thenReturn(tintColor); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + Bitmap bitmap = mock(Bitmap.class); + when(bitmapProvider.generateBitmap(drawableResId, tintColor)).thenReturn(bitmap); + + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addImage(FOREGROUND_ICON, bitmap); + } + + @Test + public void applyStyle_styleForegroundStaleFromOptions() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(mock(GeoJsonSource.class)); + LocationComponentOptions options = mock(LocationComponentOptions.class); + int drawableResId = 123; + int tintColor = 456; + when(options.foregroundDrawableStale()).thenReturn(drawableResId); + when(options.foregroundStaleTintColor()).thenReturn(tintColor); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + Bitmap bitmap = mock(Bitmap.class); + when(bitmapProvider.generateBitmap(drawableResId, tintColor)).thenReturn(bitmap); + + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addImage(FOREGROUND_STALE_ICON, bitmap); + } + + @Test + public void applyStyle_styleBackgroundFromOptions() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(mock(GeoJsonSource.class)); + LocationComponentOptions options = mock(LocationComponentOptions.class); + int drawableResId = 123; + int tintColor = 456; + when(options.backgroundDrawable()).thenReturn(drawableResId); + when(options.backgroundTintColor()).thenReturn(tintColor); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + Bitmap bitmap = mock(Bitmap.class); + when(bitmapProvider.generateBitmap(drawableResId, tintColor)).thenReturn(bitmap); + + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addImage(BACKGROUND_ICON, bitmap); + } + + @Test + public void applyStyle_styleBackgroundStaleFromOptions() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(mock(GeoJsonSource.class)); + LocationComponentOptions options = mock(LocationComponentOptions.class); + int drawableResId = 123; + int tintColor = 456; + when(options.backgroundDrawableStale()).thenReturn(drawableResId); + when(options.backgroundStaleTintColor()).thenReturn(tintColor); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + Bitmap bitmap = mock(Bitmap.class); + when(bitmapProvider.generateBitmap(drawableResId, tintColor)).thenReturn(bitmap); + + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addImage(BACKGROUND_STALE_ICON, bitmap); + } + + @Test + public void applyStyle_styleBearingFromOptions() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(mock(GeoJsonSource.class)); + LocationComponentOptions options = mock(LocationComponentOptions.class); + int drawableResId = 123; + int tintColor = 456; + when(options.bearingDrawable()).thenReturn(drawableResId); + when(options.bearingTintColor()).thenReturn(tintColor); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + Bitmap bitmap = mock(Bitmap.class); + when(bitmapProvider.generateBitmap(drawableResId, tintColor)).thenReturn(bitmap); + + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addImage(BEARING_ICON, bitmap); + } + + @Test + public void applyStyle_layerBelowChanged() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(mock(GeoJsonSource.class)); + + LocationComponentOptions options = mock(LocationComponentOptions.class); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + Bitmap bitmap = mock(Bitmap.class); + when(bitmapProvider.generateShadowBitmap(any(LocationComponentOptions.class))).thenReturn(bitmap); + + LocationLayerController layerController = + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + Layer bearingLayer2 = mock(Layer.class); + when(sourceProvider.generateLayer(BEARING_LAYER)).thenReturn(bearingLayer2); + Layer foregroundLayer2 = mock(Layer.class); + when(sourceProvider.generateLayer(FOREGROUND_LAYER)).thenReturn(foregroundLayer2); + Layer backgroundLayer2 = mock(Layer.class); + when(sourceProvider.generateLayer(BACKGROUND_LAYER)).thenReturn(backgroundLayer2); + Layer shadowLayer2 = mock(Layer.class); + when(sourceProvider.generateLayer(SHADOW_LAYER)).thenReturn(shadowLayer2); + Layer accuracyLayer2 = mock(Layer.class); + when(sourceProvider.generateAccuracyLayer()).thenReturn(accuracyLayer2); + String layerBelow = "layer-below"; + when(options.layerBelow()).thenReturn(layerBelow); + + layerController.applyStyle(options); + + verify(style).removeLayer(BEARING_LAYER); + verify(style).removeLayer(FOREGROUND_LAYER); + verify(style).removeLayer(BACKGROUND_LAYER); + verify(style).removeLayer(SHADOW_LAYER); + verify(style).removeLayer(ACCURACY_LAYER); + + verify(style).addLayerBelow(bearingLayer2, layerBelow); + verify(style).addLayerBelow(foregroundLayer2, BEARING_LAYER); + verify(style).addLayerBelow(backgroundLayer2, FOREGROUND_LAYER); + verify(style).addLayerBelow(shadowLayer2, BACKGROUND_LAYER); + verify(style).addLayerBelow(accuracyLayer2, BACKGROUND_LAYER); + } + + @Test + public void applyStyle_layerBelowNotChanged() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(mock(GeoJsonSource.class)); + LocationComponentOptions options = mock(LocationComponentOptions.class); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + Bitmap bitmap = mock(Bitmap.class); + when(bitmapProvider.generateShadowBitmap(any(LocationComponentOptions.class))).thenReturn(bitmap); + + String layerBelow = "layer-below"; + when(options.layerBelow()).thenReturn(layerBelow); + + LocationLayerController layerController = + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + options = mock(LocationComponentOptions.class); + layerBelow = "layer-below"; + when(options.layerBelow()).thenReturn(layerBelow); + layerController.applyStyle(options); + + verify(style, times(0)).removeLayer(any(String.class)); + verify(style, times(5)).addLayerBelow(any(Layer.class), any(String.class)); + } + + @Test + public void applyStyle_layerBelowNotChangedNull() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(mock(GeoJsonSource.class)); + LocationComponentOptions options = mock(LocationComponentOptions.class); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + Bitmap bitmap = mock(Bitmap.class); + when(bitmapProvider.generateShadowBitmap(any(LocationComponentOptions.class))).thenReturn(bitmap); + + LocationLayerController layerController = + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + options = mock(LocationComponentOptions.class); + layerController.applyStyle(options); + + verify(style, times(0)).removeLayer(any(String.class)); + verify(style, times(1)).addLayer(any(Layer.class)); + verify(style, times(4)).addLayerBelow(any(Layer.class), Mockito.any()); + } + + @Test + public void updateForegroundOffset_foregroundIconPropertyIsUpdated() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + Feature locationFeature = mock(Feature.class); + LocationLayerController layer = new LocationLayerController( + mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(locationFeature, options), + bitmapProvider, options, internalRenderModeChangedListener + ); + + layer.updateForegroundOffset(2d); + + verify(locationFeature).addProperty(eq(PROPERTY_FOREGROUND_ICON_OFFSET), any(JsonElement.class)); + } + + @Test + public void updateForegroundOffset_shadowPropertyIsUpdated() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + Feature locationFeature = mock(Feature.class); + LocationLayerController layer = new LocationLayerController( + mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(locationFeature, options), + bitmapProvider, options, internalRenderModeChangedListener + ); + layer.updateForegroundOffset(2d); + + verify(locationFeature).addProperty(eq(PROPERTY_SHADOW_ICON_OFFSET), any(JsonElement.class)); + } + + @Test + public void onNewLatLngValue_locationFeatureIsUpdated() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(style.getSourceAs(LOCATION_SOURCE)).thenReturn(locationSource); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + Feature locationFeature = mock(Feature.class); + LocationLayerController layer = new LocationLayerController( + mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(locationFeature, options), + bitmapProvider, options, internalRenderModeChangedListener + ); + + getAnimationListener(ANIMATOR_LAYER_LATLNG, layer.getAnimationListeners()).onNewAnimationValue(new LatLng()); + + // wanted twice (once for initialization) + verify(locationSource, times(2)).setGeoJson(locationFeature); + } + + @Test + public void onNewGpsBearingValue_locationFeatureIsUpdated() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(style.getSourceAs(LOCATION_SOURCE)).thenReturn(locationSource); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + Feature locationFeature = mock(Feature.class); + LocationLayerController layer = new LocationLayerController( + mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(locationFeature, options), + bitmapProvider, options, internalRenderModeChangedListener + ); + layer.setRenderMode(RenderMode.GPS); + float gpsBearing = 2f; + + getAnimationListener(ANIMATOR_LAYER_GPS_BEARING, layer.getAnimationListeners()).onNewAnimationValue(gpsBearing); + + verify(locationFeature).addNumberProperty(PROPERTY_GPS_BEARING, gpsBearing); + } + + @Test + public void onNewGpsBearingValue_updateIgnoredWithInvalidRenderMode() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(style.getSourceAs(LOCATION_SOURCE)).thenReturn(locationSource); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + Feature locationFeature = mock(Feature.class); + LocationLayerController layer = new LocationLayerController( + mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(locationFeature, options), + bitmapProvider, options, internalRenderModeChangedListener + ); + layer.setRenderMode(RenderMode.COMPASS); + float gpsBearing = 2f; + + assertNull(getAnimationListener(ANIMATOR_LAYER_GPS_BEARING, layer.getAnimationListeners())); + verify(locationFeature, times(0)).addNumberProperty(PROPERTY_GPS_BEARING, gpsBearing); + } + + @Test + public void onNewCompassBearingValue_locationFeatureIsUpdated() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(style.getSourceAs(LOCATION_SOURCE)).thenReturn(locationSource); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + Feature locationFeature = mock(Feature.class); + LocationLayerController layer = new LocationLayerController( + mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(locationFeature, options), + bitmapProvider, options, internalRenderModeChangedListener + ); + layer.setRenderMode(RenderMode.COMPASS); + float compassBearing = 2f; + + getAnimationListener(ANIMATOR_LAYER_COMPASS_BEARING, layer.getAnimationListeners()) + .onNewAnimationValue(compassBearing); + + verify(locationFeature).addNumberProperty(PROPERTY_COMPASS_BEARING, compassBearing); + } + + @Test + public void onNewCompassBearingValue_updateIgnoredWithInvalidRenderMode() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(style.getSourceAs(LOCATION_SOURCE)).thenReturn(locationSource); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + Feature locationFeature = mock(Feature.class); + LocationLayerController layer = new LocationLayerController( + mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(locationFeature, options), + bitmapProvider, options, internalRenderModeChangedListener + ); + layer.setRenderMode(RenderMode.GPS); + float compassBearing = 2f; + + assertNull(getAnimationListener(ANIMATOR_LAYER_COMPASS_BEARING, layer.getAnimationListeners())); + verify(locationFeature, times(0)).addNumberProperty(PROPERTY_COMPASS_BEARING, compassBearing); + } + + @Test + public void onNewAccuracyRadiusValue_locationFeatureIsUpdated() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(style.getSourceAs(LOCATION_SOURCE)).thenReturn(locationSource); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + Feature locationFeature = mock(Feature.class); + LocationLayerController layer = new LocationLayerController( + mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(locationFeature, options), + bitmapProvider, options, internalRenderModeChangedListener + ); + layer.setRenderMode(RenderMode.NORMAL); + float accuracyRadiusValue = 2f; + + getAnimationListener(ANIMATOR_LAYER_ACCURACY, layer.getAnimationListeners()) + .onNewAnimationValue(accuracyRadiusValue); + + verify(locationFeature).addNumberProperty(PROPERTY_ACCURACY_RADIUS, accuracyRadiusValue); + } + + @Test + public void onNewAccuracyRadiusValue_updateIgnoredWithInvalidRenderMode() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(style.getSourceAs(LOCATION_SOURCE)).thenReturn(locationSource); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + Feature locationFeature = mock(Feature.class); + LocationLayerController layer = new LocationLayerController( + mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(locationFeature, options), + bitmapProvider, options, internalRenderModeChangedListener + ); + layer.setRenderMode(RenderMode.GPS); + float accuracyRadiusValue = 2f; + + assertNull(getAnimationListener(ANIMATOR_LAYER_ACCURACY, layer.getAnimationListeners())); + verify(locationFeature, times(0)) + .addNumberProperty(PROPERTY_ACCURACY_RADIUS, accuracyRadiusValue); + } + + @Test + public void renderModeChanged_doNotNotifyAboutDuplicates_NORMAL() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + + LocationLayerController controller = + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + controller.setRenderMode(RenderMode.NORMAL); + controller.setRenderMode(RenderMode.NORMAL); + + verify(internalRenderModeChangedListener, times(1)).onRenderModeChanged(RenderMode.NORMAL); + } + + @Test + public void renderModeChanged_doNotNotifyAboutDuplicates_GPS() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + GeoJsonSource locationSource = mock(GeoJsonSource.class); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + LocationComponentOptions options = mock(LocationComponentOptions.class); + + LocationLayerController controller = + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + controller.setRenderMode(RenderMode.GPS); + controller.setRenderMode(RenderMode.GPS); + + verify(internalRenderModeChangedListener, times(1)).onRenderModeChanged(RenderMode.GPS); + } + + @Test + public void layerHidden_renderModeChanged_layerShown_foregroundIconUpdated() { + OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); + LayerSourceProvider sourceProvider = buildLayerProvider(); + when(sourceProvider.generateSource(any(Feature.class))).thenReturn(mock(GeoJsonSource.class)); + LocationComponentOptions options = mock(LocationComponentOptions.class); + int drawableResId = 123; + int tintColor = 456; + when(options.foregroundDrawable()).thenReturn(drawableResId); + when(options.foregroundTintColor()).thenReturn(tintColor); + LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class); + Bitmap bitmap = mock(Bitmap.class); + when(bitmapProvider.generateBitmap(drawableResId, tintColor)).thenReturn(bitmap); + + LocationLayerController controller = + new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options), + bitmapProvider, options, internalRenderModeChangedListener); + + verify(style).addImage(FOREGROUND_ICON, bitmap); + + int drawableGpsResId = 789; + when(options.gpsDrawable()).thenReturn(drawableGpsResId); + + Bitmap bitmapGps = mock(Bitmap.class); + when(bitmapProvider.generateBitmap(drawableGpsResId, tintColor)).thenReturn(bitmapGps); + + controller.hide(); + + controller.setRenderMode(RenderMode.GPS); + + controller.show(); + + verify(style).addImage(FOREGROUND_ICON, bitmapGps); + } + + private LayerFeatureProvider buildFeatureProvider(@NonNull LocationComponentOptions options) { + LayerFeatureProvider provider = mock(LayerFeatureProvider.class); + when(provider.generateLocationFeature(null, options)).thenReturn(mock(Feature.class)); + return provider; + } + + private LayerFeatureProvider buildFeatureProvider(Feature feature, @NonNull LocationComponentOptions options) { + LayerFeatureProvider provider = mock(LayerFeatureProvider.class); + when(provider.generateLocationFeature(null, options)).thenReturn(feature); + return provider; + } + + private LayerSourceProvider buildLayerProvider() { + LayerSourceProvider layerSourceProvider = mock(LayerSourceProvider.class); + + Layer shadowLayer = mock(Layer.class); + when(shadowLayer.getId()).thenReturn(SHADOW_LAYER); + when(layerSourceProvider.generateLayer(SHADOW_LAYER)).thenReturn(shadowLayer); + + Layer backgroundLayer = mock(Layer.class); + when(backgroundLayer.getId()).thenReturn(BACKGROUND_LAYER); + when(layerSourceProvider.generateLayer(BACKGROUND_LAYER)).thenReturn(backgroundLayer); + + Layer foregroundLayer = mock(Layer.class); + when(foregroundLayer.getId()).thenReturn(FOREGROUND_LAYER); + when(layerSourceProvider.generateLayer(FOREGROUND_LAYER)).thenReturn(foregroundLayer); + + Layer bearingLayer = mock(Layer.class); + when(bearingLayer.getId()).thenReturn(BEARING_LAYER); + when(layerSourceProvider.generateLayer(BEARING_LAYER)).thenReturn(bearingLayer); + + Layer accuracyLayer = mock(Layer.class); + when(accuracyLayer.getId()).thenReturn(ACCURACY_LAYER); + when(layerSourceProvider.generateAccuracyLayer()).thenReturn(accuracyLayer); + return layerSourceProvider; + } + + private MapboxAnimator.AnimationsValueChangeListener getAnimationListener( + @MapboxAnimator.Type int animatorType, + Set holders) { + for (AnimatorListenerHolder holder : holders) { + @MapboxAnimator.Type int type = holder.getAnimatorType(); + if (type == animatorType) { + return holder.getListener(); + } + } + return null; + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/MapboxAnimatorTest.kt b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/MapboxAnimatorTest.kt new file mode 100644 index 000000000..7c126c783 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/MapboxAnimatorTest.kt @@ -0,0 +1,43 @@ +package com.mapbox.mapboxsdk.location + +import android.animation.ValueAnimator +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MapboxAnimatorTest { + + @Test + fun fps_unlimited() { + val valueAnimator = mockk() + every { valueAnimator.animatedValue } answers { 5f } + val listener = mockk>() + every { listener.onNewAnimationValue(any()) } answers {} + val mapboxAnimator = MapboxFloatAnimator(0f, 10f, listener, Int.MAX_VALUE) + + for (i in 0 until 5) + mapboxAnimator.onAnimationUpdate(valueAnimator) + + verify(exactly = 5) { listener.onNewAnimationValue(5f) } + } + + @Test + fun fps_limited() { + val valueAnimator = mockk() + every { valueAnimator.animatedValue } answers { 5f } + val listener = mockk>() + every { listener.onNewAnimationValue(any()) } answers {} + val mapboxAnimator = MapboxFloatAnimator(0f, 10f, listener, 5) + + for (i in 0 until 5) { + mapboxAnimator.onAnimationUpdate(valueAnimator) + Thread.sleep(150) + } + + verify(exactly = 3) { listener.onNewAnimationValue(5f) } + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/UtilsTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/UtilsTest.java new file mode 100644 index 000000000..bf5ee40f4 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/UtilsTest.java @@ -0,0 +1,28 @@ +package com.mapbox.mapboxsdk.location; + +import org.junit.Test; + +import static junit.framework.Assert.assertEquals; + +public final class UtilsTest { + + @Test + public void shortestRotation_doesReturnValueDistanceQuickestToZero() throws Exception { + float value = Utils.shortestRotation(0, 181); + assertEquals(360f, value); + value = Utils.shortestRotation(0, 179); + assertEquals(0f, value); + value = Utils.shortestRotation(0, 180); + assertEquals(0f, value); + } + + @Test + public void shortestRotation_doesReturnValueDistanceQuickestToFifty() throws Exception { + float value = Utils.shortestRotation(50, 231); + assertEquals(410f, value); + value = Utils.shortestRotation(50, 229); + assertEquals(50f, value); + value = Utils.shortestRotation(50, 180); + assertEquals(50f, value); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/log/LoggerTest.kt b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/log/LoggerTest.kt new file mode 100644 index 000000000..82e1c67f6 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/log/LoggerTest.kt @@ -0,0 +1,93 @@ +package com.mapbox.mapboxsdk.log + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class LoggerTest { + + private val logger: LoggerDefinition = mockk(relaxed = true) + + @Before + fun setUp() { + Logger.setLoggerDefinition(logger) + } + + @Test + fun verbosityLogLevel() { + Logger.setVerbosity(Logger.VERBOSE) + Logger.v(TAG, MESSAGE) + verify { logger.v(TAG, MESSAGE) } + } + + @Test + fun verbosityLogLevelIgnore() { + Logger.setVerbosity(Logger.DEBUG) + Logger.v(TAG, MESSAGE) + verify(exactly = 0) { logger.v(TAG, MESSAGE) } + } + + @Test + fun debugLogLevel() { + Logger.setVerbosity(Logger.VERBOSE) + Logger.d(TAG, MESSAGE) + verify { logger.d(TAG, MESSAGE) } + } + + @Test + fun debugLogLevelIgnore() { + Logger.setVerbosity(Logger.WARN) + Logger.d(TAG, MESSAGE) + verify(exactly = 0) { logger.d(TAG, MESSAGE) } + } + + @Test + fun warnLogLevel() { + Logger.setVerbosity(Logger.WARN) + Logger.w(TAG, MESSAGE) + verify { logger.w(TAG, MESSAGE) } + } + + @Test + fun warnLogLevelIgnore() { + Logger.setVerbosity(Logger.ERROR) + Logger.w(TAG, MESSAGE) + verify(exactly = 0) { logger.w(TAG, MESSAGE) } + } + + @Test + fun errorLogLevel() { + Logger.setVerbosity(Logger.ERROR) + Logger.e(TAG, MESSAGE) + verify { logger.e(TAG, MESSAGE) } + } + + @Test + fun errorLogLevelIgnore() { + Logger.setVerbosity(Logger.NONE) + Logger.e(TAG, MESSAGE) + verify(exactly = 0) { logger.e(TAG, MESSAGE) } + } + + @Test + fun noneLogLevelIgnore() { + Logger.setVerbosity(Logger.NONE) + Logger.v(TAG, MESSAGE) + Logger.d(TAG, MESSAGE) + Logger.w(TAG, MESSAGE) + Logger.e(TAG, MESSAGE) + verify(exactly = 0) { logger.v(TAG, MESSAGE) } + verify(exactly = 0) { logger.d(TAG, MESSAGE) } + verify(exactly = 0) { logger.w(TAG, MESSAGE) } + verify(exactly = 0) { logger.e(TAG, MESSAGE) } + } + + companion object { + const val TAG: String = "TAG" + const val MESSAGE: String = "message" + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/AnnotationManagerTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/AnnotationManagerTest.java new file mode 100644 index 000000000..fd59f52a5 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/AnnotationManagerTest.java @@ -0,0 +1,86 @@ +package com.mapbox.mapboxsdk.maps; + +import android.support.v4.util.LongSparseArray; + +import com.mapbox.mapboxsdk.annotations.Annotation; +import com.mapbox.mapboxsdk.annotations.BaseMarkerOptions; +import com.mapbox.mapboxsdk.annotations.Marker; +import com.mapbox.mapboxsdk.annotations.MarkerOptions; +import com.mapbox.mapboxsdk.geometry.LatLng; + +import org.junit.Test; +import org.mockito.ArgumentMatchers; + +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AnnotationManagerTest { + + @Test + public void checksAddAMarker() throws Exception { + NativeMap aNativeMapView = mock(NativeMapView.class); + MapView aMapView = mock(MapView.class); + LongSparseArray annotationsArray = new LongSparseArray<>(); + IconManager aIconManager = mock(IconManager.class); + Annotations annotations = new AnnotationContainer(aNativeMapView, annotationsArray); + Markers markers = new MarkerContainer(aNativeMapView, annotationsArray, aIconManager); + Polygons polygons = new PolygonContainer(aNativeMapView, annotationsArray); + Polylines polylines = new PolylineContainer(aNativeMapView, annotationsArray); + ShapeAnnotations shapeAnnotations = new ShapeAnnotationContainer(aNativeMapView, annotationsArray); + AnnotationManager annotationManager = new AnnotationManager(aMapView, annotationsArray, + aIconManager, annotations, markers, polygons, polylines, shapeAnnotations); + Marker aMarker = mock(Marker.class); + long aId = 5L; + when(aNativeMapView.addMarker(aMarker)).thenReturn(aId); + BaseMarkerOptions aMarkerOptions = mock(BaseMarkerOptions.class); + MapboxMap aMapboxMap = mock(MapboxMap.class); + when(aMarkerOptions.getMarker()).thenReturn(aMarker); + + annotationManager.addMarker(aMarkerOptions, aMapboxMap); + + assertEquals(aMarker, annotationManager.getAnnotations().get(0)); + assertEquals(aMarker, annotationManager.getAnnotation(aId)); + } + + @Test + public void checksAddMarkers() throws Exception { + NativeMapView aNativeMapView = mock(NativeMapView.class); + MapView aMapView = mock(MapView.class); + LongSparseArray annotationsArray = new LongSparseArray<>(); + IconManager aIconManager = mock(IconManager.class); + Annotations annotations = new AnnotationContainer(aNativeMapView, annotationsArray); + Markers markers = new MarkerContainer(aNativeMapView, annotationsArray, aIconManager); + Polygons polygons = new PolygonContainer(aNativeMapView, annotationsArray); + Polylines polylines = new PolylineContainer(aNativeMapView, annotationsArray); + ShapeAnnotations shapeAnnotations = new ShapeAnnotationContainer(aNativeMapView, annotationsArray); + AnnotationManager annotationManager = new AnnotationManager(aMapView, annotationsArray, + aIconManager, annotations, markers, polygons, polylines, shapeAnnotations); + + long firstId = 1L; + long secondId = 2L; + List markerList = new ArrayList<>(); + MarkerOptions firstMarkerOption = new MarkerOptions().position(new LatLng()).title("first"); + MarkerOptions secondMarkerOption = new MarkerOptions().position(new LatLng()).title("second"); + + markerList.add(firstMarkerOption); + markerList.add(secondMarkerOption); + MapboxMap aMapboxMap = mock(MapboxMap.class); + when(aNativeMapView.addMarker(any(Marker.class))).thenReturn(firstId, secondId); + + when(aNativeMapView.addMarkers(ArgumentMatchers.anyList())) + .thenReturn(new long[] {firstId, secondId}); + + annotationManager.addMarkers(markerList, aMapboxMap); + + assertEquals(2, annotationManager.getAnnotations().size()); + assertEquals("first", ((Marker) annotationManager.getAnnotations().get(0)).getTitle()); + assertEquals("second", ((Marker) annotationManager.getAnnotations().get(1)).getTitle()); + assertEquals("first", ((Marker) annotationManager.getAnnotation(firstId)).getTitle()); + assertEquals("second", ((Marker) annotationManager.getAnnotation(secondId)).getTitle()); + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapChangeReceiverTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapChangeReceiverTest.java new file mode 100644 index 000000000..025f06752 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapChangeReceiverTest.java @@ -0,0 +1,586 @@ +package com.mapbox.mapboxsdk.maps; + +import com.google.common.util.concurrent.ExecutionError; +import com.mapbox.mapboxsdk.log.Logger; +import com.mapbox.mapboxsdk.log.LoggerDefinition; + +import junit.framework.Assert; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +/** + * Tests integration of MapChangeDispatcher and see if events are correctly forwarded. + */ +public class MapChangeReceiverTest { + + private static final String TEST_STRING = "mapChangeRandom"; + + private MapChangeReceiver mapChangeEventManager; + + @Mock + private MapView.OnCameraWillChangeListener onCameraWillChangeListener; + + @Mock + private MapView.OnCameraDidChangeListener onCameraDidChangeListener; + + @Mock + private MapView.OnCameraIsChangingListener onCameraIsChangingListener; + + @Mock + private MapView.OnWillStartLoadingMapListener onWillStartLoadingMapListener; + + @Mock + private MapView.OnDidFinishLoadingMapListener onDidFinishLoadingMapListener; + + @Mock + private MapView.OnDidFailLoadingMapListener onDidFailLoadingMapListener; + + @Mock + private MapView.OnWillStartRenderingFrameListener onWillStartRenderingFrameListener; + + @Mock + private MapView.OnDidFinishRenderingFrameListener onDidFinishRenderingFrameListener; + + @Mock + private MapView.OnWillStartRenderingMapListener onWillStartRenderingMapListener; + + @Mock + private MapView.OnDidFinishRenderingMapListener onDidFinishRenderingMapListener; + + @Mock + private MapView.OnDidBecomeIdleListener onDidBecomeIdleListener; + + @Mock + private MapView.OnDidFinishLoadingStyleListener onDidFinishLoadingStyleListener; + + @Mock + private MapView.OnSourceChangedListener onSourceChangedListener; + + @Mock + private LoggerDefinition loggerDefinition; + + @Before + public void beforeTest() { + MockitoAnnotations.initMocks(this); + mapChangeEventManager = new MapChangeReceiver(); + } + + @Test + public void testOnCameraRegionWillChangeListener() { + mapChangeEventManager.addOnCameraWillChangeListener(onCameraWillChangeListener); + mapChangeEventManager.onCameraWillChange(false); + verify(onCameraWillChangeListener).onCameraWillChange(false); + + mapChangeEventManager.removeOnCameraWillChangeListener(onCameraWillChangeListener); + mapChangeEventManager.onCameraWillChange(false); + verify(onCameraWillChangeListener).onCameraWillChange(false); + + mapChangeEventManager.addOnCameraWillChangeListener(onCameraWillChangeListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onCameraWillChangeListener).onCameraWillChange(false); + try { + mapChangeEventManager.onCameraWillChange(false); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onCameraWillChangeListener).onCameraWillChange(false); + try { + mapChangeEventManager.onCameraWillChange(false); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnCameraRegionWillChangeAnimatedListener() { + mapChangeEventManager.addOnCameraWillChangeListener(onCameraWillChangeListener); + mapChangeEventManager.onCameraWillChange(true); + verify(onCameraWillChangeListener).onCameraWillChange(true); + mapChangeEventManager.removeOnCameraWillChangeListener(onCameraWillChangeListener); + mapChangeEventManager.onCameraWillChange(true); + verify(onCameraWillChangeListener).onCameraWillChange(true); + + mapChangeEventManager.addOnCameraWillChangeListener(onCameraWillChangeListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onCameraWillChangeListener).onCameraWillChange(true); + try { + mapChangeEventManager.onCameraWillChange(true); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onCameraWillChangeListener).onCameraWillChange(true); + try { + mapChangeEventManager.onCameraWillChange(true); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnCameraIsChangingListener() { + mapChangeEventManager.addOnCameraIsChangingListener(onCameraIsChangingListener); + mapChangeEventManager.onCameraIsChanging(); + verify(onCameraIsChangingListener).onCameraIsChanging(); + mapChangeEventManager.removeOnCameraIsChangingListener(onCameraIsChangingListener); + mapChangeEventManager.onCameraIsChanging(); + verify(onCameraIsChangingListener).onCameraIsChanging(); + + mapChangeEventManager.addOnCameraIsChangingListener(onCameraIsChangingListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onCameraIsChangingListener).onCameraIsChanging(); + try { + mapChangeEventManager.onCameraIsChanging(); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onCameraIsChangingListener).onCameraIsChanging(); + try { + mapChangeEventManager.onCameraIsChanging(); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnCameraRegionDidChangeListener() { + mapChangeEventManager.addOnCameraDidChangeListener(onCameraDidChangeListener); + mapChangeEventManager.onCameraDidChange(false); + verify(onCameraDidChangeListener).onCameraDidChange(false); + mapChangeEventManager.removeOnCameraDidChangeListener(onCameraDidChangeListener); + mapChangeEventManager.onCameraDidChange(false); + verify(onCameraDidChangeListener).onCameraDidChange(false); + + mapChangeEventManager.addOnCameraDidChangeListener(onCameraDidChangeListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onCameraDidChangeListener).onCameraDidChange(false); + try { + mapChangeEventManager.onCameraDidChange(false); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onCameraDidChangeListener).onCameraDidChange(false); + try { + mapChangeEventManager.onCameraDidChange(false); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnCameraRegionDidChangeAnimatedListener() { + mapChangeEventManager.addOnCameraDidChangeListener(onCameraDidChangeListener); + mapChangeEventManager.onCameraDidChange(true); + verify(onCameraDidChangeListener).onCameraDidChange(true); + mapChangeEventManager.removeOnCameraDidChangeListener(onCameraDidChangeListener); + mapChangeEventManager.onCameraDidChange(true); + verify(onCameraDidChangeListener).onCameraDidChange(true); + + mapChangeEventManager.addOnCameraDidChangeListener(onCameraDidChangeListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onCameraDidChangeListener).onCameraDidChange(true); + try { + mapChangeEventManager.onCameraDidChange(true); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onCameraDidChangeListener).onCameraDidChange(true); + try { + mapChangeEventManager.onCameraDidChange(true); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnWillStartLoadingMapListener() { + mapChangeEventManager.addOnWillStartLoadingMapListener(onWillStartLoadingMapListener); + mapChangeEventManager.onWillStartLoadingMap(); + verify(onWillStartLoadingMapListener).onWillStartLoadingMap(); + mapChangeEventManager.removeOnWillStartLoadingMapListener(onWillStartLoadingMapListener); + mapChangeEventManager.onWillStartLoadingMap(); + verify(onWillStartLoadingMapListener).onWillStartLoadingMap(); + + mapChangeEventManager.addOnWillStartLoadingMapListener(onWillStartLoadingMapListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onWillStartLoadingMapListener).onWillStartLoadingMap(); + try { + mapChangeEventManager.onWillStartLoadingMap(); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onWillStartLoadingMapListener).onWillStartLoadingMap(); + try { + mapChangeEventManager.onWillStartLoadingMap(); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnDidFinishLoadingMapListener() { + mapChangeEventManager.addOnDidFinishLoadingMapListener(onDidFinishLoadingMapListener); + mapChangeEventManager.onDidFinishLoadingMap(); + verify(onDidFinishLoadingMapListener).onDidFinishLoadingMap(); + mapChangeEventManager.removeOnDidFinishLoadingMapListener(onDidFinishLoadingMapListener); + mapChangeEventManager.onDidFinishLoadingMap(); + verify(onDidFinishLoadingMapListener).onDidFinishLoadingMap(); + + mapChangeEventManager.addOnDidFinishLoadingMapListener(onDidFinishLoadingMapListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onDidFinishLoadingMapListener).onDidFinishLoadingMap(); + try { + mapChangeEventManager.onDidFinishLoadingMap(); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onDidFinishLoadingMapListener).onDidFinishLoadingMap(); + try { + mapChangeEventManager.onDidFinishLoadingMap(); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnDidFailLoadingMapListener() { + mapChangeEventManager.addOnDidFailLoadingMapListener(onDidFailLoadingMapListener); + mapChangeEventManager.onDidFailLoadingMap(TEST_STRING); + verify(onDidFailLoadingMapListener).onDidFailLoadingMap(TEST_STRING); + mapChangeEventManager.removeOnDidFailLoadingMapListener(onDidFailLoadingMapListener); + mapChangeEventManager.onDidFailLoadingMap(TEST_STRING); + verify(onDidFailLoadingMapListener).onDidFailLoadingMap(TEST_STRING); + + mapChangeEventManager.addOnDidFailLoadingMapListener(onDidFailLoadingMapListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onDidFailLoadingMapListener).onDidFailLoadingMap(TEST_STRING); + try { + mapChangeEventManager.onDidFailLoadingMap(TEST_STRING); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onDidFailLoadingMapListener).onDidFailLoadingMap(TEST_STRING); + try { + mapChangeEventManager.onDidFailLoadingMap(TEST_STRING); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnWillStartRenderingFrameListener() { + mapChangeEventManager.addOnWillStartRenderingFrameListener(onWillStartRenderingFrameListener); + mapChangeEventManager.onWillStartRenderingFrame(); + verify(onWillStartRenderingFrameListener).onWillStartRenderingFrame(); + mapChangeEventManager.removeOnWillStartRenderingFrameListener(onWillStartRenderingFrameListener); + mapChangeEventManager.onWillStartRenderingFrame(); + verify(onWillStartRenderingFrameListener).onWillStartRenderingFrame(); + + mapChangeEventManager.addOnWillStartRenderingFrameListener(onWillStartRenderingFrameListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onWillStartRenderingFrameListener).onWillStartRenderingFrame(); + try { + mapChangeEventManager.onWillStartRenderingFrame(); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onWillStartRenderingFrameListener).onWillStartRenderingFrame(); + try { + mapChangeEventManager.onWillStartRenderingFrame(); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnDidFinishRenderingFrameListener() { + mapChangeEventManager.addOnDidFinishRenderingFrameListener(onDidFinishRenderingFrameListener); + mapChangeEventManager.onDidFinishRenderingFrame(true); + verify(onDidFinishRenderingFrameListener).onDidFinishRenderingFrame(true); + mapChangeEventManager.removeOnDidFinishRenderingFrameListener(onDidFinishRenderingFrameListener); + mapChangeEventManager.onDidFinishRenderingFrame(true); + verify(onDidFinishRenderingFrameListener).onDidFinishRenderingFrame(true); + + mapChangeEventManager.addOnDidFinishRenderingFrameListener(onDidFinishRenderingFrameListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onDidFinishRenderingFrameListener).onDidFinishRenderingFrame(true); + try { + mapChangeEventManager.onDidFinishRenderingFrame(true); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onDidFinishRenderingFrameListener).onDidFinishRenderingFrame(true); + try { + mapChangeEventManager.onDidFinishRenderingFrame(true); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnDidFinishRenderingFrameFullyRenderedListener() { + mapChangeEventManager.addOnDidFinishRenderingFrameListener(onDidFinishRenderingFrameListener); + mapChangeEventManager.onDidFinishRenderingFrame(false); + verify(onDidFinishRenderingFrameListener).onDidFinishRenderingFrame(false); + mapChangeEventManager.removeOnDidFinishRenderingFrameListener(onDidFinishRenderingFrameListener); + mapChangeEventManager.onDidFinishRenderingFrame(false); + verify(onDidFinishRenderingFrameListener).onDidFinishRenderingFrame(false); + + mapChangeEventManager.addOnDidFinishRenderingFrameListener(onDidFinishRenderingFrameListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onDidFinishRenderingFrameListener).onDidFinishRenderingFrame(false); + try { + mapChangeEventManager.onDidFinishRenderingFrame(false); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onDidFinishRenderingFrameListener).onDidFinishRenderingFrame(false); + try { + mapChangeEventManager.onDidFinishRenderingFrame(false); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnWillStartRenderingMapListener() { + mapChangeEventManager.addOnWillStartRenderingMapListener(onWillStartRenderingMapListener); + mapChangeEventManager.onWillStartRenderingMap(); + verify(onWillStartRenderingMapListener).onWillStartRenderingMap(); + mapChangeEventManager.removeOnWillStartRenderingMapListener(onWillStartRenderingMapListener); + mapChangeEventManager.onWillStartRenderingMap(); + verify(onWillStartRenderingMapListener).onWillStartRenderingMap(); + + mapChangeEventManager.addOnWillStartRenderingMapListener(onWillStartRenderingMapListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onWillStartRenderingMapListener).onWillStartRenderingMap(); + try { + mapChangeEventManager.onWillStartRenderingMap(); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onWillStartRenderingMapListener).onWillStartRenderingMap(); + try { + mapChangeEventManager.onWillStartRenderingMap(); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnDidFinishRenderingMapListener() { + mapChangeEventManager.addOnDidFinishRenderingMapListener(onDidFinishRenderingMapListener); + mapChangeEventManager.onDidFinishRenderingMap(true); + verify(onDidFinishRenderingMapListener).onDidFinishRenderingMap(true); + mapChangeEventManager.removeOnDidFinishRenderingMapListener(onDidFinishRenderingMapListener); + mapChangeEventManager.onDidFinishRenderingMap(true); + verify(onDidFinishRenderingMapListener).onDidFinishRenderingMap(true); + + mapChangeEventManager.addOnDidFinishRenderingMapListener(onDidFinishRenderingMapListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onDidFinishRenderingMapListener).onDidFinishRenderingMap(true); + try { + mapChangeEventManager.onDidFinishRenderingMap(true); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onDidFinishRenderingMapListener).onDidFinishRenderingMap(true); + try { + mapChangeEventManager.onDidFinishRenderingMap(true); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnDidFinishRenderingMapFullyRenderedListener() { + mapChangeEventManager.addOnDidFinishRenderingMapListener(onDidFinishRenderingMapListener); + mapChangeEventManager.onDidFinishRenderingMap(false); + verify(onDidFinishRenderingMapListener).onDidFinishRenderingMap(false); + mapChangeEventManager.removeOnDidFinishRenderingMapListener(onDidFinishRenderingMapListener); + mapChangeEventManager.onDidFinishRenderingMap(false); + verify(onDidFinishRenderingMapListener).onDidFinishRenderingMap(false); + + mapChangeEventManager.addOnDidFinishRenderingMapListener(onDidFinishRenderingMapListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onDidFinishRenderingMapListener).onDidFinishRenderingMap(false); + try { + mapChangeEventManager.onDidFinishRenderingMap(false); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onDidFinishRenderingMapListener).onDidFinishRenderingMap(false); + try { + mapChangeEventManager.onDidFinishRenderingMap(false); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnDidBecomeIdleListener() { + mapChangeEventManager.addOnDidBecomeIdleListener(onDidBecomeIdleListener); + mapChangeEventManager.onDidBecomeIdle(); + verify(onDidBecomeIdleListener).onDidBecomeIdle(); + mapChangeEventManager.removeOnDidBecomeIdleListener(onDidBecomeIdleListener); + mapChangeEventManager.onDidBecomeIdle(); + verify(onDidBecomeIdleListener).onDidBecomeIdle(); + + mapChangeEventManager.addOnDidBecomeIdleListener(onDidBecomeIdleListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onDidBecomeIdleListener).onDidBecomeIdle(); + try { + mapChangeEventManager.onDidBecomeIdle(); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onDidBecomeIdleListener).onDidBecomeIdle(); + try { + mapChangeEventManager.onDidBecomeIdle(); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnDidFinishLoadingStyleListener() { + mapChangeEventManager.addOnDidFinishLoadingStyleListener(onDidFinishLoadingStyleListener); + mapChangeEventManager.onDidFinishLoadingStyle(); + verify(onDidFinishLoadingStyleListener).onDidFinishLoadingStyle(); + mapChangeEventManager.removeOnDidFinishLoadingStyleListener(onDidFinishLoadingStyleListener); + mapChangeEventManager.onDidFinishLoadingStyle(); + verify(onDidFinishLoadingStyleListener).onDidFinishLoadingStyle(); + + mapChangeEventManager.addOnDidFinishLoadingStyleListener(onDidFinishLoadingStyleListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onDidFinishLoadingStyleListener).onDidFinishLoadingStyle(); + try { + mapChangeEventManager.onDidFinishLoadingStyle(); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onDidFinishLoadingStyleListener).onDidFinishLoadingStyle(); + try { + mapChangeEventManager.onDidFinishLoadingStyle(); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + + @Test + public void testOnSourceChangedListener() { + mapChangeEventManager.addOnSourceChangedListener(onSourceChangedListener); + mapChangeEventManager.onSourceChanged(TEST_STRING); + verify(onSourceChangedListener).onSourceChangedListener(TEST_STRING); + mapChangeEventManager.removeOnSourceChangedListener(onSourceChangedListener); + mapChangeEventManager.onSourceChanged(TEST_STRING); + verify(onSourceChangedListener).onSourceChangedListener(TEST_STRING); + + mapChangeEventManager.addOnSourceChangedListener(onSourceChangedListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onSourceChangedListener).onSourceChangedListener(TEST_STRING); + try { + mapChangeEventManager.onSourceChanged(TEST_STRING); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onSourceChangedListener).onSourceChangedListener(TEST_STRING); + try { + mapChangeEventManager.onSourceChanged(TEST_STRING); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapTouchListenersTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapTouchListenersTest.java new file mode 100644 index 000000000..cb097c283 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapTouchListenersTest.java @@ -0,0 +1,157 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.PointF; + +import android.support.annotation.Nullable; +import com.mapbox.android.gestures.MoveGestureDetector; +import com.mapbox.android.gestures.RotateGestureDetector; +import com.mapbox.android.gestures.ShoveGestureDetector; +import com.mapbox.android.gestures.StandardScaleGestureDetector; +import com.mapbox.mapboxsdk.geometry.LatLng; + +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class MapTouchListenersTest { + + @Nullable + private MapGestureDetector mapGestureDetector; + private LatLng latLng; + private PointF pointF; + + @Before + public void setUp() throws Exception { + latLng = new LatLng(); + pointF = new PointF(); + + Projection projection = mock(Projection.class); + when(projection.fromScreenLocation(pointF)).thenReturn(latLng); + mapGestureDetector = new MapGestureDetector(null, + null, projection, null, null, null); + } + + @Test + public void onMapClickListenerTest() throws Exception { + MapboxMap.OnMapClickListener listener = mock(MapboxMap.OnMapClickListener.class); + mapGestureDetector.addOnMapClickListener(listener); + mapGestureDetector.notifyOnMapClickListeners(pointF); + verify(listener, times(1)).onMapClick(latLng); + + mapGestureDetector.removeOnMapClickListener(listener); + mapGestureDetector.notifyOnMapClickListeners(pointF); + verify(listener, times(1)).onMapClick(latLng); + } + + @Test + public void onMapLongClickListenerTest() throws Exception { + MapboxMap.OnMapLongClickListener listener = mock(MapboxMap.OnMapLongClickListener.class); + mapGestureDetector.addOnMapLongClickListener(listener); + mapGestureDetector.notifyOnMapLongClickListeners(pointF); + verify(listener, times(1)).onMapLongClick(latLng); + + mapGestureDetector.removeOnMapLongClickListener(listener); + mapGestureDetector.notifyOnMapLongClickListeners(pointF); + verify(listener, times(1)).onMapLongClick(latLng); + } + + @Test + public void onFlingListenerTest() throws Exception { + MapboxMap.OnFlingListener listener = mock(MapboxMap.OnFlingListener.class); + mapGestureDetector.addOnFlingListener(listener); + mapGestureDetector.notifyOnFlingListeners(); + verify(listener, times(1)).onFling(); + + mapGestureDetector.removeOnFlingListener(listener); + mapGestureDetector.notifyOnFlingListeners(); + verify(listener, times(1)).onFling(); + } + + @Test + public void onMoveListenerTest() throws Exception { + MapboxMap.OnMoveListener listener = mock(MapboxMap.OnMoveListener.class); + MoveGestureDetector detector = mock(MoveGestureDetector.class); + mapGestureDetector.addOnMoveListener(listener); + mapGestureDetector.notifyOnMoveBeginListeners(detector); + mapGestureDetector.notifyOnMoveListeners(detector); + mapGestureDetector.notifyOnMoveEndListeners(detector); + verify(listener, times(1)).onMoveBegin(detector); + verify(listener, times(1)).onMove(detector); + verify(listener, times(1)).onMoveEnd(detector); + + mapGestureDetector.removeOnMoveListener(listener); + mapGestureDetector.notifyOnMoveBeginListeners(detector); + mapGestureDetector.notifyOnMoveListeners(detector); + mapGestureDetector.notifyOnMoveEndListeners(detector); + verify(listener, times(1)).onMoveBegin(detector); + verify(listener, times(1)).onMove(detector); + verify(listener, times(1)).onMoveEnd(detector); + } + + @Test + public void onRotateListenerTest() throws Exception { + MapboxMap.OnRotateListener listener = mock(MapboxMap.OnRotateListener.class); + RotateGestureDetector detector = mock(RotateGestureDetector.class); + mapGestureDetector.addOnRotateListener(listener); + mapGestureDetector.notifyOnRotateBeginListeners(detector); + mapGestureDetector.notifyOnRotateListeners(detector); + mapGestureDetector.notifyOnRotateEndListeners(detector); + verify(listener, times(1)).onRotateBegin(detector); + verify(listener, times(1)).onRotate(detector); + verify(listener, times(1)).onRotateEnd(detector); + + mapGestureDetector.removeOnRotateListener(listener); + mapGestureDetector.notifyOnRotateBeginListeners(detector); + mapGestureDetector.notifyOnRotateListeners(detector); + mapGestureDetector.notifyOnRotateEndListeners(detector); + verify(listener, times(1)).onRotateBegin(detector); + verify(listener, times(1)).onRotate(detector); + verify(listener, times(1)).onRotateEnd(detector); + } + + @Test + public void onScaleListenerTest() throws Exception { + MapboxMap.OnScaleListener listener = mock(MapboxMap.OnScaleListener.class); + StandardScaleGestureDetector detector = mock(StandardScaleGestureDetector.class); + mapGestureDetector.addOnScaleListener(listener); + mapGestureDetector.notifyOnScaleBeginListeners(detector); + mapGestureDetector.notifyOnScaleListeners(detector); + mapGestureDetector.notifyOnScaleEndListeners(detector); + verify(listener, times(1)).onScaleBegin(detector); + verify(listener, times(1)).onScale(detector); + verify(listener, times(1)).onScaleEnd(detector); + + mapGestureDetector.removeOnScaleListener(listener); + mapGestureDetector.notifyOnScaleBeginListeners(detector); + mapGestureDetector.notifyOnScaleListeners(detector); + mapGestureDetector.notifyOnScaleEndListeners(detector); + verify(listener, times(1)).onScaleBegin(detector); + verify(listener, times(1)).onScale(detector); + verify(listener, times(1)).onScaleEnd(detector); + } + + @Test + public void onShoveListenerTest() throws Exception { + MapboxMap.OnShoveListener listener = mock(MapboxMap.OnShoveListener.class); + ShoveGestureDetector detector = mock(ShoveGestureDetector.class); + mapGestureDetector.addShoveListener(listener); + mapGestureDetector.notifyOnShoveBeginListeners(detector); + mapGestureDetector.notifyOnShoveListeners(detector); + mapGestureDetector.notifyOnShoveEndListeners(detector); + verify(listener, times(1)).onShoveBegin(detector); + verify(listener, times(1)).onShove(detector); + verify(listener, times(1)).onShoveEnd(detector); + + mapGestureDetector.removeShoveListener(listener); + mapGestureDetector.notifyOnShoveBeginListeners(detector); + mapGestureDetector.notifyOnShoveListeners(detector); + mapGestureDetector.notifyOnShoveEndListeners(detector); + verify(listener, times(1)).onShoveBegin(detector); + verify(listener, times(1)).onShove(detector); + verify(listener, times(1)).onShoveEnd(detector); + } +} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapboxMapOptionsAttrsTest.kt b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapboxMapOptionsAttrsTest.kt new file mode 100644 index 000000000..ee8024257 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapboxMapOptionsAttrsTest.kt @@ -0,0 +1,101 @@ +package com.mapbox.mapboxsdk.maps + +import android.content.Context +import android.content.res.Resources +import android.content.res.TypedArray +import com.mapbox.mapboxsdk.R +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.verify +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MapboxMapOptionsAttrsTest { + + @RelaxedMockK + private lateinit var options: MapboxMapOptions + + @RelaxedMockK + private lateinit var typedArray: TypedArray + + @RelaxedMockK + private lateinit var context: Context + + @RelaxedMockK + private lateinit var resources: Resources + + @Before + fun setUp() { + MockKAnnotations.init(this) + every { + context.resources + }.returns(resources) + } + + @Test + fun enabledLocalIdeographFontFamily() { + mockEnableLocalIdeograph(enabled = true) + + val options = MapboxMapOptions.createFromAttributes(options, context, typedArray) + + verify(exactly = 1) { + options.localIdeographFontFamily(any()) + } + } + + @Test + fun localIdeographFontFamily() { + mockEnableLocalIdeograph(enabled = true) + + val font = "foo" + mockLocalIdeographString(font) + + val options = MapboxMapOptions.createFromAttributes(options, context, typedArray) + + verify(exactly = 1) { + options.localIdeographFontFamily(font) + } + } + + @Test + fun localIdeographFontFamilies() { + mockEnableLocalIdeograph(enabled = true) + + val fonts = arrayOf("foo", "bar") + mockLocalIdeographStringArray(fonts) + + val options = MapboxMapOptions.createFromAttributes(options, context, typedArray) + + verify(exactly = 1) { + options.localIdeographFontFamily(*fonts) + } + } + + private fun mockEnableLocalIdeograph(enabled: Boolean) { + every { + typedArray.getBoolean(R.styleable.mapbox_MapView_mapbox_localIdeographEnabled, true) + }.returns(enabled) + } + + private fun mockLocalIdeographString(font: String) { + every { + typedArray.getString(R.styleable.mapbox_MapView_mapbox_localIdeographFontFamily) + }.returns(font) + } + + private fun mockLocalIdeographStringArray(fonts: Array) { + val resId = 9000 + + every { + typedArray.getResourceId(R.styleable.mapbox_MapView_mapbox_localIdeographFontFamilies, 0) + }.returns(resId) + + every { + resources.getStringArray(resId) + }.returns(fonts) + } +} \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapboxMapOptionsTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapboxMapOptionsTest.java new file mode 100644 index 000000000..6978afcf1 --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapboxMapOptionsTest.java @@ -0,0 +1,186 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.Color; +import android.view.Gravity; + +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.constants.MapboxConstants; +import com.mapbox.mapboxsdk.geometry.LatLng; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.Arrays; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNull; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +public class MapboxMapOptionsTest { + + private static final double DELTA = 1e-15; + + @Test + public void testSanity() { + assertNotNull("should not be null", new MapboxMapOptions()); + } + + @Test + public void testDebugEnabled() { + assertFalse(new MapboxMapOptions().getDebugActive()); + assertTrue(new MapboxMapOptions().debugActive(true).getDebugActive()); + assertFalse(new MapboxMapOptions().debugActive(false).getDebugActive()); + } + + @Test + public void testCompassEnabled() { + assertTrue(new MapboxMapOptions().compassEnabled(true).getCompassEnabled()); + assertFalse(new MapboxMapOptions().compassEnabled(false).getCompassEnabled()); + } + + @Test + public void testCompassGravity() { + assertEquals(Gravity.TOP | Gravity.END, new MapboxMapOptions().getCompassGravity()); + assertEquals(Gravity.BOTTOM, new MapboxMapOptions().compassGravity(Gravity.BOTTOM).getCompassGravity()); + assertNotEquals(Gravity.START, new MapboxMapOptions().compassGravity(Gravity.BOTTOM).getCompassGravity()); + } + + @Test + public void testCompassMargins() { + assertTrue(Arrays.equals(new int[] {0, 1, 2, 3}, new MapboxMapOptions().compassMargins( + new int[] {0, 1, 2, 3}).getCompassMargins())); + assertFalse(Arrays.equals(new int[] {0, 1, 2, 3}, new MapboxMapOptions().compassMargins( + new int[] {0, 0, 0, 0}).getCompassMargins())); + } + + @Test + public void testLogoEnabled() { + assertTrue(new MapboxMapOptions().logoEnabled(true).getLogoEnabled()); + assertFalse(new MapboxMapOptions().logoEnabled(false).getLogoEnabled()); + } + + @Test + public void testLogoGravity() { + assertEquals(Gravity.BOTTOM | Gravity.START, new MapboxMapOptions().getLogoGravity()); + assertEquals(Gravity.BOTTOM, new MapboxMapOptions().logoGravity(Gravity.BOTTOM).getLogoGravity()); + assertNotEquals(Gravity.START, new MapboxMapOptions().logoGravity(Gravity.BOTTOM).getLogoGravity()); + } + + @Test + public void testLogoMargins() { + assertTrue(Arrays.equals(new int[] {0, 1, 2, 3}, new MapboxMapOptions().logoMargins( + new int[] {0, 1, 2, 3}).getLogoMargins())); + assertFalse(Arrays.equals(new int[] {0, 1, 2, 3}, new MapboxMapOptions().logoMargins( + new int[] {0, 0, 0, 0}).getLogoMargins())); + } + + @Test + public void testAttributionTintColor() { + assertEquals(-1, new MapboxMapOptions().getAttributionTintColor()); + assertEquals(Color.RED, new MapboxMapOptions().attributionTintColor(Color.RED).getAttributionTintColor()); + } + + @Test + public void testAttributionEnabled() { + assertTrue(new MapboxMapOptions().attributionEnabled(true).getAttributionEnabled()); + assertFalse(new MapboxMapOptions().attributionEnabled(false).getAttributionEnabled()); + } + + @Test + public void testAttributionGravity() { + assertEquals(Gravity.BOTTOM | Gravity.START, new MapboxMapOptions().getAttributionGravity()); + assertEquals(Gravity.BOTTOM, new MapboxMapOptions().attributionGravity(Gravity.BOTTOM).getAttributionGravity()); + assertNotEquals(Gravity.START, new MapboxMapOptions().attributionGravity(Gravity.BOTTOM).getAttributionGravity()); + } + + @Test + public void testAttributionMargins() { + assertTrue(Arrays.equals(new int[] {0, 1, 2, 3}, new MapboxMapOptions().attributionMargins( + new int[] {0, 1, 2, 3}).getAttributionMargins())); + assertFalse(Arrays.equals(new int[] {0, 1, 2, 3}, new MapboxMapOptions().attributionMargins( + new int[] {0, 0, 0, 0}).getAttributionMargins())); + } + + @Test + public void testMinZoom() { + assertEquals(MapboxConstants.MINIMUM_ZOOM, new MapboxMapOptions().getMinZoomPreference(), DELTA); + assertEquals(5.0f, new MapboxMapOptions().minZoomPreference(5.0f).getMinZoomPreference(), DELTA); + assertNotEquals(2.0f, new MapboxMapOptions().minZoomPreference(5.0f).getMinZoomPreference(), DELTA); + } + + @Test + public void testMaxZoom() { + assertEquals(MapboxConstants.MAXIMUM_ZOOM, new MapboxMapOptions().getMaxZoomPreference(), DELTA); + assertEquals(5.0f, new MapboxMapOptions().maxZoomPreference(5.0f).getMaxZoomPreference(), DELTA); + assertNotEquals(2.0f, new MapboxMapOptions().maxZoomPreference(5.0f).getMaxZoomPreference(), DELTA); + } + + @Test + public void testTiltGesturesEnabled() { + assertTrue(new MapboxMapOptions().getTiltGesturesEnabled()); + assertTrue(new MapboxMapOptions().tiltGesturesEnabled(true).getTiltGesturesEnabled()); + assertFalse(new MapboxMapOptions().tiltGesturesEnabled(false).getTiltGesturesEnabled()); + } + + @Test + public void testScrollGesturesEnabled() { + assertTrue(new MapboxMapOptions().getScrollGesturesEnabled()); + assertTrue(new MapboxMapOptions().scrollGesturesEnabled(true).getScrollGesturesEnabled()); + assertFalse(new MapboxMapOptions().scrollGesturesEnabled(false).getScrollGesturesEnabled()); + } + + @Test + public void testZoomGesturesEnabled() { + assertTrue(new MapboxMapOptions().getZoomGesturesEnabled()); + assertTrue(new MapboxMapOptions().zoomGesturesEnabled(true).getZoomGesturesEnabled()); + assertFalse(new MapboxMapOptions().zoomGesturesEnabled(false).getZoomGesturesEnabled()); + } + + @Test + public void testRotateGesturesEnabled() { + assertTrue(new MapboxMapOptions().getRotateGesturesEnabled()); + assertTrue(new MapboxMapOptions().rotateGesturesEnabled(true).getRotateGesturesEnabled()); + assertFalse(new MapboxMapOptions().rotateGesturesEnabled(false).getRotateGesturesEnabled()); + } + + @Test + public void testCamera() { + CameraPosition position = new CameraPosition.Builder().build(); + assertEquals(new CameraPosition.Builder(position).build(), new MapboxMapOptions().camera(position).getCamera()); + assertNotEquals(new CameraPosition.Builder().target(new LatLng(1, 1)), new MapboxMapOptions().camera(position)); + assertNull(new MapboxMapOptions().getCamera()); + } + + @Test + public void testPrefetchesTiles() { + // Default value + assertTrue(new MapboxMapOptions().getPrefetchesTiles()); + + // Check mutations + assertTrue(new MapboxMapOptions().setPrefetchesTiles(true).getPrefetchesTiles()); + assertFalse(new MapboxMapOptions().setPrefetchesTiles(false).getPrefetchesTiles()); + } + + @Test + public void testCrossSourceCollisions() { + // Default value + assertTrue(new MapboxMapOptions().getCrossSourceCollisions()); + + // check mutations + assertTrue(new MapboxMapOptions().crossSourceCollisions(true).getCrossSourceCollisions()); + assertFalse(new MapboxMapOptions().crossSourceCollisions(false).getCrossSourceCollisions()); + } + + @Test + public void testLocalIdeographFontFamily_enabledByDefault() { + MapboxMapOptions options = MapboxMapOptions.createFromAttributes(RuntimeEnvironment.application, null); + assertEquals(MapboxConstants.DEFAULT_FONT, options.getLocalIdeographFontFamily()); + } +} + diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapboxMapTest.kt b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapboxMapTest.kt new file mode 100644 index 000000000..deb5f603c --- /dev/null +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapboxMapTest.kt @@ -0,0 +1,202 @@ +package com.mapbox.mapboxsdk.maps + +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory +import com.mapbox.mapboxsdk.constants.MapboxConstants +import com.mapbox.mapboxsdk.geometry.LatLng +import com.mapbox.mapboxsdk.geometry.LatLngBounds +import com.mapbox.mapboxsdk.style.layers.TransitionOptions +import io.mockk.* +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MapboxMapTest { + + private lateinit var mapboxMap: MapboxMap + + private lateinit var nativeMapView: NativeMap + + private lateinit var transform: Transform + + private lateinit var cameraChangeDispatcher: CameraChangeDispatcher + + private lateinit var developerAnimationListener: MapboxMap.OnDeveloperAnimationListener + + @Before + fun setup() { + cameraChangeDispatcher = spyk() + developerAnimationListener = mockk(relaxed = true) + nativeMapView = mockk(relaxed = true) + transform = mockk(relaxed = true) + mapboxMap = MapboxMap(nativeMapView, transform, mockk(relaxed = true), null, null, cameraChangeDispatcher, listOf(developerAnimationListener)) + every { nativeMapView.isDestroyed } returns false + every { nativeMapView.nativePtr } returns 5 + mapboxMap.injectLocationComponent(spyk()) + mapboxMap.setStyle(Style.MAPBOX_STREETS) + mapboxMap.onFinishLoadingStyle() + } + + @Test + fun testTransitionOptions() { + val expected = TransitionOptions(100, 200) + mapboxMap.style?.transition = expected + verify { nativeMapView.transitionOptions = expected } + } + + @Test + fun testMoveCamera() { + val callback = mockk() + val target = LatLng(1.0, 2.0) + val expected = CameraPosition.Builder().target(target).build() + val update = CameraUpdateFactory.newCameraPosition(expected) + mapboxMap.moveCamera(update, callback) + verify { transform.moveCamera(mapboxMap, update, callback) } + verify { developerAnimationListener.onDeveloperAnimationStarted() } + } + + @Test + fun testEaseCamera() { + val callback = mockk() + val target = LatLng(1.0, 2.0) + val expected = CameraPosition.Builder().target(target).build() + val update = CameraUpdateFactory.newCameraPosition(expected) + mapboxMap.easeCamera(update, callback) + verify { transform.easeCamera(mapboxMap, update, MapboxConstants.ANIMATION_DURATION, true, callback) } + verify { developerAnimationListener.onDeveloperAnimationStarted() } + } + + @Test + fun testAnimateCamera() { + val callback = mockk() + val target = LatLng(1.0, 2.0) + val expected = CameraPosition.Builder().target(target).build() + val update = CameraUpdateFactory.newCameraPosition(expected) + mapboxMap.animateCamera(update, callback) + verify { transform.animateCamera(mapboxMap, update, MapboxConstants.ANIMATION_DURATION, callback) } + verify { developerAnimationListener.onDeveloperAnimationStarted() } + } + + @Test + fun testScrollBy() { + mapboxMap.scrollBy(100f, 200f) + verify { nativeMapView.moveBy(100.0, 200.0, 0) } + verify { developerAnimationListener.onDeveloperAnimationStarted() } + } + + @Test + fun testResetNorth() { + mapboxMap.resetNorth() + verify { transform.resetNorth() } + verify { developerAnimationListener.onDeveloperAnimationStarted() } + } + + @Test + fun testFocalBearing() { + mapboxMap.setFocalBearing(35.0, 100f, 200f, 1000) + verify { transform.setBearing(35.0, 100f, 200f, 1000) } + verify { developerAnimationListener.onDeveloperAnimationStarted() } + } + + @Test + fun testMinZoom() { + mapboxMap.setMinZoomPreference(10.0) + verify { transform.minZoom = 10.0 } + } + + @Test + fun testMaxZoom() { + mapboxMap.setMaxZoomPreference(10.0) + verify { transform.maxZoom = 10.0 } + } + + @Test + fun testFpsListener() { + val fpsChangedListener = mockk() + mapboxMap.onFpsChangedListener = fpsChangedListener + assertEquals("Listener should match", fpsChangedListener, mapboxMap.onFpsChangedListener) + } + + @Test + fun testTilePrefetch() { + mapboxMap.prefetchesTiles = true + verify { nativeMapView.prefetchTiles = true } + } + + @Test + fun testCameraForLatLngBounds() { + val bounds = LatLngBounds.Builder().include(LatLng()).include(LatLng(1.0, 1.0)).build() + mapboxMap.setLatLngBoundsForCameraTarget(bounds) + verify { nativeMapView.setLatLngBounds(bounds) } + } + + @Test(expected = IllegalArgumentException::class) + fun testAnimateCameraChecksDurationPositive() { + mapboxMap.animateCamera(CameraUpdateFactory.newLatLng(LatLng(30.0, 30.0)), 0, null) + } + + @Test(expected = IllegalArgumentException::class) + fun testEaseCameraChecksDurationPositive() { + mapboxMap.easeCamera(CameraUpdateFactory.newLatLng(LatLng(30.0, 30.0)), 0, null) + } + + @Test + fun testGetNativeMapPtr() { + assertEquals(5, mapboxMap.nativeMapPtr) + } + + @Test + fun testNativeMapIsNotCalledOnStateSave() { + clearMocks(nativeMapView) + mapboxMap.onSaveInstanceState(mockk(relaxed = true)) + verify { nativeMapView wasNot Called } + } + + @Test + fun testCameraChangeDispatcherCleared() { + mapboxMap.onDestroy() + verify { cameraChangeDispatcher.onDestroy() } + } + + @Test + fun testStyleClearedOnDestroy() { + val style = mockk + + + + + + diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/values/actions.xml b/MapboxGLAndroidSDKTestApp/src/main/res/values/actions.xml new file mode 100644 index 000000000..d36c741c5 --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/res/values/actions.xml @@ -0,0 +1,94 @@ + + + Concurrent Open InfoWindows + Deselect Markers On Tap + Reset + Toggle icon overlap + Filter layer + Change location + Accelerate/Decelerate interpolator + Bounce interpolator + Anticipate/Overshoot interpolator + PathInterpolator interpolator + Toggle Symbol Layer Visibility + Move + Ease + Animate + Open dialog + Download region + List regions + Remove polylines + Change visibility + Change alpha + Change points + Change color + Change holes + Change width + "Click the map to calculate the distance" + Move the map by x/y pixels + Open navigation drawer + Close navigation drawer + X: %1$d + Y: %1$d + Animate to new position + Chelsea + Stamford Bridge + Arsenal + Emirates Stadium + Zoom: %.2f + ViewCache size %d + Latitude + -180 + Longitude + Zoom + 18 + Bearing + 0 + Tilt + No Results + Change intensity + Change Anchor + Amount of markers + Update layer (invalidate) + Red + Green + Blue + Add an exponential zoom function + Add an interval zoom function + Add a categorical source function + Add an exponential source function + Add an identity source function + Add an interval source function + Add a composite, categorical function + Add a composite, exponential function + Add a composite, interval function + Bangalore + List all layers in the style + List all sources in the style + Color the water + Set background opacity + Set road symbol placement to Point + Set layer visibility to false + Add a parks layer + Add a dynamic GeoJSON source + Remove buildings layer + Add a terrain layer + Add a satellite layer + Change the water color on zoom + Custom tiles + Apply filtered fill + Apply filtered line + Apply numeric fill filter + Bring water to front + Toggle text size + Toggle text field contents + Toggle text font + Zoom in + Zoom out + Zoom by 2 + Zoom to point + Zoom to 4 + Restrict almost worldview + Restrict across IDL + Reset bounds + \ No newline at end of file diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/values/arrays.xml b/MapboxGLAndroidSDKTestApp/src/main/res/values/arrays.xml new file mode 100644 index 000000000..ad9f88a46 --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/res/values/arrays.xml @@ -0,0 +1,15 @@ + + + + 10 + 100 + 500 + 1000 + 10000 + + + foo + monospace + bar + + \ No newline at end of file diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/values/categories.xml b/MapboxGLAndroidSDKTestApp/src/main/res/values/categories.xml new file mode 100644 index 000000000..94c805f35 --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/res/values/categories.xml @@ -0,0 +1,21 @@ + + + category + _Basic + Annotation + Camera + Custom Layer + Fragment + Image Generator + Info Window + Map Layout + Offline + Styling + Features + Storage + Texture View + Location + _Integration + Telemetry + Turf + \ No newline at end of file diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/values/colors.xml b/MapboxGLAndroidSDKTestApp/src/main/res/values/colors.xml new file mode 100644 index 000000000..88524d44e --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/res/values/colors.xml @@ -0,0 +1,13 @@ + + + #52A1D8 + #3887BE + #E55E5E + #F9F9F9 + #56B881 + + #D50000 + #2962FF + #1B5E20 + + diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml b/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml new file mode 100644 index 000000000..7f018a43f --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml @@ -0,0 +1,89 @@ + + + Overlay a custom native layer on the map + Learn how to create a custom InfoWindow + CameraPosition capabilities + Showcase MapFragment + Showcase SupportMapFragment + Showcase using a Map Fragment with a fragment backstack + Activity with multiple maps on screen + Add marker to map on long press + Different types of zoom methods + Configure a max and min zoomlevel + Learn how to handle the InfoWindow + Add Markers In Bulk to a Map + Showcase the different animation types + Center the camera around a bounds + Update position and icon + Map Padding example + Debug Mode + Offline Map example + Download an offline region. + Update metadata example + Delete region example + Change resources cache path example + Animate the position change of a symbol layer + Add a polyline to a map + Add a polygon to a map + Scroll with pixels in x,y direction + Example to make a snapshot of the map + 2 maps in a view hierarchy + Learn how to create a dynamic custom InfoWindow + Use SupportMapFragments in a ViewPager + Adopt the map style on the fly + Show a gradient line layer from a geojson source + Use functions to change the map appearance + Manipulate symbols at runtime + Use a custom sprite in a Symbol Layer + Use GeoJson sources and dynamic layers to cluster information + Use realtime GeoJSON data streams to move a symbol on your map + Shows how to print a map + Query rendered feature properties on click + Count all rendered features in box + Count all rendered symbols in box + Highlight buildings in box + Query source for features + Shows a simple map + Logs map change events to Logcat + Changes visibility of map and view parent + Change Symbol icon when zoom levels changes + Use a local file as the style + Display a map inside a dialog fragment + Show bus stops and route in Singapore + Transform urls on the fly + Control the cache management with FileSource API + Limit viewport to Iceland + Shows how to add 3D extruded shapes + Shows how to show 3D extruded buildings + Shows how to animate georeferenced images + Show 2 MapView on screen with a bottom sheet + Show a static bitmap taken with the MapSnapshotter + Show how to reuse a MapSnapshotter instance + Show how to add a marker to a Snapshot + Show how to load a local style with a Snapshot + Use Android SDK Animators to animate camera position changes + Use Android SDK Views as symbols + Use TextureView to render the map + Resize a map rendered on a TextureView + Animate a map rendered on a TextureView + Enable a transparent surface on TextureView + Blend an overlay on a map + Example Custom Geometry Source + Load a map without providing a style URI or JSON + Suzhou using Droid Sans for Chinese glyphs + Example raster-dem source and hillshade layer + Use HeatmapLayer to visualise earthquakes + Manipulate gestures detector\'s settings + Merge external offline database + Click to add a marker, long-click to drag + Change map\'s style while location is displayed + Showcases location render and tracking modes + Uses LocationComponent in a Fragment + Force location updates and don\'t rely on the engine + Use LocationComponentActivationOptions to set options + Show multiple TextureView MapViews as recyclerView items + Show multiple GLSurfaceView MapViews as recyclerView items + Show a MapView inside a viewpager inside a recyclerView + Show the use PerformanceEvent for performance measurements + Use TurfTransformation#circle() to show a Cirlce expressed in physical units + diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/values/dimens.xml b/MapboxGLAndroidSDKTestApp/src/main/res/values/dimens.xml new file mode 100644 index 000000000..9d3418343 --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/res/values/dimens.xml @@ -0,0 +1,10 @@ + + + 16dp + 10dp + 96dp + 256dp + 32dp + 0dp + 240dp + diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/values/ids.xml b/MapboxGLAndroidSDKTestApp/src/main/res/values/ids.xml new file mode 100644 index 000000000..6701032d9 --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/res/values/ids.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/values/strings.xml b/MapboxGLAndroidSDKTestApp/src/main/res/values/strings.xml new file mode 100644 index 000000000..45048f8b0 --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + Mapbox Android SDK TestApp + fps30 + fps60 + diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/values/styles.xml b/MapboxGLAndroidSDKTestApp/src/main/res/values/styles.xml new file mode 100644 index 000000000..8d7da603f --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/res/values/styles.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml b/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml new file mode 100644 index 000000000..90baaed28 --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml @@ -0,0 +1,89 @@ + + + Support Map Fragment + Map Fragment + Backstack Map Fragment + Multiple Maps on Screen + Add Markers In Bulk + Animated SymbolLayer + Dynamic Marker + Polyline + Polygon + Press Map For Marker + Zoom function with SymbolLayer + Standard InfoWindow + Custom InfoWindow + Custom Dynamic InfoWindow + Animation Types + Zoom Methods + LatLngBounds Method + CameraPosition Method + Scroll By Method + Double Map Activity + Snapshot Activity + Custom Layer + Map Padding + Debug Mode + Offline Map + Update metadata Map + Delete region + Change resources cache path + Min/Max Zoom + ViewPager + Runtime Style + Gradient line layer + Data Driven Style + Circle layer + Local Style file + GeoJson Clustering + Add live realtime data + Print a map + Query feature properties + Count features in box + Count symbols in box + Highlight features in box + Query source features + Symbols + Add Custom Sprite + Simple Map + Map Change Events + Visibility Map + Dialog with map + Url transform + Cache management + Restrict camera to a bounds + Fill extrusions + Building layer + Animated Image Source + Bottom sheet + Map Snapshotter + Map Snapshotter Reuse + Map Snapshot with marker + Map Snapshot with local style + Animator animation + SymbolGenerator + TextureView debug + TextureView resize + TextureView animation + TextureView transparent background + Map overlay + Grid Source + No Style URI/JSON + Local CJK glyph generation + Hillshade + Heatmap layer + Gestures detector + Offline DB merge + Draggable marker + Simple Location Activity + Location Modes Activity + Location Fragment + Manual Location updates + Build Location Activation + RecyclerView TextureView + RecyclerView GLSurfaceView + Nested ViewPager + Performance Measurement + Physical Unit Circle + Download region + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..b7b2cb225 --- /dev/null +++ b/README.md @@ -0,0 +1,118 @@ +# [Mapbox Maps SDK for Android](https://www.mapbox.com/android-sdk/) + +[![Circle CI build status](https://circleci.com/gh/mapbox/mapbox-gl-native.svg?style=shield)](https://circleci.com/gh/mapbox/workflows/mapbox-gl-native/tree/master) + +[![](https://www.mapbox.com/android-docs/assets/overview-map-sdk-322-9abe118316efb5910b6101e222a2e57c.png)](https://docs.mapbox.com/android/maps/overview/) + +The Mapbox Maps SDK for Android is a library based on [Mapbox GL Native](../../README.md) for embedding interactive map views with scalable, customizable vector maps onto Android devices. + +## Getting Started + +This particular README is for people who are interested in working on and improving the Maps SDK for Android. If you're looking for more general information and instructions on the Maps SDK: + +Visit [https://docs.mapbox.com/android/maps/overview](https://docs.mapbox.com/android/maps/overview/) to see current documentation on the Maps SDK for Android. + +Visit [https://docs.mapbox.com/android/api/map-sdk](https://docs.mapbox.com/android/api/map-sdk/) to view the Maps SDK's current API reference Javadoc files. + +See [the Mapbox website's Android install flow](https://www.mapbox.com/install/android/) to install and use the Mapbox Maps SDK for Android in an application. + +### Setup environment + +These instructions are for developers interested in making code-level contributions to the SDK itself. If you instead want to use the SDK in your app, see above. + +#### Getting the source + +Clone the git repository + +```bash +git clone git@github.com:mapbox/mapbox-gl-native.git && cd mapbox-gl-native +``` + +#### Installing dependencies + +These dependencies are required for all operating systems and all platform targets. + +- Latest stable [Android Studio](https://developer.android.com/studio/index.html) +- Update the Mapbox Maps SDK for Android with the latest + - Android SDK Build-Tools + - Android Platform-Tools + - Android SDK Tools + - CMake + - NDK + - LLDB +- Modern C++ compiler that supports `-std=c++14`\* + - clang++ 3.5 or later or + - g++-4.9 or later +- [Node.js](https://nodejs.org/) + - make sure [npm](https://www.npmjs.com) is installed as well +- [ccache](https://ccache.samba.org/) (optional) + +**Note**: We partially support C++14 because GCC 4.9 does not fully implement the +final draft of the C++14 standard. More information in [DEVELOPING.md](../../DEVELOPING.md). + +**Note**: On macOS you can install clang with installing the [Apple command line developer tools](https://developer.apple.com/download/). + +### Opening the project + +#### macOS + +Execute the following command in this repository's root folder to generate the required build files and open the project with Android Studio: + +``` +make aproj +``` + +#### linux + +run `make android-configuration` in the root folder of the project and open the Android Studio project in `/platform/android`. + +If you are using Arch Linux, install [ncurses5-compat-libs](https://aur.archlinux.org/packages/ncurses5-compat-libs). + +### Project configuration + +#### Setup Checkstyle + +Mapbox uses specific IDE settings related to code and check style. +See [checkstyle guide](https://github.com/mapbox/mapbox-gl-native/wiki/Setting-up-Mapbox-checkstyle) for configuration details. + +#### Resolving duplicate file entries +With buck build support, Android Studio can complain about duplicate source files. To remove this warning, open `MapboxGLAndroidSDK.iml` find the list of `excludeFolder` entries and add `` line. + +##### Setting Mapbox Access Token + +_The test application (used for development purposes) uses Mapbox vector tiles, which require a Mapbox account and API access token. Obtain a free access token on the [Mapbox account page](https://www.mapbox.com/studio/account/tokens/)._ + +With the first gradle invocation, gradle will take the value of the `MAPBOX_ACCESS_TOKEN` environment variable and save it to `MapboxGLAndroidSDKTestApp/src/main/res/values/developer-config.xml`. If the environment variable wasn't set, you can edit `developer-config.xml` manually and add your access token to the `mapbox_access_token` resource. + +### Running project + +Run the configuration for the `MapboxGLAndroidSDKTestApp` module and select a device or emulator to deploy on. Based on the selected device, the c++ code will be compiled for the related processor architecture. You can see the project compiling in the `View > Tool Windows > Gradle Console`. + +More information about building and distributing this project in [DISTRIBUTE.md](https://github.com/mapbox/mapbox-gl-native/blob/master/platform/android/DISTRIBUTE.md). + +### Additional resources + +#### Using the SDK snapshot + +Instead of using the latest stable release of the Maps SDK for Android, you can use a "snapshot" or the beta version if there is one available. Our snapshots are built every time a Github pull request adds code to this repository's `master` branch. If you'd like to use a snapshot build, your Android project's gradle file should have -SNAPSHOT appended to the SDK version number. For example, the `7.3.0-SNAPSHOT` would look like: + +```java +// Mapbox SDK dependency +implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:7.3.0-SNAPSHOT' +``` + +You also need to have the section below in your build.gradle root folder to be able to resolve the SNAPSHOT dependencies: +``` +allprojects { + repositories { + jcenter() + maven { url 'https://oss.jfrog.org/artifactory/oss-snapshot-local/' } + } +} +``` + + +#### Symbolicating native crashes + +When hitting native crashes you can use ndk-stack to symbolicate crashes. +More information in [this](https://github.com/mapbox/mapbox-gl-native/wiki/Getting-line-numbers-from-an-Android-crash-with-ndk-stack) guide. diff --git a/TESTS.md b/TESTS.md new file mode 100644 index 000000000..f0bdeb2dc --- /dev/null +++ b/TESTS.md @@ -0,0 +1,117 @@ +# Mapbox GL Test App + +## Testing + +### Running Espresso tests on a device + +This test project comes with all the required Android Testing Support Library dependencies +in the Gradle file. Tests are under the `app/src/androidTest` folder. + +Note that before running your tests, you might want to turn off animations on your test device. +It's a known issue that leaving system animations turned on in a test device +(window animation scale, transition animation scale, animator duration scale) +might cause unexpected results, or may lead tests to fail. + +To create a new run configuration: +* Click on Run -> Edit Configurations... +* Click on the plus sign and then on "Android Tests" +* Give a name to the configuration, e.g. `TestAppTests` +* Choose the `MapboxGLAndroidSDKTestApp` module +* Choose `android.support.test.runner.AndroidJUnitRunner` as the instrumentation runner +* Click OK to save the new configuration + +You can now run this configuration from the main toolbar dropdown menu. + +### Running Espresso tests on Firebase within AS + +Tests are under the `app/src/androidTest` folder. + +1) _Sign In_ with your Google account. + +![Firebase Tests AS 1][1] + +2) Or _Add Account_ if you want to sign in with a different account. + +![Firebase Tests AS 2][2] + +3) Select the test configuration you want to run and click _Play_ button. + +4) Click on _Cloud Testing_ tab. You need to create a project in Firebase and define a _Matrix configuration_ previously (see +[Firebase Test Lab for Android guide](https://firebase.google.com/docs/test-lab/overview)). Select your template (e.g. _Nexus 5 (3)_). + +5) Select your _Cloud project_ (e.g. _android-gl-native (android-gl-native)_). + +![Firebase Tests AS 3-5][3] + +6) Click on _OK_ button to run the tests. + +![Firebase Tests AS 6][4] + +7) When tests finish the results will appear within AS. + +![Firebase Tests AS 7][5] + +### Running Espresso tests on AWS Device Farm + +On a terminal, within `mapbox-gl-native/android/java`, +run the tests (`cC` stands for `connectedCheck`): + +``` +$ ./gradlew -Pmapbox.abis=all cC -p MapboxGLAndroidSDKTestApp +``` + +Then: +* Go to your AWS Console and choose Device Farm. +* Create a new project, e.g. `MapboxGLAndroidSDKTestApp` +* On step 1, upload the APK in `mapbox-gl-native/android/java/MapboxGLAndroidSDKTestApp/build/outputs/apk/MapboxGLAndroidSDKTestApp-debug-unaligned.apk` +* On step 2, choose Instrumentation, test filter is `com.mapbox.mapboxgl.testapp.MainActivityTest` and upload the APK in `mapbox-gl-native/android/java/MapboxGLAndroidSDKTestApp/build/outputs/apk/MapboxGLAndroidSDKTestApp-debug-androidTest-unaligned.apk` +* On step 3, choose a device pool. E.g. Top Devices +* On step 4, customize your device state (if needed) +* Finally, confirm the configuration and run the tests. + +On Step 2, you can also separate by commas different classes: `com.mapbox.mapboxgl.testapp.MainActivityTest,com.mapbox.mapboxgl.testapp.MainActivityScreenTest` + +If you have no tests for your app, or want to test some random user behavior, +you can just choose "Built-in: Fuzz" in step 2. + +### Running pure JUnit tests + +These tests run on a local JVM on your development machine and they are extremely fast to run +(as compared to Espresso). These tests are located under `src/test/java`. To run them you switch +to the Unit Tests build variant, then right click the corresponding test class or method +and select "Run ...". + +You can also have a run configuration: +* Click on Run -> Edit Configurations... +* Click on "Junit Tests" +* Give a name to the configuration, e.g. `JUnit tests` +* As "Test Kind", choose "All in directory" +* As folder, choose the following folder: `mapbox-gl-native/android/java/MapboxGLAndroidSDKTestApp/src/test/java` +* Click OK to save the new configuration + +You can also run the tests from the command line with: + +``` +$ ./gradlew -Pmapbox.abis=none test -p MapboxGLAndroidSDKTestApp +``` + +### Running the UI/Application Exerciser Monkey + +Similar to the "Built-in: Fuzz" test mentioned above, Android provides +[Monkey](http://developer.android.com/tools/help/monkey.html), +"a program that runs on your emulator or device and generates pseudo-random streams of user events +such as clicks, touches, or gestures, as well as a number of system-level events." + +To exercise Monkey on the test app, install the package on the device (e.g. via Android Studio) +and then: + +``` +$ adb shell monkey -p com.mapbox.mapboxgl.testapp -v 500 +``` + + +[1]: ./art/tests/FirebaseTestsAS_1.png +[2]: ./art/tests/FirebaseTestsAS_2.png +[3]: ./art/tests/FirebaseTestsAS_3-5.png +[4]: ./art/tests/FirebaseTestsAS_6.png +[5]: ./art/tests/FirebaseTestsAS_7.png diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..c74912fb6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,34 @@ +buildscript { + apply from: "${rootDir}/gradle/dependencies.gradle" + + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + classpath dependenciesList.licensesPlugin + classpath dependenciesList.kotlinPlugin + classpath dependenciesList.bintrayPlugin + classpath dependenciesList.artifactoryPlugin + classpath dependenciesList.androidPublishPlugin + classpath dependenciesList.jacocoPlugin + } +} + +allprojects { + repositories { + mavenCentral() + google() + jcenter() + // For publishing Maps SDK files to Bintray + maven { url 'https://mapbox.bintray.com/mapbox' } + // Snapshot repository + // maven { url 'https://oss.jfrog.org/artifactory/oss-snapshot-local/' } + } + apply from: "${rootDir}/gradle/ktlint.gradle" +} + +subprojects { + apply from: "${rootDir}/gradle/dependencies.gradle" +} \ No newline at end of file diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 000000000..c73ada156 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/android-nitpick.gradle b/gradle/android-nitpick.gradle new file mode 100644 index 000000000..6db351967 --- /dev/null +++ b/gradle/android-nitpick.gradle @@ -0,0 +1,66 @@ +apply from: "${rootDir}/gradle/dependencies.gradle" + +def MAPBOX_JAVA_DIR = 'mapbox-java' +def MAPBOX_JAVA_TAG_PREFIX = 'v' + +def MAPBOX_TELEMETRY_DIR = 'mapbox-events-android' +def MAPBOX_TELEMETRY_TAG_PREFIX = 'telem-' +def MAPBOX_CORE_TAG_PREFIX = '-core-' + +def MAPBOX_GESTURES_DIR = 'mapbox-gestures-android' +def MAPBOX_GESTURES_TAG_PREFIX = 'v' + +task androidNitpick { + doLast { + println "Running android nitpick script" + + println "Verify vendor submodule pins" + verifyVendorSubmodulePin(MAPBOX_JAVA_DIR, MAPBOX_JAVA_TAG_PREFIX, versions.mapboxServices) + verifyVendorSubmodulePin(MAPBOX_TELEMETRY_DIR, MAPBOX_TELEMETRY_TAG_PREFIX, + versions.mapboxTelemetry + MAPBOX_CORE_TAG_PREFIX + versions.mapboxCore) + verifyVendorSubmodulePin(MAPBOX_GESTURES_DIR, MAPBOX_GESTURES_TAG_PREFIX, versions.mapboxGestures) + + verifyLicenseGeneration() + } +} + +private def verifyVendorSubmodulePin(def dir, def prefix, def version) { + println "Verify vendor submodule pin: ${dir} (${prefix + version})" + exec { + workingDir = "${rootDir}/vendor/${dir}" + commandLine "git", "fetch", "-t" + } + + def output = new ByteArrayOutputStream() + exec { + workingDir = "${rootDir}/vendor/${dir}" + commandLine "git", "rev-list", "-n", "1", "tags/${prefix + version}" + standardOutput = output + } + def expectedCommit = output.toString().trim() + output.reset() + + exec { + workingDir = "${rootDir}/vendor/${dir}" + commandLine "git", "rev-parse", "HEAD" + standardOutput = output + } + def actualCommit = output.toString().trim() + + if (actualCommit != expectedCommit) { + throw new IllegalStateException("${dir} vendor repository is not checked out on the consumed binary's tag.\n" + + "Expected commit: " + expectedCommit + "(${prefix + version} tag).\n" + + "Actual commit: " + actualCommit + ".\n" + + "If you've updated the version in the dependencies.gradle file, make sure to bump the submodule pin in the platform/android/vendor/ directory to match the release tag.\n" + + "If you've bumped the pin, make sure to verify the version tag prefix in the android-nitpick.gradle file.") + } + output.close() +} + +private def verifyLicenseGeneration() { + println "Verify license generation with git diff..." + exec { + workingDir = "${rootDir}" + commandLine "python", "scripts/validate-license.py" + } +} \ No newline at end of file diff --git a/gradle/artifact-settings.gradle b/gradle/artifact-settings.gradle new file mode 100644 index 000000000..acf29eeb8 --- /dev/null +++ b/gradle/artifact-settings.gradle @@ -0,0 +1,21 @@ +ext { + mapboxArtifactGroupId = 'com.mapbox.mapboxsdk' + mapboxArtifactId = 'mapbox-android-sdk' + mapboxArtifactTitle = 'Mapbox Maps SDK for Android' + mapboxArtifactDescription = 'Mapbox Maps SDK for Android' + mapboxDeveloperName = 'Mapbox' + mapboxDeveloperId = 'mapbox' + mapboxArtifactUrl = 'https://github.com/mapbox/mapbox-gl-native' + mapboxArtifactVcsUrl = 'https://github.com/mapbox/mapbox-gl-native.git' + mapboxArtifactScmUrl = 'scm:git@github.com:mapbox/mapbox-gl-native.git' + mapboxArtifactLicenseName = 'BSD' + mapboxArtifactLicenseUrl = 'https://opensource.org/licenses/BSD-2-Clause' + versionName = project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : System.getenv('VERSION_NAME') + + mapboxBintrayUserOrg = 'mapbox' + mapboxBintrayRepoName = 'mapbox' + mapboxBintrayPackageName = 'com.mapbox.mapboxsdk:mapbox-android-sdk' + mapboxBintrayUser = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER') + mapboxBintrayApiKey = project.hasProperty('BINTRAY_API_KEY') ? project.property('BINTRAY_API_KEY') : System.getenv('BINTRAY_API_KEY') + mapboxGpgPassphrase = project.hasProperty('GPG_PASSPHRASE') ? project.property('GPG_PASSPHRASE') : System.getenv('GPG_PASSPHRASE') +} diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle new file mode 100644 index 000000000..c1c489472 --- /dev/null +++ b/gradle/dependencies.gradle @@ -0,0 +1,90 @@ +ext { + + androidVersions = [ + minSdkVersion : 14, + targetSdkVersion : 28, + compileSdkVersion: 28, + ] + + versions = [ + mapboxServices : '4.9.0-alpha.1', + mapboxTelemetry : '4.5.1', + mapboxCore : '1.3.0', + mapboxGestures : '0.5.1', + mapboxAccounts : '0.2.0', + supportLib : '28.0.0', + constraintLayout: '1.1.2', + uiAutomator : '2.1.3', + espresso : '3.0.2', + testRunner : '1.0.2', + leakCanary : '1.6.3', + junit : '4.12', + mockito : '3.0.0', + mockk : '1.9.3', + robolectric : '4.3', + timber : '4.7.1', + okhttp : '3.12.3', + kotlin : '1.3.41', + licenses : '0.8.5', + bintray : '1.8.4', + artifactory : '4.9.3', + androidPublish : '3.6.2', + lint : '26.4.2', + gms : '16.0.0', + soLoader : '0.6.1', + jacoco : '0.8.4', + appcenter : '1.4', + ktlint : '0.34.0', + commonsIO : '2.6' + ] + + dependenciesList = [ + mapboxJavaGeoJSON : "com.mapbox.mapboxsdk:mapbox-sdk-geojson:${versions.mapboxServices}", + mapboxAndroidTelemetry : "com.mapbox.mapboxsdk:mapbox-android-telemetry:${versions.mapboxTelemetry}", + mapboxAndroidGestures : "com.mapbox.mapboxsdk:mapbox-android-gestures:${versions.mapboxGestures}", + mapboxAndroidAccounts : "com.mapbox.mapboxsdk:mapbox-android-accounts:${versions.mapboxAccounts}", + mapboxJavaTurf : "com.mapbox.mapboxsdk:mapbox-sdk-turf:${versions.mapboxServices}", + + junit : "junit:junit:${versions.junit}", + mockito : "org.mockito:mockito-core:${versions.mockito}", + mockk : "io.mockk:mockk:${versions.mockk}", + robolectric : "org.robolectric:robolectric:${versions.robolectric}", + + testRunner : "com.android.support.test:runner:${versions.testRunner}", + testRules : "com.android.support.test:rules:${versions.testRunner}", + testEspressoCore : "com.android.support.test.espresso:espresso-core:${versions.espresso}", + testEspressoIntents : "com.android.support.test.espresso:espresso-intents:${versions.espresso}", + testEspressoContrib : "com.android.support.test.espresso:espresso-contrib:${versions.espresso}", + testUiAutomator : "com.android.support.test.uiautomator:uiautomator-v18:${versions.uiAutomator}", + appCenter : "com.microsoft.appcenter:espresso-test-extension:${versions.appcenter}", + commonsIO : "commons-io:commons-io:${versions.commonsIO}", + supportAnnotations : "com.android.support:support-annotations:${versions.supportLib}", + supportAppcompatV7 : "com.android.support:appcompat-v7:${versions.supportLib}", + supportFragmentV4 : "com.android.support:support-fragment:${versions.supportLib}", + supportUtilV4 : "com.android.support:support-core-utils:${versions.supportLib}", + supportDesign : "com.android.support:design:${versions.supportLib}", + supportRecyclerView : "com.android.support:recyclerview-v7:${versions.supportLib}", + supportConstraintLayout: "com.android.support.constraint:constraint-layout:${versions.constraintLayout}", + + gmsLocation : "com.google.android.gms:play-services-location:${versions.gms}", + timber : "com.jakewharton.timber:timber:${versions.timber}", + okhttp3 : "com.squareup.okhttp3:okhttp:${versions.okhttp}", + leakCanaryDebug : "com.squareup.leakcanary:leakcanary-android:${versions.leakCanary}", + leakCanaryRelease : "com.squareup.leakcanary:leakcanary-android-no-op:${versions.leakCanary}", + soLoader : "com.facebook.soloader:soloader:${versions.soLoader}", + + kotlinLib : "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}", + kotlinPlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}", + licensesPlugin : "com.jaredsburrows:gradle-license-plugin:${versions.licenses}", + bintrayPlugin : "com.jfrog.bintray.gradle:gradle-bintray-plugin:${versions.bintray}", + artifactoryPlugin : "org.jfrog.buildinfo:build-info-extractor-gradle:${versions.artifactory}", + androidPublishPlugin : "digital.wup:android-maven-publish:${versions.androidPublish}", + jacocoPlugin : "org.jacoco:org.jacoco.core:${versions.jacoco}", + + lint : "com.android.tools.lint:lint:${versions.lint}", + lintApi : "com.android.tools.lint:lint-api:${versions.lint}", + lintChecks : "com.android.tools.lint:lint-checks:${versions.lint}", + lintTests : "com.android.tools.lint:lint-tests:${versions.lint}", + ktlint : "com.pinterest:ktlint:${versions.ktlint}" + ] +} diff --git a/gradle/gradle-bintray.gradle b/gradle/gradle-bintray.gradle new file mode 100644 index 000000000..e8ace7359 --- /dev/null +++ b/gradle/gradle-bintray.gradle @@ -0,0 +1,117 @@ +apply plugin: 'digital.wup.android-maven-publish' +apply plugin: 'maven-publish' +apply plugin: 'com.jfrog.bintray' +apply plugin: 'com.jfrog.artifactory' +apply from: file('../gradle/artifact-settings.gradle') + +version = project.ext.versionName +group = project.ext.mapboxArtifactGroupId + +publishing { + publications { + MapboxMapsSdkPublication(MavenPublication) { + from components.android + groupId this.group + artifactId project.ext.mapboxArtifactId + version this.version + + afterEvaluate { + artifact(androidSourcesJar) + artifact(androidJavadocsJar) + } + + pom.withXml { + final mainNode = asNode() + mainNode.appendNode('name', project.ext.mapboxArtifactTitle) + mainNode.appendNode('description', project.ext.mapboxArtifactTitle) + mainNode.appendNode('url', project.ext.mapboxArtifactUrl) + + final licenseNode = mainNode.appendNode('licenses').appendNode('license') + licenseNode.appendNode('name', project.ext.mapboxArtifactLicenseName) + licenseNode.appendNode('url', project.ext.mapboxArtifactLicenseUrl) + licenseNode.appendNode('distribution', "repo") + + final developerNode = mainNode.appendNode('developers').appendNode('developer') + developerNode.appendNode('id', project.ext.mapboxDeveloperId) + developerNode.appendNode('name', project.ext.mapboxDeveloperName) + + final scmNode = mainNode.appendNode("scm") + scmNode.appendNode("connection", project.ext.mapboxArtifactScmUrl) + scmNode.appendNode("developerConnection", project.ext.mapboxArtifactScmUrl) + scmNode.appendNode("url", project.ext.mapboxArtifactUrl) + } + } + } +} + +bintray { + user = mapboxBintrayUser + key = mapboxBintrayApiKey + publications('MapboxMapsSdkPublication') + pkg { + repo = project.ext.mapboxBintrayRepoName + name = project.ext.mapboxBintrayPackageName + userOrg = project.ext.mapboxBintrayUserOrg + licenses = [project.ext.mapboxArtifactLicenseName] + vcsUrl = project.ext.mapboxArtifactVcsUrl + publish = false + version { + name = project.ext.versionName + desc = project.ext.mapboxArtifactDescription + released = new Date() + gpg { + sign = true + passphrase = mapboxGpgPassphrase + } + mavenCentralSync { + sync = false + } + } + } +} + +artifactory { + contextUrl = 'http://oss.jfrog.org' + publish { + repository { + repoKey = 'oss-snapshot-local' + username = mapboxBintrayUser + password = mapboxBintrayApiKey + } + defaults { + publications('MapboxMapsSdkPublication') + } + } +} + +task androidJavadocs(type: Javadoc) { + source = android.sourceSets.main.java.sourceFiles + classpath = files(android.bootClasspath) + failOnError = false +} + +task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { + classifier = 'javadoc' + from androidJavadocs.destinationDir +} + +task androidSourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.sourceFiles +} + +tasks.withType(Javadoc) { + options.addStringOption('encoding', 'UTF-8') + options.addStringOption('docencoding', 'UTF-8') + options.addStringOption('charset', 'UTF-8') +} + +afterEvaluate { project -> + android.libraryVariants.all { variant -> + tasks.androidJavadocs.doFirst { + classpath += files(variant.javaCompile.classpath.files) + } + } +} + + diff --git a/gradle/gradle-checkstyle.gradle b/gradle/gradle-checkstyle.gradle new file mode 100644 index 000000000..8eb4baa85 --- /dev/null +++ b/gradle/gradle-checkstyle.gradle @@ -0,0 +1,26 @@ +apply plugin: 'checkstyle' + +checkstyle { + toolVersion = "7.1.1" // 7.3 + configFile = "../checkstyle.xml" as File +} + +task checkstyle(type: Checkstyle) { + description 'Checks if the code adheres to coding standards' + group 'verification' + configFile file("../checkstyle.xml") + source 'src' + include '**/*.java' + exclude '**/gen/**' + exclude '**/style/*LayerTest.java' + exclude '**/style/LightTest.java' + exclude '**/style/layers/Property.java' + exclude '**/style/layers/PropertyFactory.java' + exclude '**/style/layers/*Layer.java' + exclude '**/style/light/Light.java' + exclude '**/log/LoggerDefinition.java' + exclude '**/log/Logger.java' + exclude '**/Expression.java' + classpath = files() + ignoreFailures = false +} diff --git a/gradle/gradle-config.gradle b/gradle/gradle-config.gradle new file mode 100644 index 000000000..834680663 --- /dev/null +++ b/gradle/gradle-config.gradle @@ -0,0 +1,23 @@ +// +// Configuration file for gradle build execution. +// + +task accessToken { + def tokenFile = new File("${projectDir}/src/main/res/values/developer-config.xml") + if (!tokenFile.exists()) { + String mapboxAccessToken = "$System.env.MAPBOX_ACCESS_TOKEN" + if (mapboxAccessToken == "null") { + System.out.println("You should set the MAPBOX_ACCESS_TOKEN environment variable.") + mapboxAccessToken = "YOUR_MAPBOX_ACCESS_TOKEN_GOES_HERE" + } + String tokenFileContents = "\n" + + "\n" + + " " + mapboxAccessToken + "\n" + + "" + tokenFile.write(tokenFileContents) + } +} + +gradle.projectsEvaluated { + preBuild.dependsOn('accessToken') +} diff --git a/gradle/gradle-dependencies-graph.gradle b/gradle/gradle-dependencies-graph.gradle new file mode 100644 index 000000000..5cbc7b974 --- /dev/null +++ b/gradle/gradle-dependencies-graph.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath "com.vanniktech:gradle-dependency-graph-generator-plugin:0.3.0" + } +} + +import com.vanniktech.dependency.graph.generator.DependencyGraphGeneratorPlugin +import com.vanniktech.dependency.graph.generator.DependencyGraphGeneratorExtension.Generator +import com.vanniktech.dependency.graph.generator.dot.GraphFormattingOptions +import com.vanniktech.dependency.graph.generator.dot.Color +import com.vanniktech.dependency.graph.generator.dot.Shape +import com.vanniktech.dependency.graph.generator.dot.Style + +plugins.apply(DependencyGraphGeneratorPlugin) + +def mapboxGenerator = new Generator( + "mapboxLibraries", // Suffix for our Gradle task. + "", // Root suffix that we don't want in this case. + { dependency -> dependency.getModuleGroup().startsWith("com.mapbox.mapboxsdk") }, // Only want Mapbox libs. + { dependency -> true }, // Include transitive dependencies. +) + +dependencyGraphGenerator { + generators = [mapboxGenerator] +} diff --git a/gradle/gradle-javadoc.gradle b/gradle/gradle-javadoc.gradle new file mode 100644 index 000000000..ba530a9b6 --- /dev/null +++ b/gradle/gradle-javadoc.gradle @@ -0,0 +1,18 @@ +android.libraryVariants.all { variant -> + def name = variant.name + task "javadoc$name"(type: Javadoc) { + description = "Generates javadoc for build $name" + destinationDir = new File(destinationDir, variant.baseName) + source = variant.sourceSets.collect { it.java.sourceFiles }.inject { m, i -> m + i } + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) + options.windowTitle("Mapbox Maps SDK for Android $VERSION_NAME Reference") + options.docTitle("Mapbox Maps SDK for Android $VERSION_NAME") + options.header("Mapbox Maps SDK for Android $VERSION_NAME Reference") + options.bottom("© 2015–2019 Mapbox. All rights reserved.") + options.links("http://docs.oracle.com/javase/7/docs/api/") + options.linksOffline "http://d.android.com/reference","${android.sdkDirectory}/docs/reference" + options.overview("src/main/java/overview.html") + options.group("Mapbox Android SDK", "com.mapbox.*") + exclude '**/R.java', '**/BuildConfig.java', '**/**.java.ejs', '**/**.html' + } +} \ No newline at end of file diff --git a/gradle/gradle-lint.gradle b/gradle/gradle-lint.gradle new file mode 100644 index 000000000..cbebeaa74 --- /dev/null +++ b/gradle/gradle-lint.gradle @@ -0,0 +1,30 @@ +task ciLint(type: Copy) { + if (isLocalBuild()) { + from "${projectDir}/lint/lint-baseline-local.xml" + into "${projectDir}" + rename { String fileName -> + fileName.replace("lint-baseline-local.xml", "lint-baseline.xml") + } + } else { + from "${projectDir}/lint/lint-baseline-ci.xml" + into "${projectDir}" + rename { String fileName -> + fileName.replace("lint-baseline-ci.xml", "lint-baseline.xml") + } + } +} + +def isLocalBuild() { + if (System.getenv('IS_LOCAL_DEVELOPMENT') != null) { + return System.getenv('IS_LOCAL_DEVELOPMENT').toBoolean() + } + return true +} + +lint.dependsOn ciLint + +tasks.whenTaskAdded { task -> + if (task.name == 'lintVitalRelease') { + task.dependsOn ciLint + } +} \ No newline at end of file diff --git a/gradle/gradle-make.gradle b/gradle/gradle-make.gradle new file mode 100644 index 000000000..65d971cc9 --- /dev/null +++ b/gradle/gradle-make.gradle @@ -0,0 +1,18 @@ +// +// Contains GL-native make commands +// + +task makeClean(type: Exec) { + workingDir '../../' + commandLine 'make', 'clean' +} + +task makeAndroid(type: Exec) { + workingDir '../../' + commandLine 'make', 'android' +} + +task makeAndroidAll(type: Exec) { + workingDir '../../' + commandLine 'make', 'apackage' +} diff --git a/gradle/gradle-update-vendor-modules.gradle b/gradle/gradle-update-vendor-modules.gradle new file mode 100644 index 000000000..e215450ec --- /dev/null +++ b/gradle/gradle-update-vendor-modules.gradle @@ -0,0 +1,8 @@ +task updateVendorSubmodules { + doLast { + exec { + workingDir = "${rootDir}" + commandLine "git", "submodule", "update", "--init", "vendor" + } + } +} \ No newline at end of file diff --git a/gradle/jacoco-report.gradle b/gradle/jacoco-report.gradle new file mode 100644 index 000000000..c4e0bb913 --- /dev/null +++ b/gradle/jacoco-report.gradle @@ -0,0 +1,33 @@ +apply plugin: 'jacoco' +apply from: "${rootDir}/gradle/dependencies.gradle" + +jacoco { + toolVersion = versions.jacoco +} + +task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { + group = "Reporting" + description = "Combine code coverage to unified report." + + reports { + xml.enabled = true + html.enabled = true + } + + def fileExcludes = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'] + def debugTree = fileTree(dir: "${project.buildDir}/intermediates/javac/debug/compileDebugJavaWithJavac/classes", excludes: fileExcludes) + def mainSrc = "${project.projectDir}/src/main/java" + println(mainSrc) + def ecSrc = fileTree(dir: "$project.buildDir", include: "**/*.ec") + def execSrc = fileTree(dir: "$project.buildDir", include: "**/*.exec") + + doFirst { + def files = files([ecSrc, execSrc]).files + println "Creating Jacoco Report for ${files.size()} coverage files" + files.each { file -> println file } + } + + sourceDirectories.from = files([mainSrc]) + classDirectories.from = files([debugTree]) + executionData.from = files([ecSrc, execSrc]) +} \ No newline at end of file diff --git a/gradle/ktlint.gradle b/gradle/ktlint.gradle new file mode 100644 index 000000000..31743ccd4 --- /dev/null +++ b/gradle/ktlint.gradle @@ -0,0 +1,27 @@ +apply from: "${rootDir}/gradle/dependencies.gradle" + +repositories { + jcenter() +} + +configurations { + ktlint +} + +dependencies { + ktlint dependenciesList.ktlint +} + +task ktlint(type: JavaExec, group: "verification") { + description = "Check Kotlin code style." + classpath = configurations.ktlint + main = "com.pinterest.ktlint.Main" + args "src/**/*.kt" +} + +task ktlintFormat(type: JavaExec, group: "formatting") { + description = "Fix Kotlin code style deviations." + classpath = configurations.ktlint + main = "com.pinterest.ktlint.Main" + args "-F", "src/**/*.kt" +} \ No newline at end of file diff --git a/gradle/native-build.gradle b/gradle/native-build.gradle new file mode 100644 index 000000000..9e8f58015 --- /dev/null +++ b/gradle/native-build.gradle @@ -0,0 +1,63 @@ +ext.nativeBuild = { nativeTargets -> + android { + defaultPublishConfig project.hasProperty("mapbox.buildtype") ? project.getProperty("mapbox.buildtype") : "debug" + + // We sometimes want to invoke Gradle without building a native dependency, e.g. when we just want + // to invoke the Java tests. When we explicitly specify an ABI of 'none', no native dependencies are + // added. When another ABI is specified explicitly, we're just going to build that ABI. In all other + // cases, all ABIs are built. + // + // When invoking from the command line or to override the device default, set `-Pmapbox.abis=...` to + // only build the desired architectures. + // + // When building from Android Studio, gradle.properties sets `android.buildOnlyTargetAbi=true` so that + // only the architecture for the device you're running on gets built. + def abi = 'all' + if (!project.hasProperty('android.injected.invoked.from.ide') || project.hasProperty("mapbox.abis")) { + // Errors when the user invokes Gradle from the command line and didn't set mapbox.abis + abi = project.getProperty("mapbox.abis") + } + + if (abi != 'none') { + externalNativeBuild { + cmake { + path "../vendor/mapbox-gl-native/CMakeLists.txt" + version "3.10.2" + } + } + } + + // Allow determining the C++ STL we're using when building Mapbox GL. + def stl = 'c++_static' + if (project.hasProperty("mapbox.stl")) { + stl = project.getProperty("mapbox.stl") + } + + defaultConfig { + if (abi != 'none') { + externalNativeBuild { + cmake { + arguments "-DANDROID_TOOLCHAIN=clang" + arguments "-DANDROID_STL=" + stl + arguments "-DANDROID_CPP_FEATURES=exceptions" + + // Enable ccache if the user has installed it. + if (file("/usr/bin/ccache").exists()) { + arguments "-DANDROID_CCACHE=/usr/bin/ccache" + } + + for (target in nativeTargets) { + targets target + } + + if (abi != 'all') { + abiFilters abi.split(' ') + } else { + abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..51288f9c2f05faf8d42e1a751a387ca7923882c3 GIT binary patch literal 54227 zcmaI7W0WS{vNc-lF59+k+qP}nt}ffQZQE5{wp~xzc30g#d_WEY_lIM<*>kEH?T**n)L|d4_rKbms*8e?2n)Ki&=O?<4J<%>V7R{?iBg zUq0qeh9)+q|G%!t|L*GUWa!{v>h%8_vhfuLJO%*-l>FD9;-818|MyT)4^v}T7kei< zV;e(f=M?n_Z{$_P@0tV}PfeKL81;-1K%hd2jPC7;DA>wd?+UcoR*R#W($?M))u>gd zdn}ehUd3m+8_k40@?K-j2J`KZyalp1AAH~OemS09)oMlsj}Ip=JKY&CS%B#&?VkHQ zbp0RoxZiO54G4JffN7@lTM-EP35bJi7)bfM8EqjCYYcnn{VS&2QEn5KJxt6fN4jA5 z35`f+Sduq7yq)PG@7FzVPY-Jv0EQW7A|u)@XZ43(l=9JiPNOEX-?M91MKSb8LB7*ix_1 zP;pX`ce%>4<(cfvymJ}t(%d{Q0z=8R1bQv29j?-2sm$ONs(@PIGdy@9R%Ta=*YdEA z&d3ZtFAJZsYhJ)vf*)knsFyO}Y=592S4#xG?1vm(x|kp{tuD2chGmvsN`Pzy^ zUV@_Yn2NIb#=1!#ZV!rb;}EdSGKZGG!tBKgC=%PM#u0uc?HGxKOZ9pZ7_T|A*1#Qy zf$Rm!>2sH}rRl&DRAmM3N>s80GU*RYMpwv5BW)XS&*^g<%=05?3M^7)<&I1!aW`0s z&|9LT!iow_V9V%qPOf<7C7X$)ABMd05mgn;wfKiF_Pq*?msg;KGz$4;ER)t4nxfpQ z>NJFiAO81*#gMz>5y%b5ZK(1yt)P-Kre5KQC3YH?naRFiq=b zJGw+CtdguK-J8U(x~Pj8^~>uka;Ov~%`0$5NxZck$uYX(d75Q@4yqjY9dl38H@E9W2#wa*RJ8+Fq#c%!`8 zF?p_di`A_*fY_}yp!i7Tkv@$(lKu;SFRYmSf^wOzrJS-o78?qR;M;FFvyw!qlC@@U zjOGrCIjd*tv02Jqu9Kx`kDN_RazI{F##4p9_>71}duTTOg+3*QxxW8&f8T&T>SwHa-%rxr1sm0msfK|vl&OQI zX&*rkG-JX~)q&mH7lf~Zy}Z}7U|~=(6%wDv zXggVP{CTshNqV&AXub;T_43>o*E6$B1-0epUkaVp40X1y$=!96kgUo zQAWHAQ4ln6PPLTKRtm|vB90=hFW>%6&a3Vxd&+4)-RLEH+7&*$N#o@kG-=jl9>#1{ zmA_w6RaTULUkhTUPuuirdzrP-E*H7VLpxQbZ1nM0jU1g9WR`IdH%?agZbu88eAX!D zmmH#^1wp!tnHJ!>H+UD~o#?l=TaYt97?r-!bgjq6OdAYtzBP>2kC7H6i5oHEphkp zXwA+ySm+72eNV~9feSko<*D;cffr6e&cy%;qNZSI2spGwz0q^o0ocX!Xj{Q3F_FFy zb;xtD7*4?WLK3%$cb>s{W9Q&3(PD@F(i%LS6uG@nCDEj-8k$@3;>2k48067l8*}^f zDK1+;{IRWcX9VNm7Upz%V#NEjq}`_K^*p)Vza)4Z!DS(t4nSte6V)4t{4zL9S!Rt( zG{n^ISmKsVo2QDFceIUfS|WN{o_BbkcY09LPkC0>vo5ZtVTJ0qcQ!3~XP=nGv1 zJo-kS`382}lM(yeA}-?&GjM11ZN}hk3NG%6OLm!3viy4Se)z*zljGWqNMTRT z;|}+51hU6pvm>GAzY!k9b#*Qp!4uUd{BHHM=>z7(TUQ&61wgNHQl)lHtm$Tv{V8?< zk}YmbB)h;VK~-h)&*iAh&3CrB-vGbFBvrq;kS955Nc(!x4d&^0{<$rOYwALrDOsQ4 zW9hcssstO*k44cLQ%oj=Xj9q|<70?iI9T0cS6YjjP+BYXQepHl8|&Ti8B#65lL2A6H|t*f&z#g5Xapa^l!(# z{3O494S#6RU}ob3Jn||E zC_hOlR0p5`1g7@8F13LjC$1vO%+&M*gnInCca^I=)t1-j*Sz;lm3mi<95Fr|0BFc+ zu7#3Ed?%B~Ue(h6*BMLO%@xB;6k8MgnjgMWjopLNBA9B0ynXzYzi2B4s?*J`Z6}M9 z`z+Ety}W&Vn)+KW)ql|+_RkE4d_;T&aHIwh$qd6>IR;n7%`5{W&?Roh16J=vX;Sz(L_>M=+Ys zq^-2;)!L)YO}{_rzt7a@)sp)32M`bz91sxMKhIRb(8WT~+1b?A$j0=4K%d&1KK5VG zM<=1BS#3*W%xxfHDuYZ2T)91JD|?Vi-HHU3Og$negw~995uVc6)mWhJ)ggOB&AZU~ z;+wRRur(+D;Qd|Ry1T9`ogYs2CU`ULoyq=?b2F9I_Wd<`@eSet_Z_{@6D^!vJEqVt z7M(~r1dExgu9$o!tEoXH#W-y6TTZhWy|CWY%}8N?!c8UQ{2+>vnqAL)pG@)nixQ8l3#ZDiH- zOAKW<`aqk}2YVOsgkJn^o1&MTTwm?Z7nNVZ!KSmh7M)yntYx|-(mIs}Tx+Ef#=Eeb zJ9eLf!`0Kz#>vad)zZ+)?`guu&dSx$k&oM1Q_$x<)K7U9#kDG4GatB$p>pq5WZX$- z+bQkIXhgxm#Ln+Iq9S7Cy@D8Adlo&0bgaHEnN4(v-8hvgE=x3sj3_QM^-ND`E*D2f z4Iz`_sKU){3q9S;{>cO(!Duz;Y3V>sB$$VB=phfv*F;pxRVz!1i{0*%QQ}ZK7ARHbULmBp8A7 zz93B7-OS_TvFL!)V<3|WKIDk{_$C$Gpw}Sl&YiUR!TvO4y;-YbzS-gk4rR>KFm}GQ z(y0jd8o`kZO}4_uCe0pGLAzRy6L%bc)KC-;ODa9{Yw9EW>F00Sp=p|Fb9e~NQ=P`B zC!*duS%7J&u3)s-SO#I-pRb^G3l)C}q=_KtlE@yr-(=Lj`miy-Az_O=REB;!DvnTN zk=+WzuHH6>mN8TjMp^0|_N20YLb$W={LE4B#2oP$p*e=t9*Pq^A%CUB9JhGQs200( zhV4T*OKUIM`{b^3Xt<+B?OS08(%d=UIwYp@E81^+&57K#eg*w1-RZiVpAJkC=VrCOt1UIT|y#IB$V5l}&G8kO{eaZFLGx3RoJ&N;4O9yunGUqu?ea z_(0(#OeEfb*Vg)R}!1*oms#bJAGG83BAD4+Q+e;y0;RhEa~F zz`Oo_z!J)KI)+4vzOeSt1mYkFe~D1AC&ZuJlB8ZY14)uVQUWpt%(YK?-imDdvIW)V zRfLI>P%DVp;?PA_5xP*fg4*Bu^H^|KM6F7~Kh?ER!vNwOE@2!hj?JMnQTrdAW6x)! zFK1p03IGKY^f7g%?fq*BSY7TX2QP&EbQ6jo4k+)%X3L5vc zBgoN{WOq|@unntspvNN5q`cUiO$=zt{SNkTy)J;pf8r@$(il*!`~7x?@Y+~G>*n=) zOt^zka-hG2j|*o5^BkI2ePjqWjvNycLpt5jBDy$f4bZ&vTS|W08a~$$$HtK(P_@p8 zMUUu?UUT1Fa_^fo7g>yK@y-M_sE#1BGpaSmh3BY-tK<}L=50V1>P|u{*fT7yL9RD% za;^U1y<;Kt&T{dD>E)ZssbT7R>Ro$ff_p}?@(ZTg`{ZFNUj&X*n&bB+ub zq)0d%*cs3S34)l8068Ko@q8d;1PK@jgqshQOc2Q6-z+H zuPims#_D_e+utKms(L7Wt2x0o!w2`NyC1Z>_E%p=yZCiLYmpC*HUjFTD@EG0@>Xl; z;$Xa3UV!idY2D{4WqokC@;~{7k5xzUT5wfI;h|+z4l1~DBMmU}V}?co!o46N9BNe0 zDe;atV+Pq`p$GL2g&;#xdAflKR2&Wfj1gwlTZN@PYYIya%6gjBly&E7;;c>AYIA>a z@#lO2-Roe_1)23y=JfMB-bqXw)bg2jxp{WnzS(AJrTm39v109e?kErRK^uE&eT3(g3KwitamUX%r=36jqIb}JtYaiXHS`+tt!u3-IgyIxh2=i!{PiU ziqcdkUO^Rtvovc=tj8HeMGMkbTq$kRq8G)N^)~Cdvy(;*i!M`wXNVS4Fc2VD<8$zn zvXs3mw`%va+$rukKI0F?fHJ01Tk5D9*{1GmEDpj4z7$KB^@mO0_x7xG8+Lx0IRqyz zm27+_T=9*OevoYpwS>f`eMTl{LyFZYqUGv8_yQoP6ME$)Z;yA3lf*2m*yiUvi(#uf z1;-wx@s7ARy%wQ~djP3Us92T<6aCTY08l35F3~rW@xs znO_V+$s1G;i=QBEw1vU3Pwk}i{3#nP>~+JmQW|4q(Pt8clH$ynV2|s1=N#aLJ-&Wz zS2UV#f?iy?BhFm8W6(4iv~GSHV^NXyko0FBm}Rcrg{kU}nVx+G#`@!mHJpHyeqPKr z6Lgrdr^rU7D#=j(%P zZ|R{u#_gd#2JjFX^5)tZ`w61jvtP`x1qgLI5q4`4#CapsWEhp1ESm%l$mnGpL}T+| z?@wZ5@hJuvXD{AH#L6?4?la@&8AnTMk;H+}wz06RNj^X;ZwuB7si%=< z9lJM)rS=>{;H#5lp=D(=n(k5}s5D6G>Bm1% z&25b2t5a2TTv;n8-Fpr!v`+I`w|y7B3>^u0nue5~^_Wj1wrQ)-Q{lX7w%LTLapV}S zU}NSrz$z1N7!KELA^`=x@CI}k?Cp3e+bR>gqs{FQrlJMm(rzflHaaJErBK6;Un&*o zH63;rpuq`y`-PhKB30SJ#oBo+q?##C--15`GXw<&iTO3N-?|szKyHO}5!NTPF%2u0 zmHwP{wF&^k`4!WVVGV8n=aBM5Z_@{{(DMnN!rJZ?_-Ge=P#dHHVpXj6BHxeS%7RQZ z5;1Zg@2h}-gOZtugHqU(XE*BOw#HJSWIx*1at zRrG=z+kI=U#BiDXcaYY~o@1WMB|UH8m?VuAep2nWY@x&2tkj;XN(pgME;mT(R%;^N zjPg%R-0mgo>D{8sXwPeCo4nzPuhh=>sCHglSrKvvajw*GSpq}{{cl#mebopG^vLAA zj^Jf|{X7*fEi-Jpe9;mo5bN{nL(fGVge-dF8Ue0cf&mB=B<1|1TRRMi;XYY)+8Je6 z)|y$Pr8=f}|Gi8F#A>t_tDn>=HZa$+R_58Ntusu@2Is4c*i`s&Ov~aKz)5xBW)nto zHe0i2-#|aF*kXsT+5^IFNf%zBYF>ZXzUC^W&C5k~Im9Hr%A4Gwy8-Mk2l#(8j_49a z+7)`}R12t))N8XC#916}iuKwGx+@ThYtWOO?D zFTB$Pyvq-=m#?4yUaVOWSj9I3{}m=cfq*#vIRX7!u|^_eZ)|8IV(IMSWNGB;Vrg&p zFV0>`TN+ys6LOpaue7UiNVcXV$m-QIYJ`mi2d> zN*=@RX=EOenr3$$(wpB_CAIABy)kR~B?22(#`W;uA0{H&-*Wy&8rL83?QX|_zrLmQShDC*nAJ$v8q7U*1(xBK1(s;q##37*2IvNSh zwr=LR#R|?jr6hQdK(?fo^jC?eiF;+s1TB)E_FBT*H)&U{lIz3qbtKQ2HY+B%ep7+n z_ytKe!a7?!kSGrXf&p(ojv44{`cs3Thr55=<*1gV$VLP|dskx$GL&l5#61gJ^+VqN z9lx@G1&;#?massD3C#waJrev|_Q@qTPP-4ixHhrEBevY5SFq5d8Q4qo1sobpScc-% zlIapW4WVEJwW*mg4cc2!OmwO-agkj!o}pVvG$tjGyP_80MsjiUD8o|l1)goh9L zS`Q>&2k7Nq1h4u!?d#QW4*DljC&<4?!i!gRKm1=YOW?0q#PZLP@IU+niGq{8ho_*c zi-oD3i>0yQ|0*}iS9VB(D7>J_Bc_&)s`V>Uw$>q)G_RKpcNod+9FgK(P z%wk(;FCf9Dit(O)wTUp7%@pjrN-(0z@I2k{h& z4~F7+(KryqTkF$=|I@hh${li3@!mq=>Z}!_Wp_`D>c@#H29zqaM;mpxa;5b0y;dYU z=>8MxjZ5&o-fuq)iJdJT?fk*|EhMk7g%2fGat8fS80}&QYkaDB+Yeim^_98)?{BZ&SKW{kHEC(riD; zhlrcGqj$)(j!z6}4Epde;Noe%4KH~04;znvE3$&p$l+!b_DaXNtv|Yg>(s(wBAM=? zkd)yySesZSr_g#ctWx5@Hu7K!dX1|%JHv>WYg{?5tVAJ4Z16Gc8;#GIH|jI1bvK4FrXAHDcrx5L zkM{>n8}B?MRqQ8SYnMVh8@B)V05kEL+D-f$T)cmkQigvHFlA3?7gO86(e+nObg?vb zmN2yYt402cA&ODcR@PQU`C^mgAjFyIVUf@-ZVA>_qL(0Tra~b>1tEnf(K{#1CJPI7 zJ{Xq-wtsrp(#J7B53+}w;h0%UWB48I`yyVkD@Sy=GGWsAykYI@ebv&}vMTrW{t?Fy z+#PQ3dpc75ixU5w;cUoNhC2OjfH=nfE#E**j!3DiV9yrZ8S2>AVz3xpk&{lMfnr1! zUxh}}qZ)D2qYRN+Bjq4<>#!8vnr~~gOR1ZD#*E=)8=%sI;kT;5+0HOSUygZGV4cfcL~k zev9V}!;^)ZwQgHl%TqF4kftMTOW}HGHhPR5xupqM=m`xsqSV@km=wJ2rWRMtX2F`> z=zJTRZ4~m@9CK5qWZN{#26CtJ87c=pMX|L;0VZ)t*iWd%YU5&|2d&JD?$B}L(~Z=C zJ{WAqMa{B3A{mB;8Df7ELof2$GTb4iGw1C^cT9;UxoTBTfXkcvO zAL3X$>sZnx{uI6mO*>g(&kc|m(P6r#R2enAp)@+qr!%A%Mr^|~3V=8JAN-S|BVh8% zC>`65(EwORVnZvr6|?9wb7$X!^xpK87Jye^cy5^r4@FvKdpJ6nR@_U*GLz(W2|=w9 zsLrlp6l7p+nTnQJcNI}!F_iGAU7ey+;5A@4t+CYD=8SZvc0yU!EM_Ux4q7L@)pqiF zTkhP`^n$lm`$qTJNp*zmS~5YY0bH5Pq(TBf4Bi?Eg9EOK`chD}HVQ{Cb{w9c z|4SDYAApRScfgFA2MCO$zb*Myr{WW15B5WTM+t;BB~3$swj{>Z4yBHI^ONmD!4ufz zV$vBZo6{}jIYq?dfRdUxXxv4{NE~GO#VCJ5g@jcnkiK=Z=~i!yD&2z{WLbddhqZ}< zk*3_nO6z5o&)3uCSNipDK-!K+=HZZrS$v&r!`XV2>22aze$56|K1qv=%B~?qTL%7w zGQXFp78SBN|D9=T&Q#?-$y?|$QCdrt{F#{$h{goO2Un8rEXu0;4fqhaWg*@tFeBqP+0OptF|;X6xeNL61nagmA#cXZQGqZ#*UV zO?`Jx#RHURffxb=V_`7{85DJBdV{hg_S%G(YOaP@+0%nQ#63ds@CSv3|9lcAvx+^7 zg^4>^?5(!_blsvw?fV$V^C}8cScRJxd8}wX#?itkWDRRg@NMSDoGAbR<$%m5fM%n4 z1Yf9hf{kwbxx<-nd~&+*58loncHnr8^7_HI5?q%B@nRd%Wx$F=`Eq+GY z(j4pfcwnwGU`-Q(zw5lpAkCmiBRAmww;b{T)-QV&)AN4N|M&GRrwZeB>2EF12LJ!f z@Y^}N7~0s7F#b1a%K6J2pr~W|$v-nqngt+eIcQtVi>=YXLKi4N20}sF3a1HR3xOHC zHgeniAY;C`q)DrM3TQRYuuSg_CvAw9-c#rxH=zCXxEx%!J%G$O`g*f(n z&gR_pNqh0x<=*A={MfDQ{b7Lp)e571+YW=Dyf4gz!6Y*tFHaQp>j!fLn4A>_S%Qcl z+Ty(lse{z5=Ynrk!K+?{(!jXzHy^Z zBQpLNIoF+}k5s5Uxr5+vp5#H(s}53LnnBX55QseKZ8Hxa@eumRm$anNS7WF-D^J-z zHI%M7OxKv(L#Xc{6|cp5>0sm*B_KMXWvjZ{o!TW*Zh-1zD7CDEc^J=m@4 zUC-W2IZE*bPdOZ}1KiBlbjE3Feqt*5441XE7;c-Ba*}bV$wU`f%rp&3VNM(;4AG`Y zhmEqsI%4|K2GCpNGAiU+;;ns=^=K~jupEa{{2Ny-ZqT-XsL9@3+$X8af%k9EME7DT zdL_NPOp^kh+bmbBH44fSBvY|XHcOZ`X5oh&rFkx+ZDY7cS7!D}IW+|D>J#b;&DP7C zlJ5<+mdp=yJCiyCZU;L1R1!1IA@z=sce|i!{9G>6W7362#;Q{czmcL<-ze0~gZP2B z$dioNJY1tMFo~{uW4n>bEJLYBt(CTLENJik@lCsoBR-S&MLO+~RTrMPx(kecyPQ8I z`eSaG*F$i)+>U**F9vC`f<8gLnZjqUWOynXn^Hx8P$%iqb7P}C1jpKLZPgs3ytT&M zO?bo6U477Z7w>#A{RkS<{!--p3wKO(l*6sbFte+7#-4wWKa&+No{Aq}5AMvd{AvN{ zuO+ei`;DRME}vL^^N!$6T$SyWhH_)9-Ie>DvAdCZyMar3yfz!9^l}nZWGhcIi`&*7 zxe{!J*)rP@)j;g)1*DYv=RkiJ_+jaECX~u^OjvXHv)OEUGTI z5%xHYFBW9u!w0hrT@UIHhwIc2>nvLqCs%W9^O)Wu$yv-78n(g5!RM__FQyUbL8+!d zx2$|y{ZX!sR&w3RPG%bvahWHlMCV}2xW~RAO*d)=PH|awjnKinvJZD_E)?te6>Bs`87e+# zcT=hsbqYzg=jtQJR#{G>h${J2Oh^zT$LJbxZ?oz zdCb~Js#N#7J%y2ps_s7uSQSRgqSd=PqA#M=1p*NaJ`G-QZMtx@UHSoNJ;4?x@T>Ej zTYR0@L~`_)=F>jeffiI+BO8!+d|~cODqYGdr&R}VRH}~VuTDAb@}hNyzF-v=r&qNR zT-BUgya?S#Z#)RnBenZEk?66!_m6ZT?C}`kZD1W?f=Z6pq$0}D$&s@K<_8P@dUc+$ zs(05^Rf8U%cSogY?3S5f7Q>3}_U+Nl?=_qRb27Nh?(GSlIN{)q#?EU~I=kEnk-Bhe zgv-7-)|}78vT|2Mc}^w^B%Ue+VWs1RCx>`qaJq&myTv2B4fx;v2wx$FEqFhd$n>O` zJTzeU^DFldU4g@v9xznt59+-j<9<|U3^ToZ0>LxNC8uWHG2|V*l54X33NnH7R0gPM z@kWThsu?Qbl4J@WstaC?QDop59=188%fvE`QhD8!T^Y0Lh2D%}>{E~`N{x0sClrhc zh5pb_sAo^DD7?8xdW>DppX%D@Ks@b(9}9W|+rjV78vdCMb?dJ>GRyM%LI3YL@y1Il z?dC7$U;c~vY5y7X3)$Pd{ACji9b^qH?UY>%om~DG@N2p+sv&%#Or{cxpqT^zlz<@| zo-tCEvXr!dYKH{N7BGfjEkGIRbpvZmo?s%`M(=!qzuiddO-0X2RrY${@So=w;#O}& zvUkYLc1~OG%it#d{`2`_4x~3A&m`obJ!&{5tr71aI7&b_p)pK~nU-)cnFxcQYN|7! z+S`ph3{_Sf)K0eF8BB?}t&!p&JlgeJqP^qiAB`jj>hPELj!hrWJbL$E3Z~V>)P(3@ zfU)jqVG4;g1O$#E%uHVLe)O0cD1;=lSjba3%OBcw=&Kp7vbw7gNKzS^s`{$5F}v0x z^E-1=AV!pnR3uQTnTwhSfhBf!Ow^7$ELVxLX{4&m3$0e`Q<Vw37*ldO`jX2H?M?}WZW16~7e3r1*O?D#={<(WU@*&&8Z>oH)j1BH zB%EvM?N_q&o`0szW@V-vCm*ymTP>|i6%+FnDep5Aq$^B`y9Q`}PB}DF%1{3u={{Bw zZ>cUaF{lZtV-CU?EDV5)fpglR`8#UQB4iVlZibeE-TP!7L=jt31C8-Q?RZdtAYtEv2e z_^XD%EZy70yKZH0usn)SK-#O+fUu8j zz;=6SPkSiD{5aBWG&)G%j z=GWkI>em+7sEVaH?T2XSBrpTt>Q64PQtbkt6xSiWGFLpa(miCGSqZ*P*_tclLfca9+#UIs42wtVx zv*e~MD@-;dAbr*MklPxeIg~q#GQ>HBR?5QhBY|#|y^;bCrJ_Qz0-J|TuyeSxwlQ@J z=5GwXd2e~}AbFn6cqQ7u+ydRcMNkKgxm*mQR|15$;*6WAYJo-(OLFj-$Rr}E`N=sW zoa_b0>s1*Tq0sc+R0B=`eW1nVPbFz+A_>oo>)>oJt5*0=%iAHupM+c|en z1?0MdH!=h`2;oQ*uViOC?!+`-Az$J;H|#JF9+zOVMckosT4BFGfI*xPk{*xvBMf!= zA=D5DY(XZ&hl=`6#WbRF^T-&F2quiN9EFhjaixT_ZQ_U`0FT;YB-Z4o->O2>wz#%lGz(!3Mwd@goQ6zrP+(!k7vS>EY!Wltd3_a|U4&wQ~a?fHTP?#?E{q z2^`Cv57~a@Qaftgta9lPRbFza&z3gO8zG?Yw^p( ztmyBGx%}6N|9R#7KXlx`$_d|S*}n}#2qC)`tXkA=^uM52Lvsi49SG@o1Z{+EASER$ zd$OS6_X>vpa$-Ekym(VSbuR!&0^z}a3?m_*R`#?>-wIO0&X$^aHIxgfEpWqD=8o20 zmUD`%Dq0M(;1JhHYH}!A&kGS<=`nx*s%Rh~CkC<37_=Q}c?oW;)jg8~A2rEYwbGUaC^6rW{vlW+!}y<| zpGDCpt*}TMP4T(8&NrNS4^;kNU+=Jc2=Yl1duAYM4%C60dJ=r+;=K`ljPXCT1zv>Dvcqy zIifqtMuQ8MT5xseaa|3Zj*5z6#kLeXTWg)mMi5wSa|#r$ax3svMpS5Z&pKia&u9}A zu`j1 z5=G(SLOf^(kVlX?OhcpJpZ4$OIz=FDGt?1QwUG_g^sml6XO2xtcr2#D~XL-K!qyMNX28jwCH ztG-{k&0Wk`KM2tfXx8f2Mj>#i0c$u1^$GC?4Rp;CCIf5HYQ&azR_46S(7){#I%^kk zY`4iQvAb*+*er2RENoZx5)j7hF5*9W>)2!ujxC%Y`4-6Db~cys7DA8J4R*T${yjJT zQ~s}Wal3!?fVfAJ137Z71~orJaB@?Jn16)*iUkKvyQPD@Vao?J2EOcRg5HccJ9+3W ze3syRO$d;GX$?VR?U);|)?!!;e8B#t+>^)f6NkcFeTMtR?-)6Kw))9$B|-BB%Z_d{ z$gJL~V`nj6d3y8rqqura2qNi!7JR{oyZVgutKYHz_(IcPOh}CCOA_j(bMLL)$6g4w zpV$}W{OrN$B@MHm+~;c5+D0Me{Bd=w|I>pBnYO%qEKbABFc}*rxWU&V=1^O z9Pf8DJT(fTlbJxJwv4b?8KKf`J~e8g8|>65ifZZ9AO~-@GU_2?vmhb@0%p#g-mudj zz316PhKTE&8F>@-^k12IKTAI1t`&GVUHhHT@Fj9^-CG10a-Yj=@1Ni!zj#wy$*X7b z>{1k>s4sN)G06A7ycYRX;cshgg9v7tnTnRB%=&@rUg|yCQp)7^0=_zNWYgxYfP0NR z*N)eEY?I00F~eo=YF>RQ<38xVxQ>a3&68WNOMhiT>EzFq^$ZX7*s3df?8G?f?j{2P z_MS~ysmq!^voZA6A-TzGC~KBXC^s=7qhY62E1d=G&nBB)TT}L0?@XBGSWUU%EjuwT zS7v@++k>Lu8Z^pQp_xa;2w`JMjg{8&=lZU}V|=N3kvD#3Z9--`Gtg~)%(#@HczTu)Tz0vtTeu_4V3!u5{)}$6XdbmA>R1suiJ>%W#Z81vcnO-k{Tvk>> zjxG1HkZa>-H~CZQc}P3;e6-$WcQ#w9!m_>gcRR>AxZ7m(6?(=N;bxv|_{k_0*J|#J zg0}lz^kZpFIJ#xa!9tB}R>HWmNg`jecUx@xpVTZ#=}K2sm7{tMk}X|Pj6_Bv@{1}C zl?)7(pNE>$ml8+JsrGSchV8Yz>{d6`29uR0tIo+D1CE|44lx|49w646=TZ!ZJHa_Q ziAR>Aj*3)pRn+1szxF*wHWm&&mb1dPmS{pqv%9kzOhWbcq1qsZ)7N9Y(IO*J+;U>vu59(lgYef z8ssn0HI!$T6P#=yMbPN+d;U@%RHNxE5jI4n5ul|U6n2ZlSEU>zUb6G#&shic!|Tdj&~hqoIHy@j?I|fZXeG zIAXrM5^R1$3Bh${({5>EL26d6!9$#S9q%dU)adGO`>mHq2M1TmGe%01rzCPzteMRU z@c3~_2I;(yrXkVjUg9|pyRUx5ZM8nuM$emzn5k~LMXreH@G$mmIHlMTZCn(iGRZI` z8r(Q`(S}U5dJ3Lwbe54&cCk1$Ysj*vlBz4vNo#q--Tvx|7OtE3WV8!v zeY+KFN(bhth|xCss>}_J5s{f@o3Raz-qVKdSxHwyPs^x36(REq!mz! z8>!Z^IkwM^wdcS=wq@S#!`3?fMy(#ri6JUNz3sKWCFt&yex@5y>N~K@IlJZ6V3X73 z8>N1#biY;WZW3ePGIk+dI&{n`?n|wXdo?FzB@CJ~Q2{&};R!Dzma1u$bpW5`fVYu#3hOwiUJpy#w<-6WCV;0lnqY<` zXe}#q*?ZB}?9tf~_uV&sR<~q$z6Pc}&2L$ccTDawu10Qrgux0J6YR!$HhTCa&m}ydNF6WaC z(R7akrkk)xuBOE^w6IF^1D?`h z(I}cD=;}fGJyWR>s&1SDGCvXX>cjD<%S!KGxM_1b{gt_Mrp+Yu z#;%ExL(m$`IFP9It>kk%h>$b}xDB>f^F)ES@8N3wp%VE467mjDn`yk3iT#7NPn z;5IVXR#qO!VswVez^gpJT;=ySZjV7)b;MXOcRB+ua;;fcSb0sZ*tB9=+Wl(Jp6rV+ z%jOYw)cRw{mrHv#Pa{ubE;6CkzSiXN)XQwaFT=s=1meJ;Ep9po^?`BY_TTZEzB)=$NKpL@3;vGsq>S9c4QZqYBHcEZ9c<>K401) z{BCu6SN8Mj@a7o2`!IsZhj7F}UDd#z6oww)tT=KZF;2gf7|E{GA=4gog$vzo+J5l| zf(Ns=C8!f>?TI4NZcCQkE%ZfabmtJnYw!bVZFi?70MrM~A>L7aJbXGBGQAj=Qiw^E zpe$p9c`A|x*R7Un1U_|uOXJ4{fnQiZOT0I{pL)q_;j-fOy>dD>O8-H!! zHh9-ic+@%UYil30Fz8eKYZ&<7PYFP)+fg%$INZ~Rp0=KZ^MAt`rPAX5C!8>}F!~WAs2h5!?)MOufMx$>Gd$oq&H?KceyVD4IAZsdNrjjVD{j$W$F|6ax5 zN>6Rf&XNfqlREmW@#r>LNuyL2TgbSTwwyEIsEFn!je*WPUPFf1rNke&#e9PZxv`p0 z@y4i+i7*L>6CjeLCr>PpU`SXZDiBe+ks#3)%E4)_9VXrSGn`oOOFQj_d+5IxoI!y2 z!#H`()_iZg3WpX;rBe!m{EjW8N;0lbSC@1u+Bd9I3M}kSZU!XT0my$;Gv3Lq0gCg` zMnLf)Ne1Cf%+_Eq8(hof{glQAgCG3gdEg(m_?ftWw}D?$0s#^Hb6F+%-`eQEswz!L zAC;pPesbmv?hG9QFd>W;#0V33ZY%^?$@n#d_<9V4J2K}aoea{Q4q>DVHGDO#E3s|c z5nL-v)D*CoNZaj}R<){{mX_9)=a%Q5-jbWY*8Sb#ZpI`@2SV28^Ri>ldGVcn!>ehBY z>QA>YA6-JJlp(Ev?N+UWctERTSKg4{%AYODd$%J;cI?9C62BeF*>DJCRSvX#ISnLP zo;>eYCQF$RWZ;eQm<0_c#)@0I5O2^WVrRe>IrO{e{3Ph7X0H%mU3?q$2)R0i4=OTa zg=|B*-HLd#h>$!3I5w90s;$*ddr?7!D*A9SHI(RJIqN^n<66Lzw ztW}D5p*6!&%ZDeep*6KpEt)uYWJRV=va?*0C9{|XFjJRJs|X?XlC7(FdOS6gZQ>12 z`EuxHM~kpFEY-9_&cadx43>yDbzTq{HS-cBpS06zHY1<(O;?m`ipX;5d-(ftAI;nPe)j?!(-7}v&!zmP%Bj7i@~jBdR+ z7K05s2=oX;Td&Rt^(o`(QELO9opv~)>5UK}@Xa4uVwJXMKtfy8ti`f%%yqCRn@?e3 zoeUpKP2-efKVUUNb`%wV=1^XjO!_5eSkE5wTeob^fep<-w9+tNHtStJO|V}>;+Xd~ z(G=RB9iYXyln^u6p$R-mjxAO?M&r%3>xqki{bU_T%3*uUrVuhJW#%n#Cy^aGuhWF4 z0SSi{OUtb{F)ichp%^NnTuEvcP+u*?Fijs=&Aq%tDL=8%4zaMl3439cHl6@g76&P< zFfP_Itpkbqe;9kmC|$Z_UAtDI=uV?m2{xMj_2Ff|G(rG=7gi|!L z*F28JDpjJmUD54UP_$C>kim#}`@v(r94s=S%@DUHq24wY%Y)+mu;DQ@8Src(exhbB zTLJ^97|N;R3)2WoRr=FP_K0Lxyk6+fO-A+8!!b@0Zj%npngK&htPT)$OLSxNbJ?@#jkI$U-a8{Zodq^Qq_FCZ?oCd@`2N5Ml~;!gwHJyIJ8y;|D0q^g_0Z zF_|22Z6TM>UFy-uS|gZ^2%MX&&kF2*CGFF+^z}lA8c<7jD`s~> zgBD*CO{dFmEoo|+_MeLTPnXeB@?zUgW~c&n_Hm2fH4a-ia}`=oZBp{_qz!W5mF&)f zD?eq^7W0tiIQ4DB=QOAr%47l;7k36Ejc!25YM+(vEM&Iw^l2pn$8Hr}TwNLp#A&#U zt&Q!=GbeHS%S+uC7^Mr3j@=VCi*?JH`TZ$BB7RlPDt==EmS*`%oM60V3WHA&5+AP{ z^}Hldil_l2r{*3gFN)5PH!E+Q-`x|dk9Z(`1b913ZehIgM|F3xYI*QqZv2${(20{4-r86b*@b|Yl4{5v$pXz!Ti^ID=MY4)AWz zAiu;8@b0iEzXG=U)v6Ui%idAHln(y9;LPV#HAYKx9c5FzJ0@X{CjnO0!Qw@XjRjI0Rl20snZEc<;5<59i;9_`w(RQ{yd3R?Gg=lRiS+bSxE z3$oR#$VXDlW7$SGW?GV1C_-ax5qH!c-ww~e5JGe)_6kyHoNo(U6qT0XJeZ|*>QM%p zDaKTUQ|thATUvz#DH>{>jm>xpqnh_)d~BWi{Bz!TN~}Dy!Qt{DMOHG?_*4syl(nuF z&4oDj#!yB2TaF2iV?VTGRV8QlN~UGB-3|R<{b7JJ?elxAr~Mq5jUZlY7Ht**SHEY! zN!eI>ntTOmk($D1dZ&9HO(t}kh9({>E|iEV-di%oc3=rwHwihBwPklAcuyjE!FuTT zH6qv8iGzpv$MQ?+TjJ>6*(vLW?ZBKv1fKe)5Czc;`bDiB@4fS?`3i4;L~f^bg{Rg7 zg+sj5B?;2t0evnQEtX+KM4R`ku~<*2ywdFXzSu=ZLFRF_rEEWuxMZ>$W{db^KBF%l z&oW`%)Yloo#I?FhoFv=$43REEi|Xtf^jcrk(2a3%CwEIS0{_|)uCXDl)@`J)P^+Fl zxCZ(p_ecMeww^pd=4qoEcaQ?@E;&o#`~hh9WHNt;xhCUUOdlE^(vZ{WU;zedlg^u( zqr$bgZVhm>r&Z#ei{Gbn2HaELLcvb%JWK5TD+zY`MOzL^alkkO zn?xKP6B=tA&tkEJauJivDDl`FyCa~O!k=|HYm15ZU?NMsmQ0|uy$-Pqa^@g+qbd7? z+#J8@w?$`MvW&Fzw%UNbUrSVZdge_KB03|;EwK0nt&RPw&82K2_vDOB0P%QpZx!=L z$1qGt^^oJoT8=MyT$$y|+cj82>+|zQYkK`O^EcOwE0IY`-LcK+EPv2awTtIUvPVP6 zll`1Pw8f=Vcc_A^iUc_{_d4WYS09l?jow1<7_;K(UE#ccT%g@J!;rh(WN7k=xgQ82 zn^BEti_yWvd87;`s$!ECw%^jfjF!XBJqxi0V@=Bt;jMD0VdRZCaT=va)|UX=nodWe zQ@EPVCLfOpKc)}&Kc04>v1$50FYJ-FUl!=x5tc54sG1b zarK3?bUa`vT@3!T7ubTAwxNQ1^=nB)QTA72=2y%r+!YlWQ3u5t6KFxKQ<-rp3);W4 z=2&81XLP74sGiXgOOQ03Svnz=gP1mG#dv(sv*y?k{3BF2)t% z@n~?Mt>%AMmJi34OXnMV23d@oyS++~wy44&HmAug*?|OgOyOf~6U%0bH&Ft42s<6| zef)H#J@l!7G5&xoFp~Nf?XAYaUvH6naNZ;Uxj>m!_(|w9{WRXe>_dnH^&0jbUd1U` zyl@C|2Wl4eWHSm!333N^d`~0rv-Qws<>KgLNC%D*NQu!ZX@MExA`reUl5|>y-w;d7 z4Pm0_f;pNtDA#2=bsp9+;DmeD9LRZ=Wd0|p^PI60MNTo0nfzKnrrb4RMJJfZX#Sl! z#D?TktZu?&$^&OBo`_(--U84)enARV_hNPqIc!8p^2jc#W0f^NLjY{0iZW`OGD85U)sx&296;^J zYn%v=`dMk+qFia+nWL&yI^msw3YC@Q7Ck+?lagu3bs$0Co_3269gh$zAiS3f|EU$S z1#rG)HslNEO=)l=0Uj4E=GLnN@|)3ubvE8*GM25!C+dUMXh^(CK~?}mw3O{SL9HAs z&$#;5kLni#X#JbeOq;#pC`49Mv9bI5qm;>74t6%32jbB`)i2_?l4TQl>=aV3D zTh-W)6V&P3(sE}e&xfd#g_;P9e+0HQkFi4%VXqZ(K7MhDktu@k2#MDIqO|+0h-^K% zK6$!MqUtq9u8@40fwS1{$cT8#$m%9k(O+rHxP*c2~*$8Fxv{UX9Ywb~C85>i@M zk#Ba0nN%O65puFgzcMegyhEhfvMuUg5|`4kip1fp;uAd zc>%h|uxG{vn&;-Xe40BYurSBUbGBym`M}qv7sDb}Y%u5<)c866r?hhM3lIe?v3CM? z-v4v)$7&sj`%lE)K$rR4VA~(CHHce}02v)2gJ{P;)jlw-_~>TFwwr@ZW~qGh@Yba~ z5_SBOEzQY3VswI&@p{+sW};_Ymw$Qo1wD#lO!)}&)$;I#ZuVbgHU-&EYqB;t?lV#~ z+S?2ycmLAZi6U|u$uEGB^K|^FKLilycv-APy z;8xC@wOQLoDQYvO=!NT>;5T86;=--8ms|Z%k)a+cz1`q|rIW~n2B_=5p9r%LJNkgV zei5tIGY`@y$bR!h2z`&J))ArS214qILUGPQ{GM!1fQ*)l>aPu9(jnR9yS#^h8#m3P z<_J3eC8@b@jNwR=ue)+M8e9H3U*$@`E)IW=lP9xs)rxK8Ea|~Y%!B3DlAs@v3A;}8 zN3qsSfdWtdn310$=YE193}~0fnrIO8q)vcQ9ssywHWB<4bOf=P>}if2?0F=|!Rf0u zJ>Z1)r-cPeR*?$0F}c)H?P`?_QhIcT>QO7mZUzaIF3W z$Y>l+;Dl)U6=>iDPv8Wur0)}4124bog^Aod!s}Hwm6#cA?E>r`YuSaXX=TfxvEaD_ zgXC+PR3a($#3 zl#aI)Smp8atcp@QK>EKVzG5L`F}aOZs)$KlXUmv+2qFi*Ahd^QA2Zi?yL$L1cUP4V zuU8I&-z><6y)i1iqEvG3`0l%GoZ7);V<*36wiHct;LgO#Zu-Vcf_Z|qC^&wG^9hZ> zNle;Y24`k&tji9GTpoh_S`O9TNZp$}jNfoKP2&2EGN~V(&aj;MXdfZKL0xuWN9(;f z*~TH}I5}0x4bu4Ii|>D?*8YU*o#lNaDJjvvum5{$P4pi~$A7XG|0=WmuhIoDFQg@m zFTNP>+ZXN_7?A)1@NEzkbny^rF`PBu9-#IR0tk-%KGawz5l)7*g;*zI>($xR25Hwh zr?`g14R(kVsq{;ytB%=@r{}J=dTPnwn`*x8lg+2j5AUnD&Y}1(_h$xx^nO$nF1tTr z6KH@j)|A(yUCG_}d$>N_l7Txk2O!%;dpwmZ2H%vZhtxsf z%G9V@)}w=w+qa_ON=gR6l#S>Kx=0Uh30k<%hZ69jORo3iTYRK?I8dVZ`Y3t*qRXDq zd8>Aj+yr~bP`o4tSh-7g=bj_*vJ;uJ@AmE>JMsCt$lybryml^>0MJrcd zUftMHzIUzXBf7P-(XoLO?D+)a^!Nx4+!}R4c5f#o8xizYX~fL1N+`EzLaLCIRTFft zwh9l967$Cuy1~W8xxw^BsfGLvjEf7jZu+1;2Sj*aEkCrRQAkf;`ydAQYXMol!xf z%Z3NDK)#p#CnR+#my%f>jkau5oNnS!t?#MBWnsYB z5wRkx$U{R!d%0V$fZZjKvN$FMQ)UEuCB-hfhnAkb%Ad)8w^fI3;)SS!{MuMvA_L)} zO6s%;Ue&P)XEd33z`N=+v>7dq+{ZN&_9tWUoXSabdB?1Kf<*OKoNJ=NdGG+iW<_P_J?PD%u_tzY=6a)P#Y zo-jXs`6!>`gZTF7q{>pE8Z~7jozM0#Kf}7rpSeDB2?DuTG6tDg)S4xG_*mYdzEIT; zcRFR%lMNYk*rWSg?u-acPKhdhOpb`dObuMjhJU4AG?|Bax3VmzNnqHk%op^((V?{h zG~y1c))y60+ZcJh(rmD*fYyI#mM0El$)(Thm0b{qurbAG zN!pRALLr|ZK`0|nbiy1P@Umzp9A%cLr9_r54gE=&6GADi6h089mKTayn;?_E*Ezez z^fH?)qc+nV%adRMj8ZFwgeLCoEoxpwniP%#$hCSe+}j{X1iFD>N*F-Mz>|nsY$_x= zVKl7F)<22BnZ~@A3!c7?z+)U>u~cZ0Zc=D|6>BKcKie-+DvY!`FR1T~xMkYdDxLRIdX;7(F9Qu%_vdwsFXDLmtkCrZHu^u8E zEfN_k$83-LDIHcaC(Ki2=>AAf7onA&O1fbMC;ir+xI`RjEhn5$${G^c9_44%m(~pY zhsq$n9q-R9#1Rc?oJYMCTy2WG1HHYW-%^fit9eCbdA95x4Xd^{-4iz4q23Iaw&)SL zWC!itECbI%8n*OMWPmflLY>!vlpQl?n#`?1B^7yF;NWnQs*xn~%(}x`BZZHO_79jD4X(dJJ7Vd7rM?-RrYq1gMn`3(zu?v0#_S0 zSYsAF`ow`;yvVJl(nd%B9&^p&NL1pE$|T3S&YqohmNUH47bR!IX_LmX$W}9~R#K#L zZpRBdOq+u(XbU!|nkE`rtvw=?G)qdvdy|Ve(E(f+~}uqxfZ?hwo}-RMq|3 zkeHA`ku^A|#nH(H|F%t20L9d9d83Fcvv>_X{vxYPH^j9GttuOjG+W0h00{PZ%V4xK;At7Sy%w#OB}Shxq(M2%ky#eh3D!Y8koB3ca}5{eD%0;?(Dd zyJx9_GuOaj>D~gmprgNYs*mNLgV8`GUW2`VH1he+LN{R8r2;2QUjL?0im9t>Ay*?d zM+5PO!2nJRCzD^O07elEFP4)|7{H(&$srw%FP!+y@!2ExbEvOqG3q0xwNUZ5=tE9| z1CuPO@w3!BAXbsaXAWHG6ZisNl-k~#lA)&s$; zMZv1Ul3tcA*7ldapFl+q$m%t2hFP-%OS>Vo8p6;Dbu?@AX1jb2b`!nW=IJ%vL;q0s zgAjfp0lJ09i!oFa1=?`hdBi$97K4QC$7N;H9w*w!F4G>hX#O58^E^esw$|xMU zfPUco9Un17p&7-`*7f4y*oRF}4f0-DNLDPzwGLmB&i_Gtyorf6p8}L-FUElav??D6 z+iFy7J&zcSVv~nh(-pNMQhG~r4%t3GQLa#D>GYX3F;+F1aZcIz0a({eGM2Y6P+p5N zsR#6B4b2SFi=oB%Tl(d^)p2>jXyqpHvaT(gt31EUmX` zXAnZmK8i67hz7%|3r&)Q-mdUU{v2Ird73Y@ob)=vv5K)0srj{nraZ2*WqrYj#2DZs z00x26*8-Z+8J0e@YUhF{PgZidcC{m}Tl@MHTwG75 zr`!cG726xraXfFf@R!tn=6M4-L8P(NS1Pocxh~eoMz1#QlQr$rEd_LpIR=T89z9>Q z&FGH8r2|5{1>)Jj*S~=!flEC$w7$#5<-Y|*|BIOD??_kK(bz%E*4kLg*4EP5?(bkX zQc+VD^BYhi605!Vpin{H*kqvzthu6bp*+_=Ru-xRjt?Pg6bMm8+h(M0^{(1MY+*4w z#CtEw!qcZfHSFIu(lX)N>3BFke#OW43s9}M%ujxN#4T@|B1=4k0$fi=MC&;eUZe(p zr^PP@pab4RUPQr6x>piH5;|V<_3>4JQg?Nv%C_)+RzPMd$z?9vDah`0nVp4stfpLJ z(owptb(DT1lIq*LMPyB&NqTz3>YL5gVrA)jg7jH9>~;k%Jwfp@H=C>$J;mnoWs%bZ zvt~k)L=;G+U$I#RuTNkFqoR4U8~0J7S5dsX&~M|U!UPGA+LvoAuu!5d@wSO{Oln&f zAK)@Kx1G(qjoQnkIa_!)uOgVDN0PnMq(wDZYh=*hYOGke7Vc*hX&xH^a-8)hGS@)4ZVawHSMC<> zI$xc14^{3K4F`SaiVfy`-6i&Vos!%UC3jh|bA#EZm*|PHM1Xk}L4exjO@i2AGvY>v zpJ^X+-0Uq8WHwPnPrVsh#{*!`pS2f^emUehKS;gtD(`{w5fWw|hcbX}-W$Im>Vy1U zT!l}N$}SWak31vBGz$cUrl+eJLq4KM#U2Gltc>SN!A*?Q%Z%F?FRNj6&)nyod`x%w z;pY*^A6o+28Q`BB!ztAJ462^86i6rxO{fy$0v#*lTl6BIr=0?DF_;nzwNn9g)~?Gky8ig8wgO`p>PH zkgbi0xv8^*{(p0HBIB)Pkp$p}zSfQw{dxU-^YREU2r_cEnIPbl8I++>#Jq&6?2?EM zqSm!pZWPT48Eyjcy5Wp7Iy7@C*816)>Y47cKQ6T1-lnE>0S>RpgC$c&^jO2OBcLzs z`GzLA*`8Y5yC(!O!_B#krXQ{OuFqB2YP5s(YwXQ_F~ST%MpRBb6hjAYzJ{G>LKl`4 z8**Cbi?bBdJasVn0|n)7D&2Kw=&D({M5E8@p6{6re<2t)xW3{BlY~gV4BCQT4G(zYT2rdn96eZvT`4k*;my* z%HO#3AT=de_gaHoq-}4Ps~oYNdUH^$^{5T5Pb!q3nWHS;qpkWzaHR#8eLa?mh_=O< z3a=oP{xRJl`n9DHrGzKLCpvk>Bb;c~ToNKQcj4i=2$g>IOQ~^N{Sy-g1$stD*M_#5 z<+M$6;`Q*dJd({B#($UzI^!3?C)EuLa?Fz4ZqayT&<~iOo9qwSx`|bsy1OE0)2mSQ zKR$y26hdx7kq4eHa_PWzkvoaKIPQV;GP2LnV0OTP zf6>41YD&ZuKr`k8w^V|PkK#KPq+S6jz~jeD#8g1nK~LJ3!FasGa6`1kdl&HylQHn%dOaWFPCHn(&7muIpJbHRS2 zkpZSVR0Ya*-xHwKm(|p^0cFlZ#H2MBD2{@EY(9@JdSbCj^cZ1jMd?wkSVRaZobxP- z=^>1?^0y+h9n{m@TV)#SZN;l>|7ZXOn^_5eP22PmTU&fvVZs|3_XzsZmVaiG#-+Nv zT@@*72KV{5SN(pw|I_;88~E^#_8R|eondHY{y%zQ{Pks`GbZP8-%cUpTk7<0pvS*% z7qYds{$|}sncEo4={tNcU&OL@e>nxkZ_JjB>EA0@qT+=N(l=ab4z4MoQF}C*Jk3ve zmByH2mn>uu6N%^t5;W)+%!OK#m23LOz;XUF@Moeh{!BmEb#8=f5tZN?@U!F5CYR$S z&(Y*R7O@{CyDRurISHhOMX>_vL5>tkvnj!a`UEgA#^z`&>? zEsZ>%v6`^1>5C@EYc`Il`rVenO-FI{25m0Qqx72RJWJ^m)(pfJIzseDOIE5CSASdr zzLKuk1zm1W*H?BsY4*rd!CfzW1-9g1xQxlY4qAJwIk>@rFElK(wi?oSZP|OvxSEvj zG-xhajl$X1;R1BoBguZG7J=(73#{#AFS&=mMxaW51QZEY?^(JH0|YIh&ARhum!3|-4 z7#l?mZJ<+X=2l^dK?RMpQJ8(TCL{FBnWd*Z1S7>z6Bc!rGJBnn>p*b|J^1yC#pRiY z%Q&9F$K)~glfx!I9p?mA@G`iEi)-B&{mikY@acZky^Urabmqb}hYZEMsPu4ZFNpi2 z8Tw-pT{>Ym4v^O{P zpvABswskwrFFlipjA@!4XM(>kBJKCRO|EZqY_IkDiN45gYpS_vigfub!#Jg8e?e;; z`o<-X)=x|}^T62GfAI}}nYb<-?`;0JiBo|6Kbp9J$+y(|Z##GV+r%pqt!3xF6@FiT zu+EM_vB*QFp#1ou5_0oG805XcM}(1_LfaW!EpZmASX_VJ5It*q5rj(Qh5oeya{npM zuZ#6#koK73cv|1$INEev%kJv@0FWCD14Ws`tV=l|6IW#@Cx~>1Aw^Y6P^MAYncNZ>ejld;X(x9?QtQ4XrYRFn;B#cG7d~VQIP_G|dg$Jy=ImZ>9B+XefgO%P zN+kIfmVeUYuZyh{fk443V6+=Ob*~mdV9vY|SeR_>Z6qONfp%9b0G#qa^Y@7%sATmR;HLx7FlT@|CKRp!3tj*{es!RvV8>?>D) zxAM9Aq_RZ3M?3qZOnhhX~&Jg326FbIcWSjJa zZ%ng^6NeVZPco2WCmBj;=-4H5%&CF{xan>XOiAXvltQ1J-yCLL{~{Os%jO9^Ml|BS zU3%)bWbofu_y6YB|F-jgCf-#v9sdFwB%qJg{zQcC-@(QAHAGrR1xzTA1OLM>=35{x zl=HJzSyU-jOZ{;6t>Xi#{l4;`5UxS7w_x1qwO`2emF!h$^_QrM(Mp|UQGDoR)-n;SnsE~ag!WegIEuu zG!ziwjhZ|y^^0L9=pZV{bE6*28L|^4--_&kFC(#EM23?T5g+tKG%DXePEt>rX89%B zQGQ4nWwT$zY^o+Wl%T_`Tx?*8__)}GQ)&aVKz&$*t;rVNN9Hale_>!O&XjoqMuNKv zUxUdq?~qEGj_FK+I2~yis491SD`Jt)`E=Gr;(M{aRsjnf>Yr^*Dx}RcR zw`5{juDQ=Lta#`<8==cqIHhv1 zm%~m}lB<@&c*U-Z?zkGKCthk+n@1E6q%H`eL945I?X+_ToR-N*O;R@It*2nyR0+bI z_Gxe-WN7fxlDseaf?tjyrxg`Aj6o-m3 z2_Y&ZHUX?-Ol5wOK74#m0o2%HQbil4%iUd@|JEycoS(5?ezRqMr!@S;*3(^fhTQ?s z=4h3pG$ie-i~6rY5=8fyTLOz;qFjVpbjDeNqufGk>xvD00>pPri#!4iMX~i%A>z#r zb|5&2xCU5(&HZ2Tb`x^%Um&;#y5 z290*%aA=yK*zK#-5AM{%bXB{Q=MYFydf?_+8EleU!vvHn~`v z&L0=GmNAaub?EuPTMDra%@mcYfS|H#Y@hFLI=SL{63qaf%_Sz3h6J8Z{ zOR1PDxhGGxSq^>elf~eU5t#aOu`40{7CBA)IYQi7P z@>3((I=!n*PIV{RdlfoQj^ygKw1$1uOZ$!m^<&G=IJ+yCoyJya6Qpe6uD#?ZkR!NL ztPk)Vkxd;{BaBV+09p7XJt~9C{j`@JE5X{-)4yB{VlZ&^v#Jpe)jzcdIkd>!5A9;R z1nEFVtKA_6?804+v;Z@PKAqWIBVf z-2=qlqG5#7A?MrHs-FrEe=YyP(|As`K0R`{+mFlLKTKVpKpnxkj+1|g!;4?21yq)JSytE_(8My$9eLgJN}Od;ZT>o1^~}T2@;o$`rt)_sb+1eS)4Vbt_*AlvIgD|94|)Js z>!41HC*@?IF_fv*MC`E$}VkZO!Bo?B3;XtH=?}J`}I<` z7~2F~8~xJtP8r779u_1pvlMnEz1m5aad<%_>S;_4I8=q7)MBdV_t zbkqo=a_!$>i-j5pzxz=~C+L+Yl8^s@>IFvNEx1E>5qc*|5PU;t>*Zx1(GiNe{FP1Z zP`dTsd-8u+tFCc$(YKWze_#LBX7S%*kbiD1|A`9;j1c$>7xMGVtWg8X<)AghQfNvJ z8Te-pQollmZ!n^wR;wNF9Uq>_=GliE;4pkJxR7wbv6W5JwsmDn&9L;E zg!ZN71erO`nlsT=+&7?E5JUCsw0mPAfxsl7ur{%zf*K`UaAEhzJrRLF#8snyBGD-d zhu|buP+U87uW%=isNhRX{FDR8%k&sz3oyb96x}oXHS!(ifhN`K-*(*@R$5cV-?-`` zp#Q*C|KGp$@38Cd5C4WsIT`*xE&2lO_)u9s2G zN#8(Ga9pr;(0QrSYzBi-nWr`AAA~1hp-zg#W{i6-!L2(P;nV?>^#l2IOY^I}0?W>k z0R5fEkzutB`%Lf+#`Wpy^+p9jHo`1oKk}gowP1k~cREA?wzzMO>{H9>3U7YvC|4|J zpH~I>{GkZ+L<=lW0>F)aB5MdFC-nm&M_jZ>qGzSfNZrV0k7&tU0DjT?T~fx-q!iq1XkWeCR< zfkmOf9kW+udSo5#)(h@1T9_AS%w+V@cu{?FX~Gr~k6zmIaDExf`Z5k{UgNO_roTbzro=wfX=9UD50wOIcAG$@x zAJvana4Ln5Q;#R3k4cuKp4UkdkQPCs5O1Oaqp&H1bwf)II`|$ILpaF^gSmgN1{nBA zEJQv+py5Y1D49E76-?@?m1g-Rl=krl(pbL`+`r_-Bm^L4qjOBRS$g6`>aY$PY*If*51a zir%@`IJF0*@fMoXJ(d7W%lbJMdTarhmim(lqU6Putn>lye>yKRU@>Tn?=dy~D{=6j zWBR`jVWhmYWFJ4g_uS&(e03Sw6d^ODNVPPDm^?BdB*f;QlU6Hnwz`dkPgb~30H1_r z>0ID=LA0by_xh`E%GuG=8CEyXB#skK24V)WB2*Pj?iN(BRnL0@#ZDJ`)e8H80E8!w zth)hVH6#LO^jLroW@>4RKa(Gpcy7uF3)%Vt1e}DLQG*g8I;cIi+aqF@r+!f&9dmlY zx~#%U7X!~xygX|<0}DjqYD@hYP62CYRpwwY^3OF@6oXI&v*T`0vU^bbE(`98OofC4 z&K+6C##c0wmKhHSt*t7xfU%3o^y+|1AE@35mtB96fF@R5s%p0YTn{jgPP#Gyd^d`|I6~{M3PPk1n zu<+VMgnZ00VbGdFH=)pBOf*oBL>txMPW)q~NU@w=B(E&3saR>8uONR<$kt|c3`VRp z0oHYl7>XLX`5yz4aaFi zlxDPOQi-ajSPLSZ0qj`5H*@TF3NB?P5w3;7 zjaY^rJVl7a!tqzKHciFLh(Dfm!OQ47z1eA5tW0z2bCOJI?n?9!uqH&lfiY$)Pjnh^ zse9YQx?3q;8>Z-~dW0EXX^GuZg>oU>MU@{tO?G!INWF0^ZvaScf(4GE+V^{ctcxcJg%qFZ-{Xvgb%8-A#kRY5^T%?|Wr5Z$F%Vg)!+gKt<%|&iT8TQWNu0Ueo^;TWxnrSGXOXs z9gGa$+^MO%Ea#Pem2rc_s?I5$h<8^{{a&VRclriG2~r)*^P&OF^O6BG3KshE=aMA= zU}HS6Q&{H_0%0_EkJFq6(r+fnndsO}aOE8O3zK%Y7}TdvGWRFN>~M!J7-&{}9Q_de zk68qxpzIn!eU0=huIAqb9qG;>MQemZP~i)0Bc=oGP?F7P^`M`~ zL6S3{9WI_7?rraYf*-Ke+azwZEsuUnqKZ3Y4g!kXY!d}{rRP)JCgva<=_OJ-q|bUW zKO*kI0B1jpTgR)HUZ@M!9jN!x2yUDV&E4KJ@Y=u5|4UixUyf_vV1aDsdr`3Y&L;Ey zr{nsUh2fu3t+I`!&3FII|0Uq-P##nG>+pnUgjk=E9~}NBFob*r9(*U4FodwMP#IYx zuugA@SaI*zC@4)TLHAl$OAIBN6784o*Y@C2ZzL95YfU>(DkOp+iT%#u=H=Q`MyL1X z#Qw+CpDTcUdrbl8JxEUa8G-T)`uN`cfu00E^KoH&mM}|bhn2l7+yQK%cLdk5);3Z= z=u@Dz#SP=|tPU%Ph-PP@Dm%V6~u~{Bd5{+Nf;&Sdcl^j-Q}B-WjzV0-gh=ix|CQGO=N2s1)Ui zvqhJ@hv`YQI{V1Yjg#7n=l*)jo)&RpK(gmYO?R`unh#Q=K9VzN66_FDvbKzmH@$Uq z93mye|L`cVWv=AZ7%B#xRi3VG#>IFtW}5$z4Ku`>-89d!S&LxM*&}io-K=wBFknOP zzCPt+u5owSw5@C4p^(x^q0leF5YhNCH@27&ra66;n)gv^$bqyQN$D{|Hr^g)L#%X< zUn(e&9HtK~Qk%0&ikja%K>su~*F(R6yaq5oho59EYV^9piWa8NykWrQ;P z%Epsc_edr1@gs7u@y9|f6@xHdFYK#YPz*9muChYI@Y6iWErM8{H*@Vr1Jw+zRHv>X zSWH7LPe8*tZtvEvy3klkgPQBpg0~H4cRZFg!lRiBe^hzC`{bbP=FTL$No`aOJ_PhyaFc znsAxhKIBkCR139t!^vG{W%p2SL<1`md!CB-f6Ge;7B--nA_4#mepiJ5|L3Luo<0A} zORGb3OS`R;v6I50S5^yfUyZ_uk;Oy1IDS)Liql^<49?)I8~WOidk^f+c<2+dJp+ ztnOG`J-fMTblrS^Zb1@e4wpTgg?;sTdH>0aH*!>s`9YKXDT8;mpR34up*LF^s=kMv z>13|WiE+NT(x;QU$oy!Ykv_zEbfVN9B=f>8xEITba#O-RdF01UyU(=Gq_Ibs344u> z7rkK49X+>PJx7dtwa0Yu1pFd}945}G-52kzMNMQnVj319^>D*%HZte!)ilWD1W6yh zj~FTGTxRfSpW1M63o#r9`Jx2rPr1XT$XhQg+i2s0o89NQub6YAm5J)uIC$rzF?z?M zlVcw#Gv<~n)7UN+f@;|=TzoeGDe2rQXM%dyg1L0+R7lJsU)EXJYB zi+1cqP#v@R6H_-s^vta=`i#XU=8_VjWp&6>_)5}ioxE$w!mCt96@1TG3%}!GH_Z1Q z5L5SXk91`TG*iPp|DN^5tzPZOA@k2Q3TJxz!q7w>#QRKM{XiwJP5g*Uq`y?ZQ3ZH# zrJ43{#6Si5T#9n$jUMqjdNgRCwR3Eg43otmk$mJI2I(p$!^JSWamXRRn{64Y^bm?I zoy<-fuDNE(zO}wsZGBINMvO@HnZag^Kg_TKy933kXL^)&=&(#E3t)x}Ax);1`PLSY zo~722n)05^MfL&a4VWB!=#c|hZB2sJuuSWKpkoz_V)LVzOU+EizN^|M^P@50+{0t& z-M=E8EEzIuEgn`;$}yXm=NIX=>RMecRu7Vy>0eZBm|WA8wQuxFC@mV*+saka?afeh zhM6f1sM;#HMhulOojX<_($)v{G?yb>f7bRLq}GR@XA>~3s~9t>H8Ubwms^CV(GZlE zaUfSnoLd{yTdj=JY5`_}n2;l{DC4bLHxiEz^kdZkv1IMgjUzXyWy+VYD0TWGlfz-H z*7?U$vNZ5w_S36nGe`?klqF@MMWkv9p;BLJBSVAjNv3F#6rQE5JIA6*vO+atwsJOU z%rpvu%Tp0en3!c^z>Hq?=WuP}24E`k>f1J%3VVr5R0l{9((!r5jLcC;rW__(VOtH% zTFHm`NT%`I4O`+qL-fE}0c4K3vT2U^_TwHV7syxolGgR@gfb=cLijLDV9a99ff~a# zyH(mjF@j#|{GJXY-czN%%IWkCFhQE=T@@2-8bgNV=rJFzX$Vnz6Ii5^f-$v0vR_K= zU;z8WX|1CQh2*ZoalgpK#zV?f*2)zhd$11^To9L_&SABx@9ucZ4+{7jF&4QT25x_^ zAxQ z2Xtm4ri37eEclAwiwpXfCTVHb0z=oCNsYO4F*HuAL30dy%^sK2L-Bi@PfcR*2u&r# z*52C0$aQInLz#lBKPtG{N&81m#F{M5Sm_bg72|6YsOpTnQ5dL{hu!|TG&Y6F4#FTd zNKJ$^t5D!EPCfky3LiBxl+1J`j_+HaFP=PBqC7QmtVao#PUmtYox6|A705vb;h(%r z1)@3{8+8n-caWQjGpglR+b8)ou24pe&f6z25NL~Vd?6@k8{zabUA_+@DjK6dPXQg> z32noATA+0Rbh}3?U>zEwM)x3Re+op3iC-{V+Xg4>?U*J4;5L4Iq8h4WlnvW>EJhd#C`w{z0 zL)|-a@C~{%sd~<>t0(*54CWJi3m{`GipJk|A1K5>4AP%(itW43jDor26jhyV551&h z+3jU_;sA837Yw>Hu}kmT`u(TB?GYD%8HuD}Aw<|fqA}>!2w31Tt^d`b5P&rc-5jH2 zaKE{GXl>X;qOsT*GrP1(`dI#q*!*^1-u-fqIu>-Jm>|=mGn~)Jw&x44{}tN*SJ_uU zRkd|((~WdD(jna--7TF;hop3OH%NDPcXue=NQb0!H~;ay-d8Wz_kQ=^W1MjgL)Y`H zy;ttJ=6t3gcZUP!Jt43DakuNhD%~Ux1aDV_scX+_IaIwFHYBT-tB3NMj%t&3#D<2RwFoMXZw;VowBD2SQ>?4 zBEuFOjLh!XUPu(IJMrh#J`usd>1#&tulr<87WKwt8)sKH)CLvu13TT2tq*rAch^EY zsu|{2x33|5?rxl*63bf7K|m;#HECesUK5$H_hE&xxXbUfdl8eCih$17B@&@FYvHk% zDEP6P(z$%=WyIJkmQB6N<&{&xD(1M#cBhQO>&5m1J|&(Y}fGe`U0Qwa$Q*M zw3To`R$6fsT7zkm2PNR$V~|=D{6k4KUyR^I8xL&zfD2Z!FG+S;tuNiAdYN42w~N9< z2Fsp_FgQ3`aqfK5ZMaE!u*3S1Wt55jnWr2JWj`9mcw@Dx!qzI0ml(9EgAeU~mANeE+%`A+iP^7s zhk53vFCy`vo5I(>O#%>NoR*t|_F@L)LXqCu0J6r=jP8KyExRLX!=gnWcFB_UMTSq) zIG!@ENc=}mw3Qn?!z<;+R?&@53;Oawf@;nD%jEX9iJ7YC{1aM)stg0BPc72SDdWQj z>x+x3voos1Ss@b2>~ZTaNp>ddDmrywpN0+=PErl^1rjjTT4#rF`)v4TssqsB>Q$RJ zwK~FROA!gmt(KF@2qj3VCmG3SUDdxh1u;?ES1u;C<~5yzXyH1g=c&hd^WuvyElJ`k z5qvT1mA`;1C|^KR7(iw|^DnW|XwmWmcsXN0vk7&HLvB%j{>mB`;$FsDI5c7(%#+_C z0}87&(P)+SGGRU|s%+$fKT0Fv1^nq(B5O9iogrBvWArMLQ3;l9zlwRe9h6nb-7G!9 z!T=xdP~3WgcOr(w8RlbWC8{>e39bjPlR*TUr>KM)R>B{RdXl++D z)4#G&g(cq)LnTb;_Ik?~liDGmfbDeo&V*Vi)t||VIR~@{kEzx8Bn1vgZsa=(r5cw_MGN$z*5BBh1DzmZZ;1t<39~ zCqNqlVr1(6M<5l&+0eFW$n<@N(TIet%{xSabD{{7@eD@qQ?m;`41SK79KgrEPz)|r z*{!DvQ?dwFdc`9Uq5BARTXj&r`qX`n+2~vNjqrRF6vu&glkGx*U*Svj*S&0kJZ;YG zc#+vF1$hNEPZtv@K_`N+-s%1WhpTb+JVDnt&hB}ZW7pWSek+rLM)>RG_i0_wJj^GE zn^%)LWNMs^Yfvj8YwP%&-w?wfb5}suDS4P{`@UUWjxi(d19#dA%k5r$r8~^5E^cKt zK%y{&D&3mBzus&UD5dSpf~_b}q~(+xZ{mS|Q*q(aLfQ05v~Zsu&8rD_DuA*)9pqTC zH$)XA3%lHG;piayGp^7O=GvyTnBx3+GxRb`Qq~mm$2+RqLYPoP^pU;HF9O`n0V7fN zreUK;-!eG@3dW8kEij74fU(jRRtraHKcmGc@d}+)Qr}0r4Ht|vsqI0>IR|ep`ad+f zew`W=HKw%vB(B27;NhExkQD*ge*-7>oifVMKLa?mj~d1iZ4dC&e}C zD|tBApiNO8vTSDryULH$OML{B2OQCl$ag$q6&Cv4D!cEmm&Idtt(`$yMI+4^^Z1#% z)BIksrb3t~?0h>X*~Jwc;q^mn95XFWy5P64$WoCgpPbB?%%fRG?gDnZHq+5AY=I{lC$srho`453`HGxDM4l%-s+6^-)$Xj?nvAxV%ypsK1*HSZQ`ANR0SR;`!&yhrPm4vLI_)vG#%>?*dyeQp+-d>~GrJ&A zur;DOqIe8zP4b7OnYJ6M-i?^GQ26IyM;+?%O8G@87Cb!$3e0zDt8hfcXYC(niT$_Yb@RiO4%6j zl(Ka_6lT(NRF`jA)DbsxDGT8g!__&i?YKwgyB3$P zK;%dYBFRUF0lhB@6ED#GMQn8%3Kqo#MQ?e=@ ziH5B%;y3TV(rQ*$l|x|JtLJQkRNX_R)k1XSzL`)y z_aY;D+k7ZdXeEoz;~8T0saIVyI^U|;g-=-2{sQVXYoM&fOEMe$_sqUPUR71 zm*afYT?c|OxWn7=5^~XRG<6pc7U$tJ<9~|)R3*Aw4;^V9V4y!s{`ngyd0a!$lJH>= z!gv{9l#6|sC{Z!Fe8}}(VQparZ4^*i-^SFkB)=ki^w)*xvxVyIh&N(1ljDMG-Ox#5 zti$eXKzDua>Wd_~Ms#*Fkf_%=8&^5Oid4<9+J}6|z{{|3xOCIZAS=A%#6v!!-!$LKKAEwsH%sr z1uSNn6qPZ#WKN7I&TEQ@AcB2OzYcJpKbDZV1(u+P;tFduln3czWG#hpX|T-$jBA1} zX!AVASKj?bW7?(n4L+BRc1`F9c9`kD_Pq0>S*`eI!p6J-mk>2L!|dyMBxs8K}P>ke70P zQl!2O0&vJ!4N+E(KOc@%XwAHigA@cC(?a4Gu>D>Mje{laF;-=lN~n2fF9~FUmMQIu ztc#f}*{4gVTW+`8mGn{`BSU*YX2psi-n;GOv(%%-Hk-SV!``Pf4`4p7<=9Gahq4S( zqHGjBWGO5n)ll)%QzvaRZCq{5JXvu}1U-Q+B&^xh0yuC7hI2pqHdQaWwLvM{da3pH zt_2qoSEW8@SDsK81G8>7#x^g@(@*vKt~e_?T}S>WJbjCAy^b~@Tkhq$BCfC533v%N zF!4Vg>tWedbX`MxkUKe1gr=Wf>0m{y8cE%q!Q6vfNnVo{J1KUR1JY5;l>oB6Zh5}P z*$VWH@%<9-muX&ZvS%+VV1v7aWPDJ)A2oxd5kh`GquOPBP}$eV%(QHjaO*y7Z`(|k z>=D4%SkAw?H&}7YEu@8%EQu&2^vtCaIT~mwCWSuOq;SRTVzJ}p75Lbt~)`taaQ#D$TTW#P{ zt&{Zd^^&UsZq}y;BAE@_AzaQ*BW9SWP#3D!r!#0$6Y1DoISY_Ag3w(;wogD*RI+Wem#H)9RbU?y7GjutCrHVB)Jy-(!8bSY@%}zU7OSC z%V)QD(7F`wK$Sm-@3LX0Y<)X5R%bVcxy(@Mgl~w-BnO)r3aDDBtIb|Cy55VpXG%d0WHM`FWRRcI1wIjM9%!sa7*T;^!tXC`V*B%RWX0eq+6u1I=zKT4lNSi=PE&&6&mu z$DWO;!+)9HGi^xh3|cPcsL-iFb8mjH)U^UOr5jtcioA@}a0z;8>J?5}7mk}Z4Q@3& zxW^A7J!nFBo`*r_=GC;`%4p0yHE%+jZ-zNpFlkA5EP}VMp=x(-hUVy81k-9s!=0J@ z@~|MkI9@w`5zBu}ox9DGtJGgR6^ch>Dae;4$|+)}G7ip&4dccDrMo@9qmz{V<SE4rlTd+GwX154>%e!JXIz``MoXyfG!3l69-dGXmw-(bh71p8H3njf>(3zeaa)80Ol z5*Y%~(+KESmMOhS$Z$~^wfRi-a|8H@vi!)hF&FujYAO8jj_9rHB@RAzZI?svw((fRyOZ85Y=QiSWU$x9kG>S3Jl$j{3>uSh^ct@cPF~<&zA|o&q@Xq6Jp?z$#LpN80Va-9SxjK^$ zEZdm6dm@obL+G+Yg$RUIeW{&S(kfQzfmR38+Ts_pZmx+Of1KtB z`Gj=zhUW2*qa|d~BjtmALRA*()jJf6UjFC@eDF0UZT}}9s^0#nGoKFIam;JAMF@H5 zB2ikXO6e++7rHf-g^iqwTCAYfhWrTxCpCvByNqgutBBL}#~G?}Ixy?}ew1TAFbY-y zDmrjs@20*%`sc6FzuqHN%6y`1=mwdD8e_j)wo{_BANc&4=ngI#+BCPzE=h7G7i#KoRQk z!-z!3cta<*>z;%^8AXCZHYuhMKA7I}BommLW{&@tK=!6k31&Hj=MyuwN`=-UxqLY6&UWh4aJ5H%B%hoPt!U z%rZSCDHWa_^69V(HQ;z$#_BJaAaw}GTTvE8iLfBF4x6TML__Qw*%XL_Q9xL540K^E z-99ylW{l6=Mc9f+2RPSpM0z{bRn@txx=>KsSV_HOGk8@2CaFvVf32wM_$e#+qWzD4 z05`tR(7$y{`lShfobLZ8R!qd?gN?0%o$il+{^QWHSNYqUNjbzv84ai$UfI@9F@~nN%X#$Qb_?_KRW}{x(Z)~WxA!G ze)^l)W4RxxHf?3d6)l5f^leesSQq%21`c)biK6zWn4a{B>JRSXtcq=PFg%M|{&hX? z6&T2rvfmljDF;WwO@!uAiZIe)z`&@5vEO2{zX(`AH$!} zgBD|~nQw7Jqm=G){*tU7v+_BrrDT&BYf`ROvqf`ad*2C*^=36L3|CeY3*U`nsfs*b zLO>G3V7jf<90lVz5*Y5A(dk-hcZgc=nlr|xWGI~voR|6tLm-yh-~d$&Re3@0r&` zjHy=3m4gZuDU=8>H4c5<%v3MMLXa54b~D9Jobh6Wl1xx`H&U#;gi_7s$*fSU10<~I zWo9d{MN0}lSKLUVda^@Zp1cb6{E-4&+8&zra0~mzqsm?vIlnN*!=}a#PlxO4*JFV; z-M!m5!{1ovr6-rA6=D2ecca1iIjlX3xnRtjm~U~`1#(`3X~*$He&Q4Z$hJp>c=nF# z6JvFwH4TeActSV^l6j}E-h#+pQ}1bBd)?kVrXK?`w7iJQBO0(DZ>WZ}PFy(XY{rF& z5!+i)VLiZYSl{;C=TQFwTT^WSHb*^}n&CF?#RaV?%GTJ~h||+Zu|omU09_xEeU7dr zrC@xY^R9R3gtX_XnD!!)x5n>i9gC{Nq2OZ}2IXx!0(W#DfQ*#+IiIt@Swj6x|T}r$Vz6_#J3Xhj zCGejfC1pPZG-m3Wj^ltA7A<`+y>E=f!))%@^VXlB_tgOh%{NuS4LFeegnb(|ZG~42 zLnc#PmQ2Q$pmveaKf65BFe)7A$*fY}m|Sf}SKoc<7_N55@x(|oAfP~nZ|VpZ&5hk9 z?o>9;$TQCDhQyot6*G)2FuLF0J>>t!`2i$)Co2vb;XMnsD%xL4qTeaWUm6ji-Dg z^3k3z&k-uuF?3*tDC;fK-F|lY7}W|9#Y+*6mo<|h+-15`+XUUUk;PaaXwlk<+&!1| zNb0Kgh@yEz-Xd}dAYi!>_$!dF?I2Ac-FsgaYFQ^Rdz%(2W~X2G7Yg8`;7!NRPxn*e zpA>(yjec5>zW7M92AR(ly`l4AEE2bui~&vEQ3T0EC5GfobSG@p&VhO>l8obJn;Zeo z>8B-TLsu+YBf-8m6x2B9tI^(|1(gW;_j>VtH!s4z)OB(Y#6CTXW7crANcadaNc1E9 z)_3M_qu@tGfyx&T92K-jk%fT?p-QFx@Fg;(N%lAc4u6s{$HI^eGHKieN^5Z|br_WQ z#Ilh_AIArRG$bT5$NX}0!IR?Lm_odAn4!sd%s>+XSvb)|PVYCs8YMpgX+!J}VQfoSt)wFz3y;@#`E&_szN`iVvYJu_xJ4a>rGYyNwp#^3!#=?}6bLK(dDq z`c5--w%G%zg&}*}gA9pN zr!et3BXv8ro-Q)>gSN4jXMs^@%1r%06CR4hp;T8BY&dtQoI8G9VyLj$*_R6B^CB{r zHc3*w6H>xN#Y_^fK6qMq-6p&Cg^@%#)J`i-9cyQJ*q*;dg!seZQ? zB)_r0wgruiT%uYE?LHsb|Kdm{#-&TQ6AQFN=DPRUFzoV@(4WMXd7?tIOx~KE&*S{? zFsLRJ%s@}lNVspkPL2Bd((J7q@<&M}fOC3SS56^pmQM_UEu->Dq=8BS)M2rGqp9SM zDW_FVQ9MkNSZRq$_^z^_`UFSRmyBZ&YFpdECa=djnu1t$K(-0i_vhouGvL$3jguDn zQH1z7Y;o_&o2%@=xpd+r=3Bb!*;`xn(erCaOTS(jiB?qkYOV%IgAhQ~Vd363!V3X{ zJDnkG(OB+w{2Qaolw=?Dzs%M&iUTc733T#vXU7%Ilbu&NOc3hm&1DbeVt$oUlS`k?%IBtF8KFy#u;Ser3^VVkZh5^ZSpqc_^Ux5_A) zf{%mn>+IF|YqS87a&Jm0>I9&h2Nq?6dy!6ZhWyp~Ydle>rOSRw*a z%VZOm(%)3?}Yna4K(Dfaw?;>1mvUM;};KqWn~~IN27kU ztyIKL$~*il$#(QX^~3E@qy}VaDIPbKFfItanT(NJ@E9QgOjq+#rn zcUE;+El%WPK6Buc>WV=OsAF}q?(_qS}1nrFl;F(h7>pEHk@Q=)*LhGwW;K-Bnc3XRZPh}ghV6J@erVWH2sc=%D(J8 z-V)9Ru$FxPK}2%%I+W;hwG!<6@G;6))Uh+F+#NZ>@9v(mIh#KLv&tZ1%Y6KhqL z4Pe{*TH9}qv@0LBIHZpFGIVVNQ@GkQeZ{*kUEwc`;T?8h{OZCGbVnbeRZ)pUx9;BJ z3#Uycp3QBY0{lg6O(&GdKYu(#^qAqXKY?_eJsi<8`WnUYqdk6Ce`+}qKXCXu=Bii( zl5iLFXPA}4ksR4|afuXM*JuG(Z)IN@kEWNG)XzJDxEv%q!f|NvMx}WAW8EZfDR%ZL zs4^dnl32*N(ZP%=AtL~}cqT+}8DDlOqz_Hzs77LKf0~pM2;AvE0W*P}MWU%0xduzm z?cv`?a`omS<~MJAOdd!<+h@Fm6i~e|kz$AGN)^TJ=UfDBLopw^!dxRsxdWPWq~9me zkzEGA6zAUD+>>qt2XzBmP?#lJs(oRS{;84)za`jM-wx0S-No)OoMKr8`4(>~14#CB z%UjP?6Go6~l3zgkWEn8IFJ0aop06pCT-`RIaiw3HJiw7^+iTyWW%>Gr8w+wavk7gh z=bw3Wk1XsYN*;x-XqFJ)bcaVz5H zS;AEUwrv7vR+czxols#JX%>D4juIJP+srd)Ri~v0&ioCc2?natg2S zI7_*syFNTvr*NC2%mZ_2SC$fN8x#!BCKe=xv^nwk>pSNg0cgAe!0z@60tkrv@4MSy z_O(CWIi*c(Yyj#zf9!JkKN$TIKbRV;B|G^JAXGBS+En^YKZCH15#wVkpy0W9LP;=9jAC-lAdZJncn>u=OORVrzLy9D%C`vT(jQ%Fn?>a98Nv0(As@ z7BNIshDsFa{3Z=;Dho3Q`K}8&F>6(|-NVurIByVMygY>UnZR?>XAdVTdVN}_1z)7T zpC&9SOGT=*VOeiCVK(a_Dqpo3e~(6&q^TH!QzmUO*huS7Uti4F{*n$WI?kBi%#P32 z^K$mVmvn~K8k1_Aab;Sj(E(u+o6sPLET%b2pw`GHSzgl!sQawVe|ahYE3L@RaA->` zCRzAKSB8bE%VoIxXk-YZbC03=rC>MVn^V?u{$6N!Sj@?aKpgCmwHANUP*MJpo5#yQ zouYVjrlHGaK7XkzdwMB+J$CnOJ%5`9!6LR}Eroa;!zv!S(7jsS@;umhqNQOhlUvVT zA{7%=86OgGA`T)>EaHRLCPdC;r~5#eu5097cRBt8xcc~HqYjwwjxwtd*u{;q+c;#R zz+|IUMP^Mx!N+oeioU|y3AM2}!hC(>VYW^IAMVY>{>|E05@bX$SQd*g@yW{y57e)a z^W%|ugDn250~}VXPeA0Pu{nsJFzE3;Eg_)$NE1GU)(P^mZDLyTZG_=YHlSvpbMx@Y zhmSti%f=q=!|LuA2TZyO62yZ@>kxY`5)D?YRDf1I)ah%pDa|cb3~F!W+JeVu3#*MX ziL!uV2^q7K8YA7HQ0su5VkmbBPcWSeQC&U@wi6sj-`WFMU0eadVE<|a2pAYzelYkS zW*2h>6cto=iz+MYYGX0@*t$6}A3kj;{$f5vSfohgAoGu`bM?}ylhgXMU(AX;GI=wf zx<+bz;tDS{jBazEvU#&6Z5;HV$SOLm^WGi~HjeUaH;$&cTs~eLS<(UJSikL48%oVG z)E2?C!NtNtMmL~YRR@x67&!?_ps^`|nuEt;ROXn0s2H(U=!1A2%Md6Xo)U^!$9{@i zk9B)+bm464uR~*`9;?{<#-UifNOM|2ty)gDgjlFvx64)y|Nuqg=mngb`D(Ea%*Ddvg{wh6v8nOM1w(0>hI zQmrwLDNp^h2zmj@qNEfiQS?ruN&b_qdR|MR!!_9p_vs?2UC#UQdL}&Xpsz0$!IQQS zq!n5rEI9B@W2ja_tufZ-5UB&eLKWw>^?To_*_)fgLMWwq|>MZy|(Qz;|` z;^H25NWnIurE~{+>$D772M{?CWA$z|!_=LC_s7Eo&*=m@iWjWnx9KKah1$+K8Ye`A zz53*MSU?JnK~JXYHi*c}*-@orGR54j?e=CSDg9Ak(jX@h{S(z1vBiimS>HF5Nw`j* z`=NMzV}s-kE^sOdV%@~GqU|Ww2=+0nEAX}Wz3y1HV#rp+4}hganf8D_GaKDd*1dCA z+bREBWzdE7GY==5VEnf?2v<&@!=@`PO!wk$#ZX5DA*l4xh7B9$ zWG06{s<^@lSEgSm~_)dz&@_8#e!9%|zA446r-P;j5DmdOEx^n%OtE9VG9Hj~4A z?Vw%{KRR^T{mv(*VK>M0`AOX{EkObe0Z*-dNPV0q& zqOElZg_yDxJm==U?p{Y&wq$ir%d@0(!lzT|Ho{q1K zi152%8-yF^FVe+dyGlQ>Mc)(rbrq#WGxmlAtfI|;pi;(vvx@$)nEtYM#wx5>PROA? z%21-h+H@=`GNZ8J(zEWlGGR)SBqcAqGg6kR7pF}n)Ov|wHBb~ z>Kr32vFF*v*ABfayQR2&?IN-Cj_@8OliOT2#n5)k&v&s>F0aAvkgix z9zkP?FDxYrS#ZS;qHWvO`$Tmhvn=wfQvSq(Zt4pTN6Xf?VP7;$Kp2^ynyC~B`1f$9oL3bI@H<$RmqA$D#z;Knis}40jlY@Wq^F}4J)uZfxODnqHyrV_UWX+n#92@vz^ZvG zd7!XfYDmMnIPGT}X9~}0TK8bBfVHe7UeKdEx~$lQ=iVTi(u#&}>@ zd|fiGFQ+u|>nUpbIZHZoS_X1HPuNDiRlETf>Ikasm=W)>rNE@nqP(RN%cSyX|uFRh!e%g5~mg|0vxG5bDWL z2jUhyrs;TPhTgcRKWS|OXPi=!nS6h6xRSpY?e4(4WTLoq-gjf4EtYgK3&r}Kr@712 zmvQ84DH+wR-*>%6OjEq0C;$(bFcLdT1)H$D1eN6st8#HA=0*Q|8gv}R5z4FE8T?;n_rgHik@ca)VLfg0q0D+C_mZgx+#_dGl?$KZT3=fDe6Xx zbHjf+IM@v%BSxDD>0WRa7OH>wVYx)}eT@oFl_`HAR~Gik)u4)t5JxOg?kpx^+_$@% z-smRZ#U~&v|4V~zBz}dUstdZJVZQk-0@C^>zq!0OeQgWprZcdG7A#BC7UFmkgkJg} zvXy0eK5I^I!wuFnn!_zQ+TgwxPV?YTx*-;AWP_1K8j5Lc4_8=sn>4*Cp+)a@q^rfm zzifw2!d&=(-Klf!Z;Wkto7&09Bj6*lj?SY7uL&*LtTUgeZ=#O)wgK!(>P0_PDJa!| zf>>0#F{l;JhMbM?sZanfk%;xXd@HYnOW>wUBy}^FlcS}kI36E{%+!}78eeQS(}>Dy z+XP$&Um?*UI%iCqxTZe}c8D?z^+#@QBy6G#oQb2}pej3&k<`psU?aU*Cdm_s=4uec zC;w19u=;9zkT}5Qt4SdqaaveldyZ77*@w(yyj$XjSM+mxHy2Fi@AB26DP#hMqdz7q z)3dP1uSr*NN5@>mF)cyZ!t@os3qNJxesNNr0Beq~r_Y2+AOM{Gm4`57ig4*2ux#Yb zGaZY%jtZLuKwmEcyomgN^mTp*oew|ML4TP|OO*hspz?@3^#F|y`%1J-_f<$;q$X4t zmVta2WCjxWq$%c3kNN|3jQZxS!Of(H3ug%f2RoEk>CZ1&Q-Q9&& zjt#D5T^VLgcBZVPpj@Sf)2OnJ(|H4OpGvKpyZlHW2nz=6u^1z?Z{xn1wQLc~QQs}* zx3nFw=grkLZYTO`yf|Q7NDq2^-}GrkS^Cu1BTxr3k?PmqpYJ@yV+D`5o3b8f|ac0f@31z;N~X4PFOyWcrjnw zt1uEt^h9S=xQ-8sN!G2x#kHcn)Ub&MZ%<=3iv{7-`nVcK=jgXUdqy{?xSP8tpPzW9 zK78J?--WBS7=)rJ+?Yf6I2g;b`^D_4AT{khH*S9DIL@1V2P3fV54~Qt>aTc&v*e5; z{Ee%&2dI&VC0Na+j(o88sfk703C3mlX$#^`X%NDO340A01S;V+3;g$mGag7-hp=($=W@K+<^NYUG7Cn6iXe_~P6I_wg7Z z~A z_lJ2sYFGl40o70TmMOcoPTib(uj{Zz334{pJJ6fHp(6Lr7CSF0QHYUg?|max!!fWc z1%tRG8m+h5;@7mryCsy<$CF!#Rk&efm^ntB1>q%OpAL>~?|dHb+e{Bxf=B?mtIS-2gXD+K;1@2e-^mvI)Mg0$~G+&(DuS+A}^2-fKV)^L+=2n6qd+SO2+1JmagmK@aR}KSOWC1V{vHfGK{IH1pvFHLUC04%*o5d>q zN43@6Vs>1f6Vp=R7+eSr$I`%)5DqCt7afB>k?2GZYnWzNeZsyceGOqP4mEt|L5@c}xHNR0Uy+lx6_2l0px95LsJ7H7aWT z4pm!Rb>r9$S{ND!S)0k$95ZgsoRQ?ijT=OzKEWQG8(Q~-Lvk4C(oG3oLJn5J9LwCk3a;4Fn@+S-2_u*S#UiiGJqgkR%O!~Ixt6DTCqgBc^pIKPUGAReW zFjgXnzZJ99(8S~vlRZE( z$<$O?tJFDMXF9|yj8LJGbz`|RO8YE0726$VZK2DqXsnMG99r?kwQZ#uHLBAz*%65T zP8qhHg?Jic{Z8;pyiP}7gX`?AyYEqltaovEuZ~FEW4C9WbI5hV|Bt!)%u+#L($^wOO_@bGx0}p%NX(WiJ68D5{&3MW(zCIQ9Vp z_BlUXm$3IEijD>H)Zqq8=cAI}xlbL4t|=QzA+iQOf#niCC2x8~bTDFHFw+a0bKT}* zA;j(DIv+d}0*1jrF_sF}8PSAGA0Xd&4i)hbQ?d#zzIeuwrW@*-zoWZB5|n7pxm0~@ zOYljcMND>7Gs`G0jfo;ZN(C3ev-F*b6e7o2u6VRB+%=|O&-)q}oe@>rkt*N};RKQ{ z+%G5tch@FE*WYOH$2j?&q!cKtK7PTdA&wyqli;nAzvVAf?cI5HA|5$#9>)zBAqs$P z>2Kz(Kjm|Nu^m2hIf_;ML-Y?6HiWt^(7beI3}G|dcHwoRfEd)cu|N^hL=WIlZr1!= zJe***N7qUg9rJh(!D~f=VrMuo z-e~NVfD;Fv6COC8u)kyPg{GQ8;MQqAochv{mH)K=x04v1@?qgYQwL~qHrIu{H1#f} zJLrN>qlccEQ^$UKoLzgijV2CS)id~FI$x>|ubks%<@N1WxV8m8$Q#Zz3|-Vdr628( z3B@WGYm%}q;lG6IZ|qC7vq;n$v>B>L%~Bm?nrptqsyE;u@D~dq#hs|C3-m4%pLqXj znulf}4B+3g>aFu$yJM(x1(E@^b?0L&RtcrwHx92l3KM;LZMRCNw@qzqW0n0@#k-to zqygS#`|nIxvpz%G5>S zzD1M0=EfioUhEj0qeDi+D4(nh2|M1JE76461{1 z800%_>Kd|sU{&GZ{Uu4UB%17zQu(y3I}zoV%{Q=75g37Ks1h%Y{oYvn&MiZo98JPd z>+?H7mrT09EKtPUmZ74wOkJ+%XqJ`r%~Ejm4ER_)5(Ul9O1_QR&zsc8n|DqA^}R25 zZ#AE@e_CaHukyS4r)QHj!c(^jDqxZZs5$@<06ssY_W`nZ|17(|`K#DIFbFCrAXgw1 z6i^Ys-vfa1Uk!VJBi4@#5$J&b`%h_MMSfaw84-Xu=3k4ktJ(Q~{~yYL&-WVrdF6P0 zSMd68F=>7oaS>q!#aGfIzanHjBL>(Jeq7&)Qvkm$KN$Um^LYt?_41bz9f058OZ*Vu z|0gOygu##NJJq+p7W|RJ;BU?1!G?>0X;q z?*Nu@>=%DHE&K_Yl?_bcFc*T(0W zWqtz6>HY6KGtW`Jm-^ltzh8jS_~ZJ%nA`zSerfXWVzkc-KTiSkXYm{_D*e9|{$)q` zmv^r}44!}QLf`N4c><1~1n@@xg5dAzIG*3Z^K1t{34l%iiQrd1(dU@YwYGm^&RYHx z=8vcHuavj{#`#P!`6s8}hkwEOo4M-y$CTt=djQ9NPog+IsOgy&)2JeBkB8`=(&F2 zPa+MszY+aX{~!J8pVb7PlRcMP`$?AQ`G1rBel$Oii~Wfy6!U*$lKfMY?DOhB53u=3 zWtjXos^@!`U!L9n(e}@SK7P`0r2dWO7uX+7^1nks{&qjl{MdgIjb{9f=s)QH%d7o4 z@^jzdpUBgIeb4jJ_dS!>ugJe|=;wY(KQT*-|AzVZ$MO$eO3!Qm-2dYzQ+e6HVEPAd zkmpR#PlS+Hz47-s$#aX@Pm;Cvzmfbo!T-{$|BpH3xfSFm iyjj \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/resources/compass.svg b/resources/compass.svg new file mode 100755 index 000000000..fd27e73f2 --- /dev/null +++ b/resources/compass.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/direction-dot.svg b/resources/direction-dot.svg new file mode 100755 index 000000000..14ce0bf7f --- /dev/null +++ b/resources/direction-dot.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/dot-arrow.svg b/resources/dot-arrow.svg new file mode 100644 index 000000000..4c2841a54 --- /dev/null +++ b/resources/dot-arrow.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/resources/location-dot.svg b/resources/location-dot.svg new file mode 100755 index 000000000..24addea3c --- /dev/null +++ b/resources/location-dot.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/resources/location-static-dot.svg b/resources/location-static-dot.svg new file mode 100755 index 000000000..18269bcdf --- /dev/null +++ b/resources/location-static-dot.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/scripts/exclude-activity-gen.json b/scripts/exclude-activity-gen.json new file mode 100644 index 000000000..0f5bb81b5 --- /dev/null +++ b/scripts/exclude-activity-gen.json @@ -0,0 +1,56 @@ +[ + "BaseLocationActivity", + "MapSnapshotterMarkerActivity", + "MapSnapshotterReuseActivity", + "MapSnapshotterLocalStyleActivity", + "LatLngBoundsActivity", + "BottomSheetActivity", + "MapSnapshotterActivity", + "MockLocationEngine", + "DeleteRegionActivity", + "RealTimeGeoJsonActivity", + "UpdateMetadataActivity", + "CarDrivingActivity", + "MyLocationTrackingModeActivity", + "MyLocationToggleActivity", + "MyLocationTintActivity", + "MyLocationDrawableActivity", + "DoubleMapActivity", + "LocationPickerActivity", + "GeoJsonClusteringActivity", + "RuntimeStyleTestActivity", + "AnimatedSymbolLayerActivity", + "ViewPagerActivity", + "MapFragmentActivity", + "SupportMapFragmentActivity", + "SnapshotActivity", + "NavigationDrawerActivity", + "QueryRenderedFeaturesBoxHighlightActivity", + "MultiMapActivity", + "MapInDialogActivity", + "ManualZoomActivity", + "MaxMinZoomActivity", + "ScrollByActivity", + "ZoomFunctionSymbolLayerActivity", + "SymbolGeneratorActivity", + "TextureViewTransparentBackgroundActivity", + "SimpleMapActivity", + "RenderTestActivity", + "SymbolLayerActivity", + "LocationFragmentActivity", + "MergeOfflineRegionsActivity", + "NestedViewPagerActivity", + "DebugModeActivity", + "TextureViewDebugModeActivity", + "RuntimeStyleActivity", + "GridSourceActivity", + "AnimatedImageSourceActivity", + "EspressoTestActivity", + "ChangeResourcesCachePathActivity", + "EspressoTestActivity", + "FragmentBackStackActivity", + "ChildFragmentMapInDialogActivity", + "PerformanceMeasurementActivity", + "DownloadRegionActivity", + "CacheManagementActivity" +] diff --git a/scripts/generate-license.py b/scripts/generate-license.py new file mode 100644 index 000000000..9e59cb418 --- /dev/null +++ b/scripts/generate-license.py @@ -0,0 +1,39 @@ +#!/usr/bin/python + +import os +import json + +path = os.getcwd() + "/platform/android/" +with open(path + "LICENSE.md", 'w') as licenseFile: + licenseFile.write(" \n") + licenseFile.write("## Additional Mapbox GL licenses \n") + with open(path + "MapboxGLAndroidSDK/build/reports/licenses/licenseReleaseReport.json", 'r') as dataFile: + data = json.load(dataFile) + + gradleLicensePlugin =""" + { + "project": "Gradle License Plugin", + "url": "https://github.com/jaredsburrows/gradle-license-plugin", + "licenses": [ + { + "license": "The Apache Software License, Version 2.0", + "license_url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + ] + } + """ + data.append(json.loads(gradleLicensePlugin)) + + licenseName = "" + licenseUrl = "" + for entry in data: + projectName = entry["project"] + projectUrl = entry["url"] + for license in entry["licenses"]: + licenseName = license["license"] + licenseUrl = license["license_url"] + + licenseFile.write("Mapbox GL uses portions of the %s. \n" % projectName + + ("URL: [%s](%s) \n" % (projectUrl, projectUrl) if projectUrl is not None else "") + + "License: [%s](%s)" % (licenseName, licenseUrl) + + "\n\n===========================================================================\n\n") diff --git a/scripts/generate-style-code.js b/scripts/generate-style-code.js new file mode 100755 index 000000000..fa814f89a --- /dev/null +++ b/scripts/generate-style-code.js @@ -0,0 +1,394 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const ejs = require('ejs'); +const spec = require('../../../scripts/style-spec'); +const _ = require('lodash'); + +// FIXME: https://github.com/mapbox/mapbox-gl-native/issues/15008 +delete spec.layout_circle["circle-sort-key"] +delete spec.layout_line["line-sort-key"] +delete spec.layout_fill["fill-sort-key"] + +require('../../../scripts/style-code'); + +// Specification parsing // +const lightProperties = Object.keys(spec[`light`]).reduce((memo, name) => { + var property = spec[`light`][name]; + property.name = name; + property['light-property'] = true; + property.doc = property.doc.replace(/°/g,'°'); + memo.push(property); + return memo; +}, []); + +// Collect layer types from spec +var layers = Object.keys(spec.layer.type.values).map((type) => { + const layoutProperties = Object.keys(spec[`layout_${type}`]).reduce((memo, name) => { + if (name !== 'visibility') { + spec[`layout_${type}`][name].name = name; + memo.push(spec[`layout_${type}`][name]); + } + return memo; + }, []); + + const paintProperties = Object.keys(spec[`paint_${type}`]).reduce((memo, name) => { + spec[`paint_${type}`][name].name = name; + memo.push(spec[`paint_${type}`][name]); + return memo; + }, []); + + return { + type: type, + doc: spec.layer.type.values[type].doc, + layoutProperties: layoutProperties, + paintProperties: paintProperties, + properties: layoutProperties.concat(paintProperties) + }; +}); + +// Process all layer properties +const uniqueArrayEnum = (prop, enums) => { + if (prop.value !== 'enum') return false; + const enumsEqual = (val1, val2) => val1.length === val1.length && val1.every((val, i) => val === val2[i]); + return enums.filter(e => enumsEqual(Object.keys(prop.values).sort(), Object.keys(e.values).sort())).length == 0; +}; + +const layoutProperties = _(layers).map('layoutProperties').flatten().value(); +const paintProperties = _(layers).map('paintProperties').flatten().value(); +const allProperties = _(layoutProperties).union(paintProperties).union(lightProperties).value(); +let allEnumProperties = _(allProperties).filter({'type': 'enum'}).value(); +const uniqueArrayEnumProperties = _(allProperties).filter({'type': 'array'}).filter(prop => uniqueArrayEnum(prop, allEnumProperties)).value(); +const enumProperties = _(allEnumProperties).union(uniqueArrayEnumProperties).value(); + + +global.propertyType = function propertyType(property) { + switch (property.type) { + case 'boolean': + return 'Boolean'; + case 'number': + return 'Float'; + case 'formatted': + return 'Formatted'; + case 'string': + return 'String'; + case 'enum': + return 'String'; + case 'color': + return 'String'; + case 'array': + return `${propertyType({type:property.value})}[]`; + default: + throw new Error(`unknown type for ${property.name}`); + } +} + +global.propertyJavaType = function propertyType(property) { + switch (property.type) { + case 'boolean': + return 'boolean'; + case 'number': + return 'float'; + case 'formatted': + return 'Formatted'; + case 'string': + return 'String'; + case 'enum': + return 'String'; + case 'color': + return 'String'; + case 'array': + return `${propertyJavaType({type:property.value})}[]`; + default: + throw new Error(`unknown type for ${property.name}`); + } + } + +global.propertyJNIType = function propertyJNIType(property) { + switch (property.type) { + case 'boolean': + return 'jboolean'; + case 'number': + return 'jfloat'; + case 'String': + return 'String'; + case 'enum': + return 'String'; + case 'color': + return 'String'; + case 'array': + return `jarray<${propertyType({type:property.value})}[]>`; + default: + return 'jobject*'; + } +} + +global.propertyNativeType = function (property) { + if (/-translate-anchor$/.test(property.name)) { + return 'TranslateAnchorType'; + } + if (/-(rotation|pitch|illumination)-alignment$/.test(property.name)) { + return 'AlignmentType'; + } + if (/^(text|icon)-anchor$/.test(property.name)) { + return 'SymbolAnchorType'; + } + switch (property.type) { + case 'boolean': + return 'bool'; + case 'number': + return 'float'; + case 'formatted': + case 'string': + return 'std::string'; + case 'enum': + if(property['light-property']){ + return `Light${camelize(property.name)}Type`; + } + return `${camelize(property.name)}Type`; + case 'color': + return `Color`; + case 'array': + if (property.length) { + return `std::array<${propertyType({type: property.value})}, ${property.length}>`; + } else { + return `std::vector<${propertyType({type: property.value})}>`; + } + default: throw new Error(`unknown type for ${property.name}`) + } +} + +global.propertyTypeAnnotation = function propertyTypeAnnotation(property) { + switch (property.type) { + case 'enum': + return `@Property.${snakeCaseUpper(property.name)}`; + default: + return ""; + } +}; + +global.defaultExpressionJava = function(property) { + switch (property.type) { + case 'boolean': + return 'boolean'; + case 'number': + return 'number'; + case 'formatted': + return 'format'; + case 'string': + return "string"; + case 'enum': + return "string"; + case 'color': + return 'toColor'; + case 'array': + return "array"; + default: return "string"; + } +} + +global.defaultValueJava = function(property) { + if(property.name.endsWith("-pattern")) { + return '"pedestrian-polygon"'; + } + if(property.name.endsWith("-font")) { + return 'new String[]{"Open Sans Regular", "Arial Unicode MS Regular"}'; + } + switch (property.type) { + case 'boolean': + return 'true'; + case 'number': + return '0.3f'; + case 'formatted': + return 'new Formatted(new FormattedSection("default"))' + case 'string': + return '"' + property['default'] + '"'; + case 'enum': + return snakeCaseUpper(property.name) + "_" + snakeCaseUpper(Object.keys(property.values)[0]); + case 'color': + return '"rgba(255,128,0,0.7)"'; + case 'array': + switch (property.value) { + case 'string': + case 'enum': + if (property['default'] !== undefined) { + return '[' + property['default'] + ']'; + } else { + return 'new String[0]'; + } + case 'number': + var result ='new Float[] {'; + for (var i = 0; i < property.length; i++) { + result += "0f"; + if (i +1 != property.length) { + result += ", "; + } + } + return result + "}"; + } + default: throw new Error(`unknown type for ${property.name}`) + } +} + +/** + * Produces documentation for property factory methods + */ +global.propertyFactoryMethodDoc = function (property) { + var replaceIfPixels = function (doc) { + return doc.replace('pixels', 'density-independent pixels') + } + let doc = replaceIfPixels(property.doc); + // Match other items in back ticks + doc = doc.replace(/`(.+?)`/g, function (m, symbol, offset, str) { + if (str.substr(offset - 4, 3) !== 'CSS' && symbol[0].toUpperCase() != symbol[0] && _(enumProperties).filter({'name': symbol}).value().length > 0) { + // Property 'enums' + symbol = snakeCaseUpper(symbol); + return '{@link Property.' + symbol + '}'; + } else if( _(allProperties).filter({'name': symbol}).value().length > 0) { + // Other properties + return '{@link PropertyFactory#' + camelizeWithLeadingLowercase(symbol) + '}'; + } else { + // Left overs + return '`' + symbol + '`'; + } + }); + return doc; +}; + +/** + * Produces documentation for property value constants + */ +global.propertyValueDoc = function (property, value) { + + // Match references to other property names & values. + // Requires the format 'When `foo` is set to `bar`,'. + let doc = property.values[value].doc.replace(/When `(.+?)` is set to `(.+?)`(?: or `([^`]+?)`)?,/g, function (m, peerPropertyName, propertyValue, secondPropertyValue, offset, str) { + let otherProperty = snakeCaseUpper(peerPropertyName); + let otherValue = snakeCaseUpper(peerPropertyName) + '_' + snakeCaseUpper(propertyValue); + const firstPropertyValue = 'When {@link ' + `${otherProperty}` + '} is set to {@link Property#' + `${otherValue}` + '}'; + if (secondPropertyValue) { + return firstPropertyValue + ` or {@link Property#${snakeCaseUpper(peerPropertyName) + '_' + snakeCaseUpper(secondPropertyValue)}},`; + } else { + return firstPropertyValue + ','; + } + }); + + // Match references to our own property values. + // Requires the format 'is equivalent to `bar`'. + doc = doc.replace(/is equivalent to `(.+?)`/g, function(m, propertyValue, offset, str) { + propertyValue = snakeCaseUpper(property.name) + '_' + snakeCaseUpper(propertyValue); + return 'is equivalent to {@link Property#' + propertyValue + '}'; + }); + + // Match other items in back ticks + doc = doc.replace(/`(.+?)`/g, function (m, symbol, offset, str) { + if ('values' in property && Object.keys(property.values).indexOf(symbol) !== -1) { + // Property values + propertyValue = snakeCaseUpper(property.name) + '_' + snakeCaseUpper(symbol); + console.log("Transforming", symbol, propertyValue); + return '{@link Property#' + `${propertyValue}` + '}'; + } else if (str.substr(offset - 4, 3) !== 'CSS' && symbol[0].toUpperCase() != symbol[0]) { + // Property 'enums' + if (symbol === 'symbol-sort-key') { + return 'symbol sort key'; + } + + symbol = snakeCaseUpper(symbol); + return '{@link ' + symbol + '}'; + } else { + // Left overs + return symbol + } + }); + return doc; +}; + +global.isLightProperty = function (property) { + return property['light-property'] === true; +}; + +global.propertyValueType = function (property) { + switch (property['property-type']) { + default: + return `PropertyValue<${evaluatedType(property)}>`; + } +}; + +global.evaluatedType = function (property) { + if (/-translate-anchor$/.test(property.name)) { + return 'TranslateAnchorType'; + } + if (/-(rotation|pitch|illumination)-alignment$/.test(property.name)) { + return 'AlignmentType'; + } + if (/^(text|icon)-anchor$/.test(property.name)) { + return 'SymbolAnchorType'; + } + if (/position/.test(property.name)) { + return 'Position'; + } + switch (property.type) { + case 'boolean': + return 'bool'; + case 'number': + return 'float'; + case 'formatted': + case 'string': + return 'std::string'; + case 'enum': + return (isLightProperty(property) ? 'Light' : '') + `${camelize(property.name)}Type`; + case 'color': + return `Color`; + case 'array': + if (property.length) { + return `std::array<${evaluatedType({type: property.value})}, ${property.length}>`; + } else { + return `std::vector<${evaluatedType({type: property.value})}>`; + } + default: throw new Error(`unknown type for ${property.name}`) + } +}; + +global.supportsZoomFunction = function (property) { + return property.expression && property.expression.parameters.indexOf('zoom') > -1; +}; + +global.supportsPropertyFunction = function (property) { + return property['property-type'] === 'data-driven' || property['property-type'] === 'cross-faded-data-driven'; +}; + +// Template processing // + +// Java + JNI Light (Peer model) +const lightJava = ejs.compile(fs.readFileSync('platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/light/light.java.ejs', 'utf8'), {strict: true}); +const lightJavaUnitTests = ejs.compile(fs.readFileSync('platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/light.junit.ejs', 'utf8'), {strict: true}); +writeIfModified(`platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/light/Light.java`, lightJava({properties: lightProperties})); +writeIfModified(`platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/LightTest.java`, lightJavaUnitTests({properties: lightProperties})); + +// Java + JNI Layers (Peer model) +const layerHpp = ejs.compile(fs.readFileSync('platform/android/src/style/layers/layer.hpp.ejs', 'utf8'), {strict: true}); +const layerCpp = ejs.compile(fs.readFileSync('platform/android/src/style/layers/layer.cpp.ejs', 'utf8'), {strict: true}); +const layerJava = ejs.compile(fs.readFileSync('platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/layer.java.ejs', 'utf8'), {strict: true}); +const layerJavaUnitTests = ejs.compile(fs.readFileSync('platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/layer.junit.ejs', 'utf8'), {strict: true}); + +for (const layer of layers) { + writeIfModified(`platform/android/src/style/layers/${layer.type.replace('-', '_')}_layer.hpp`, layerHpp(layer)); + writeIfModified(`platform/android/src/style/layers/${layer.type.replace('-', '_')}_layer.cpp`, layerCpp(layer)); + writeIfModified(`platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/${camelize(layer.type)}Layer.java`, layerJava(layer)); + writeIfModified(`platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/${camelize(layer.type)}LayerTest.java`, layerJavaUnitTests(layer)); +} + +// Java PropertyFactory +const propertiesTemplate = ejs.compile(fs.readFileSync('platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/property_factory.java.ejs', 'utf8'), {strict: true}); +writeIfModified( + `platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/PropertyFactory.java`, + propertiesTemplate({layoutProperties: layoutProperties, paintProperties: paintProperties}) +); + +// Java Property +const enumPropertyJavaTemplate = ejs.compile(fs.readFileSync('platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/property.java.ejs', 'utf8'), {strict: true}); +writeIfModified( + `platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/Property.java`, + enumPropertyJavaTemplate({properties: enumProperties}) +); diff --git a/scripts/generate-test-code.js b/scripts/generate-test-code.js new file mode 100644 index 000000000..64b38e5ac --- /dev/null +++ b/scripts/generate-test-code.js @@ -0,0 +1,73 @@ +'use strict'; + +const fs = require('fs'); +const ejs = require('ejs'); + +global.iff = function (condition, val) { + return condition() ? val : ""; +} + +global.camelize = function (str) { + return str.replace(/(?:^|-)(.)/g, function (_, x) { + return x.toUpperCase(); + }); +} + +const excludeClasses = JSON.parse(fs.readFileSync('platform/android/scripts/exclude-activity-gen.json', 'utf8')); +const appBasePath = 'platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity'; +const testBasePath = 'platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/gen'; +const subPackages = fs.readdirSync(appBasePath); +const ejsConversionTask = ejs.compile(fs.readFileSync('platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/activity.junit.ejs', 'utf8'), {strict: true}); + +if (!fs.existsSync(testBasePath)){ + fs.mkdirSync(testBasePath); +} + +console.log("\nGenerating test activities:\n"); +var generatedClasses = []; +var excludedClasses = []; +for(const subPackage of subPackages) { + if(!(subPackage.slice(-5) == '.java')) { + const activities = fs.readdirSync(appBasePath+'/'+subPackage); + + // create directories for package + if (!fs.existsSync(testBasePath+"/"+subPackage)){ + fs.mkdirSync(testBasePath+"/"+subPackage); + } + + for (const activity of activities) { + var activityName; + if (activity.slice(-5) === '.java') { + // .java file + activityName = activity.slice(0, -5); + } else { + // .kt file + activityName = activity.slice(0, -3); + } + + // create path for test file + const filePath = testBasePath+"/"+subPackage+"/"+activityName+'Test.java'; + + // try removing previous generated files + try { + fs.accessSync(filePath, fs.F_OK); + fs.unlinkSync(filePath); + } catch (e) { + } + + // only generate test file if not part of exclude list + if contains Activity in name + if ((!(excludeClasses.indexOf(activityName) > -1)) && activityName.includes("Activity")) { + fs.writeFileSync(filePath, ejsConversionTask([activityName, subPackage])); + generatedClasses.push(activityName); + }else{ + excludedClasses.push(activityName); + } + } + } +} + +for(const generatedClass of generatedClasses){ + console.log(generatedClass+"Test"); +} + +console.log("\nFinished generating " + generatedClasses.length + " activity sanity tests, excluded " + excludeClasses.length + " classes.\n"); \ No newline at end of file diff --git a/scripts/metrics.sh b/scripts/metrics.sh new file mode 100755 index 000000000..cc33daf17 --- /dev/null +++ b/scripts/metrics.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e +set -o pipefail + +# Track individual architectures +scripts/check_binary_size.js "platform/android/MapboxGLAndroidSDK/build/intermediates/intermediate-jars/release/jni/armeabi-v7a/libmapbox-gl.so" "Android arm-v7" +scripts/check_binary_size.js "platform/android/MapboxGLAndroidSDK/build/intermediates/intermediate-jars/release/jni/arm64-v8a/libmapbox-gl.so" "Android arm-v8" +scripts/check_binary_size.js "platform/android/MapboxGLAndroidSDK/build/intermediates/intermediate-jars/release/jni/x86/libmapbox-gl.so" "Android x86" +scripts/check_binary_size.js "platform/android/MapboxGLAndroidSDK/build/intermediates/intermediate-jars/release/jni/x86_64/libmapbox-gl.so" "Android x86_64" + +# Track overall library size +scripts/check_binary_size.js "platform/android/MapboxGLAndroidSDK/build/outputs/aar/MapboxGLAndroidSDK-release.aar" "Android AAR" + +if [[ $CIRCLE_BRANCH == master ]]; then + # Build source data for http://mapbox.github.io/mapbox-gl-native/metrics/binary-size/ + # and log binary sizes to metrics warehouse + scripts/publish_binary_size.js +fi diff --git a/scripts/parse-jacoco-report.py b/scripts/parse-jacoco-report.py new file mode 100755 index 000000000..4d06fda6c --- /dev/null +++ b/scripts/parse-jacoco-report.py @@ -0,0 +1,31 @@ +#!/usr/bin/python + +import os +import re +from io import open + +reportPath = os.getcwd() + "/platform/android/MapboxGLAndroidSDK/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml" +with open(reportPath, 'r', encoding='utf-8') as jacocoReport: + line = jacocoReport.readline().strip() + + # find the last INSTRUCTION coverage report which is a sum of all separate ones + instructionIndex = line.rfind('type="INSTRUCTION"') + startIndex = line.find('missed', instructionIndex) + endIndex = line.find('/>', startIndex) + + # find the missed and covered lines counts + numbers = re.match(r'missed="(\d+)" covered="(\d+)"', line[startIndex:endIndex]) + missed = int(numbers.group(1)) + covered = int(numbers.group(2)) + + # calculate the code coverage percentage + percentage = round(covered * 100.0 / (missed + covered), 2) + print("Android tests code coverage: %s%%" % (str(percentage))) + + # invoke the script that send the data to s3 + testEnvironment = "LOCAL" + if os.environ.get('IS_LOCAL_DEVELOPMENT').lower()=='false': + testEnvironment = "CI" + + cmd = os.getcwd() + ("/scripts/code-coverage.sh %.2f %s %s" % (percentage, "Android", testEnvironment)) + os.system(cmd) diff --git a/scripts/release.py b/scripts/release.py new file mode 100644 index 000000000..d0abd1eea --- /dev/null +++ b/scripts/release.py @@ -0,0 +1,236 @@ +''' +Utility to schedule SDK builds on CircleCI. + +Examples: + +- Publish a snapshot from master (release.py uses the current branch) + + $ git branch + * master + 1234-fix-crash + $ python release.py --stage snapshot + +- Publish a snapshot from a feature branch (same as before, just switch branchs with git): + + $ git branch + master + * 1234-fix-crash + $ python release.py --stage snapshot + +- Publish a beta from a pre-release branch: + + $ git branch + master + * release-android-420-beta1 + $ python release.py --stage beta --version 4.2.0-beta.1 + +- Publish a beta from a release branch: + + $ git branch + master + * release-android-420 + $ python release.py --stage final --version 4.2.0 + +TODO: + +- Add a flag to wait until the release has been built (CircleCI) and published (Maven). + +''' + +import click +import json +import os +import requests +import subprocess + +# Three stages, from less stable to more stable +ALLOWED_STAGES = ['snapshot', 'beta', 'final'] + +# Get the version from GRADLE_PROPERTIES_PATH below +CURRENT_VERSION_TAG = 'current' + +# You can add your API token on https://circleci.com/account/api +CIRCLECI_API_TOKEN_ENV_VAR = 'CIRCLECI_API_TOKEN' + +# In the future we might want to consider alpha, or rc. +ALLOWED_PRE_RELEASE = ['beta'] + +# We get the default version from here +MAPBOX_GL_ANDROID_SDK_PATH = '../MapboxGLAndroidSDK' +GRADLE_PROPERTIES_PATH = '%s/gradle.properties' % MAPBOX_GL_ANDROID_SDK_PATH +GRADLE_TOKEN = 'VERSION_NAME=' + +# Triggers a new build, returns a summary of the build +URL_CIRCLECI = 'https://circleci.com/api/v1.1/project/github/mapbox/mapbox-gl-native/tree/' # + :branch + +# We support three parameters: stage, branch, and version +@click.command() +@click.option('--stage', default=ALLOWED_STAGES[0], type=click.Choice(ALLOWED_STAGES), prompt='Set stage', help='The release stage.') +@click.option('--version', default=CURRENT_VERSION_TAG, prompt='Set version', help='The version you want to publish. E.g: 4.2.0-SNAPSHOT, 4.2.0-beta.1, or 4.2.0. If you set the version to "%s", the script will default to the current SNAPSHOT version.' % CURRENT_VERSION_TAG) +def release(stage, version): + # Validate params + final_stage = validate_stage(stage=stage) + final_branch = validate_branch(stage=final_stage) + final_version = validate_version(stage=final_stage, branch=final_branch, version=version) + + # Get user confirmation + click.echo('\n===== Build information =====') + click.echo('- Stage: %s' % final_stage) + click.echo('- Branch: %s' % final_branch) + click.echo('- Version: %s' % final_version) + click.confirm('\nDoes it look right?', abort=True) + + # Proceed + if (final_stage == 'snapshot'): + publish_snapshot(branch=final_branch, version=final_version) + elif (final_stage == 'beta'): + publish_beta(branch=final_branch, version=final_version) + elif (final_stage == 'final'): + publish_final(branch=final_branch, version=final_version) + +def validate_stage(stage): + if stage not in ALLOWED_STAGES: + abort_with_message('Invalid stage: %s' % stage) + return stage + +def validate_branch(stage): + branch = git_get_current_branch() + if not branch: + abort_with_message('The current folder is not a git repository.') + if branch == 'master' and stage != 'snapshot': + abort_with_message('You need to swtich to a release branch for a beta or a final release.') + return branch + +def validate_version(stage, branch, version): + if stage == 'snapshot' and branch == 'master' and version != CURRENT_VERSION_TAG: + abort_with_message('You cannot specify a custom version if you are building a snapshot from master.') + + if not version or version == CURRENT_VERSION_TAG: + version = get_current_version(file_path=GRADLE_PROPERTIES_PATH, file_var=GRADLE_TOKEN) + + if stage == 'snapshot': + if not 'SNAPSHOT' in version: + abort_with_message('Version should contain the word SNAPSHOT: %s' % version) + elif stage == 'beta': + if not ALLOWED_PRE_RELEASE[0] in version: + abort_with_message('Version should contain the word %s: %s' % (ALLOWED_PRE_RELEASE[0], version)) + elif stage == 'final': + if not version or 'SNAPSHOT' in version or ALLOWED_PRE_RELEASE[0] in version: + abort_with_message('Version cannot be empty, or contain the words SNAPSHOT or %s: %s' % (ALLOWED_PRE_RELEASE[0], version)) + + return version + +def publish_snapshot(branch, version): + click.echo('Publishing snapshot for branch: %s (version: %s).' % (branch, version)) + if branch != 'master': + dirty_gradle = update_current_version(file_path=GRADLE_PROPERTIES_PATH, file_var=GRADLE_TOKEN, version=version) + if dirty_gradle: + git_add(path=GRADLE_PROPERTIES_PATH) + git_commit_and_push(branch=branch, version=version) + do_circleci_request(branch=branch) + +def publish_beta(branch, version): + click.echo('Publishing beta from branch: %s (version: %s).' % (branch, version)) + dirty_gradle = update_current_version(file_path=GRADLE_PROPERTIES_PATH, file_var=GRADLE_TOKEN, version=version) + if dirty_gradle: + git_add(path=GRADLE_PROPERTIES_PATH) + git_commit_and_push(branch=branch, version=version) + do_circleci_request(branch=branch) + +def publish_final(branch, version): + click.echo('Publishing final release from branch: %s (version: %s).' % (branch, version)) + dirty_gradle = update_current_version(file_path=GRADLE_PROPERTIES_PATH, file_var=GRADLE_TOKEN, version=version) + if dirty_gradle: + git_add(path=GRADLE_PROPERTIES_PATH) + if dirty_gradle: + git_commit_and_push(branch=branch, version=version) + do_circleci_request(branch=branch) + +# +# Utils +# + +def abort_with_message(message): + click.echo(message) + click.get_current_context().abort() + +def execute_call(command): + click.echo('Executing: %s' % command) + result = subprocess.call(command, shell=True) + if result != 0: + abort_with_message('Command failed: %s' % command) + +# +# CircleCI +# + +def get_circleci_api_token(): + circleci_api_token = os.environ.get(CIRCLECI_API_TOKEN_ENV_VAR) + if not circleci_api_token: + abort_with_message('You need to set the CIRCLECI_API_TOKEN environment variable.') + click.echo('Found CircleCI API token.') + return circleci_api_token + +def do_circleci_request(branch): + url = URL_CIRCLECI + branch + params = {'circle-token': get_circleci_api_token()} + click.echo('CircleCI request to %s (params: %s)' % (url, json.dumps(params))) + click.confirm('\nDo you want to start a build?', abort=True) + + r = requests.post(url, params=params) + click.echo('- CircleCI response code: %s' % r.status_code) + +# +# Git +# + +def git_get_current_branch(): + return subprocess.check_output('git symbolic-ref --short HEAD'.split(' ')).strip() + +def git_add(path): + execute_call(command='git add %s' % path) + +def git_commit_and_push(branch, version): + message = '[android] [auto] Update properties to version %s in preparation for build.' % version + commands = [ + 'git commit -m "%s"' % message, + 'git push -u origin %s' % branch] + for command in commands: + execute_call(command=command) + +# +# Read and update properties files +# + +def get_current_version(file_path, file_var): + click.echo('Getting current version from %s.' % file_path) + with open(file_path, 'r') as f: + for line in f: + if line.startswith(file_var): + version_name = line[len(file_var):].strip() + click.echo('Current version is %s.' % version_name) + return version_name + return None + +def update_current_version(file_path, file_var, version): + dirty = False + click.echo('Updating file to version %s: %s.' % (version, file_path)) + with open(file_path, 'r') as f: + file_lines = f.readlines() + for line_number in range(len(file_lines)): + if file_lines[line_number].startswith(file_var): + content_old = file_lines[line_number] + content_new = '%s%s\n' % (file_var, version) + if (content_old != content_new): + click.echo('%s -> %s' % (content_old.strip(), content_new.strip())) + file_lines[line_number] = content_new + dirty = True + if dirty: + with open(file_path, 'w') as f: + f.writelines(file_lines) + else: + click.echo('File already has the right version.') + return dirty + +if __name__ == '__main__': + release() diff --git a/scripts/run-render-test.py b/scripts/run-render-test.py new file mode 100644 index 000000000..940ae7a3d --- /dev/null +++ b/scripts/run-render-test.py @@ -0,0 +1,43 @@ +#!/usr/bin/python + +import os +import shutil + +catPath = os.getcwd() + "/platform/android/build/render-test/" +failCounter = 0 +testCounter = 0 +for cat in os.listdir(catPath): + testPath = catPath + cat + "/" + for test in os.listdir(testPath): + inputPath = os.getcwd() + "/mapbox-gl-js/test/integration/render-tests/" + cat + "/" + test + outputPath = testPath + test + + expected = outputPath + "/expected.png" + actual = outputPath + "/actual.png" + output = outputPath + "/output.png" + + shutil.copyfile(inputPath + "/expected.png", expected) + shutil.copyfile(inputPath + "/style.json", outputPath + "/style.json") + pixelmatchCommand = "node_modules/pixelmatch/bin/pixelmatch " + actual + " " + expected + " " + output + " 0.1" + + print "Pixel match " + cat + " " + test + + with os.popen(pixelmatchCommand) as f: + for line_terminated in f: + line = line_terminated.rstrip('\n') + pair = line.split(": ") + if pair[0] == "error": + testCounter += 1 + currentError = pair[1] + if currentError != "0%": + failCounter += 1 + # below deletes successful results + # print cat + " " + test + " " + currentError + # else: + # try: + # if os.path.isdir(outputPath): + # shutil.rmtree(outputPath) + # except OSError, e: + # print ("Error: %s - %s." % (e.filename, e.strerror)) +print +print str(failCounter) + "/" + str(testCounter) diff --git a/scripts/validate-license.py b/scripts/validate-license.py new file mode 100644 index 000000000..1c9a153b8 --- /dev/null +++ b/scripts/validate-license.py @@ -0,0 +1,17 @@ +#!/usr/bin/python + +from subprocess import call +from subprocess import Popen, PIPE +import sys + +## Run license generation +call('cd ../../ && make android-license', shell=True) + +## Git diff changes +p = Popen(['git', 'diff', '--name-only', 'LICENSE.md'], stdin=PIPE, stdout=PIPE, stderr=PIPE) +output, err = p.communicate(b"input data that is passed to subprocess' stdin") +if b"platform/android/LICENSE.md" in output: + raise ValueError("""An error ocurred while validating the license generation. + Changes were detected to the license generation output + but weren't commited. Run make android-license and + commit the changeset to make this validation pass.""") \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..c0315fed0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':MapboxGLAndroidSDK', ':MapboxGLAndroidSDKTestApp', ':MapboxGLAndroidSDKLint' \ No newline at end of file diff --git a/src/android_renderer_backend.cpp b/src/android_renderer_backend.cpp new file mode 100755 index 000000000..e2d064838 --- /dev/null +++ b/src/android_renderer_backend.cpp @@ -0,0 +1,67 @@ +#include "android_renderer_backend.hpp" + +#include +#include +#include + +#include + +#include + +namespace mbgl { +namespace android { + +class AndroidGLRenderableResource final : public mbgl::gl::RenderableResource { +public: + AndroidGLRenderableResource(AndroidRendererBackend& backend_) : backend(backend_) { + } + + void bind() override { + assert(gfx::BackendScope::exists()); + backend.setFramebufferBinding(0); + backend.setViewport(0, 0, backend.getSize()); + } + +private: + AndroidRendererBackend& backend; +}; + +AndroidRendererBackend::AndroidRendererBackend() + : gl::RendererBackend(gfx::ContextMode::Unique), + mbgl::gfx::Renderable({ 64, 64 }, std::make_unique(*this)) { +} + +AndroidRendererBackend::~AndroidRendererBackend() = default; + +gl::ProcAddress AndroidRendererBackend::getExtensionFunctionPointer(const char* name) { + assert(gfx::BackendScope::exists()); + return eglGetProcAddress(name); +} + +void AndroidRendererBackend::updateViewPort() { + assert(gfx::BackendScope::exists()); + setViewport(0, 0, size); +} + +void AndroidRendererBackend::resizeFramebuffer(int width, int height) { + size = { static_cast(width), static_cast(height) }; +} + +PremultipliedImage AndroidRendererBackend::readFramebuffer() { + assert(gfx::BackendScope::exists()); + return gl::RendererBackend::readFramebuffer(size); +} + +void AndroidRendererBackend::updateAssumedState() { + assumeFramebufferBinding(0); + assumeViewport(0, 0, size); +} + +void AndroidRendererBackend::markContextLost() { + if (context) { + getContext().setCleanupOnDestruction(false); + } +} + +} // namespace android +} // namspace mbgl diff --git a/src/android_renderer_backend.hpp b/src/android_renderer_backend.hpp new file mode 100755 index 000000000..f06f38e68 --- /dev/null +++ b/src/android_renderer_backend.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace android { + +class AndroidRendererBackend : public gl::RendererBackend, public mbgl::gfx::Renderable { +public: + AndroidRendererBackend(); + ~AndroidRendererBackend() override; + + void updateViewPort(); + + // Ensures the current context is not cleaned up when destroyed + void markContextLost(); + + void resizeFramebuffer(int width, int height); + PremultipliedImage readFramebuffer(); + + // mbgl::gfx::RendererBackend implementation +public: + mbgl::gfx::Renderable& getDefaultRenderable() override { + return *this; + } + +protected: + void activate() override { + // no-op + } + void deactivate() override { + // no-op + } + + // mbgl::gl::RendererBackend implementation +protected: + mbgl::gl::ProcAddress getExtensionFunctionPointer(const char*) override; + void updateAssumedState() override; +}; + +} // namespace android +} // namespace mbgl diff --git a/src/android_renderer_frontend.cpp b/src/android_renderer_frontend.cpp new file mode 100644 index 000000000..d1a2624f0 --- /dev/null +++ b/src/android_renderer_frontend.cpp @@ -0,0 +1,143 @@ +#include "android_renderer_frontend.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "android_renderer_backend.hpp" + +namespace mbgl { +namespace android { + +// Forwards RendererObserver signals to the given +// Delegate RendererObserver on the given RunLoop +class ForwardingRendererObserver : public RendererObserver { +public: + ForwardingRendererObserver(util::RunLoop& mapRunLoop, RendererObserver& delegate_) + : mailbox(std::make_shared(mapRunLoop)) + , delegate(delegate_, mailbox) { + } + + ~ForwardingRendererObserver() { + mailbox->close(); + } + + void onInvalidate() override { + delegate.invoke(&RendererObserver::onInvalidate); + } + + void onResourceError(std::exception_ptr err) override { + delegate.invoke(&RendererObserver::onResourceError, err); + } + + void onWillStartRenderingMap() override { + delegate.invoke(&RendererObserver::onWillStartRenderingMap); + } + + void onWillStartRenderingFrame() override { + delegate.invoke(&RendererObserver::onWillStartRenderingFrame); + } + + void onDidFinishRenderingFrame(RenderMode mode, bool repaintNeeded, bool placementChanged) override { + delegate.invoke(&RendererObserver::onDidFinishRenderingFrame, mode, repaintNeeded, placementChanged); + } + + void onDidFinishRenderingMap() override { + delegate.invoke(&RendererObserver::onDidFinishRenderingMap); + } + + void onStyleImageMissing(const std::string& id, StyleImageMissingCallback done) override { + delegate.invoke(&RendererObserver::onStyleImageMissing, id, done); + } + + void onRemoveUnusedStyleImages(const std::vector& ids) override { + delegate.invoke(&RendererObserver::onRemoveUnusedStyleImages, ids); + } + +private: + std::shared_ptr mailbox; + ActorRef delegate; +}; + +AndroidRendererFrontend::AndroidRendererFrontend(MapRenderer& mapRenderer_) + : mapRenderer(mapRenderer_) + , mapRunLoop(util::RunLoop::Get()) + , updateAsyncTask(std::make_unique([this]() { + mapRenderer.update(std::move(updateParams)); + mapRenderer.requestRender(); + })) { +} + +AndroidRendererFrontend::~AndroidRendererFrontend() = default; + +void AndroidRendererFrontend::reset() { + mapRenderer.reset(); +} + +void AndroidRendererFrontend::setObserver(RendererObserver& observer) { + assert (util::RunLoop::Get()); + // Don't call the Renderer directly, but use MapRenderer#setObserver to make sure + // the Renderer may be re-initialised without losing the RendererObserver reference. + mapRenderer.setObserver(std::make_unique(*mapRunLoop, observer)); +} + +void AndroidRendererFrontend::update(std::shared_ptr params) { + updateParams = std::move(params); + updateAsyncTask->send(); +} + +void AndroidRendererFrontend::reduceMemoryUse() { + mapRenderer.actor().invoke(&Renderer::reduceMemoryUse); +} + +std::vector AndroidRendererFrontend::querySourceFeatures(const std::string& sourceID, + const SourceQueryOptions& options) const { + // Waits for the result from the orchestration thread and returns + return mapRenderer.actor().ask(&Renderer::querySourceFeatures, sourceID, options).get(); +} + +std::vector AndroidRendererFrontend::queryRenderedFeatures(const ScreenBox& box, + const RenderedQueryOptions& options) const { + + // Select the right overloaded method + std::vector (Renderer::*fn)(const ScreenBox&, const RenderedQueryOptions&) const = &Renderer::queryRenderedFeatures; + + // Waits for the result from the orchestration thread and returns + return mapRenderer.actor().ask(fn, box, options).get(); +} + +std::vector AndroidRendererFrontend::queryRenderedFeatures(const ScreenCoordinate& point, + const RenderedQueryOptions& options) const { + + // Select the right overloaded method + std::vector (Renderer::*fn)(const ScreenCoordinate&, const RenderedQueryOptions&) const = &Renderer::queryRenderedFeatures; + + // Waits for the result from the orchestration thread and returns + return mapRenderer.actor().ask(fn, point, options).get(); +} + +AnnotationIDs AndroidRendererFrontend::queryPointAnnotations(const ScreenBox& box) const { + // Waits for the result from the orchestration thread and returns + return mapRenderer.actor().ask(&Renderer::queryPointAnnotations, box).get(); +} + +AnnotationIDs AndroidRendererFrontend::queryShapeAnnotations(const ScreenBox& box) const { + // Waits for the result from the orchestration thread and returns + return mapRenderer.actor().ask(&Renderer::queryShapeAnnotations, box).get(); +} + +FeatureExtensionValue AndroidRendererFrontend::queryFeatureExtensions(const std::string& sourceID, + const Feature& feature, + const std::string& extension, + const std::string& extensionField, + const optional>& args) const { + return mapRenderer.actor().ask(&Renderer::queryFeatureExtensions, sourceID, feature, extension, extensionField, args).get(); +} + +} // namespace android +} // namespace mbgl + diff --git a/src/android_renderer_frontend.hpp b/src/android_renderer_frontend.hpp new file mode 100644 index 000000000..dc5f11e57 --- /dev/null +++ b/src/android_renderer_frontend.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "map_renderer.hpp" + +namespace mbgl { + +class RenderedQueryOptions; +class SourceQueryOptions; + +namespace util { + +class AsyncTask; + +} // namespace util + +namespace android { + +class AndroidRendererFrontend : public RendererFrontend { +public: + + AndroidRendererFrontend(MapRenderer&); + ~AndroidRendererFrontend() override; + + void reset() override; + void setObserver(RendererObserver&) override; + + void update(std::shared_ptr) override; + + // Feature querying + std::vector queryRenderedFeatures(const ScreenCoordinate&, const RenderedQueryOptions&) const; + std::vector queryRenderedFeatures(const ScreenBox&, const RenderedQueryOptions&) const; + std::vector querySourceFeatures(const std::string& sourceID, const SourceQueryOptions&) const; + AnnotationIDs queryPointAnnotations(const ScreenBox& box) const; + AnnotationIDs queryShapeAnnotations(const ScreenBox& box) const; + + + // Feature extension query + FeatureExtensionValue + queryFeatureExtensions(const std::string& sourceID, + const Feature& feature, + const std::string& extension, + const std::string& extensionField, + const optional>& args) const; + + // Memory + void reduceMemoryUse(); + +private: + MapRenderer& mapRenderer; + util::RunLoop* mapRunLoop; + std::unique_ptr updateAsyncTask; + std::shared_ptr updateParams; +}; + +} // namespace android +} // namespace mbgl diff --git a/src/annotation/marker.cpp b/src/annotation/marker.cpp new file mode 100644 index 000000000..899f87a74 --- /dev/null +++ b/src/annotation/marker.cpp @@ -0,0 +1,23 @@ +#include "marker.hpp" + +namespace mbgl { +namespace android { + +mbgl::Point Marker::getPosition(jni::JNIEnv& env, const jni::Object& marker) { + static auto& javaClass = jni::Class::Singleton(env); + static auto positionField = javaClass.GetField>(env, "position"); + return LatLng::getGeometry(env, marker.Get(env, positionField)); +} + +std::string Marker::getIconId(jni::JNIEnv& env, const jni::Object& marker) { + static auto& javaClass = jni::Class::Singleton(env); + static auto iconIdField = javaClass.GetField(env, "iconId"); + return jni::Make(env, marker.Get(env, iconIdField)); +} + +void Marker::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/annotation/marker.hpp b/src/annotation/marker.hpp new file mode 100644 index 000000000..b38a25b4f --- /dev/null +++ b/src/annotation/marker.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include + +#include "../geometry/lat_lng.hpp" + +namespace mbgl { +namespace android { + +class Marker : private mbgl::util::noncopyable { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/annotations/Marker"; }; + + static mbgl::Point getPosition(jni::JNIEnv&, const jni::Object&); + + static std::string getIconId(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); + +}; + + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/annotation/multi_point.hpp b/src/annotation/multi_point.hpp new file mode 100644 index 000000000..20f1b3eaf --- /dev/null +++ b/src/annotation/multi_point.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "../geometry/lat_lng.hpp" +#include "../java/util.hpp" + +namespace mbgl { +namespace android { + +class MultiPoint : protected mbgl::util::noncopyable { + +protected: + + template + static Geometry toGeometry(JNIEnv& env, const jni::Object& pointsList) { + auto jarray = java::util::List::toArray(env, pointsList); + + std::size_t size = jarray.Length(env); + + Geometry geometry; + geometry.reserve(size); + + for (std::size_t i = 0; i < size; i++) { + geometry.push_back(LatLng::getGeometry(env, jarray.Get(env, i))); + } + + return geometry; + } +}; + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/annotation/polygon.cpp b/src/annotation/polygon.cpp new file mode 100644 index 000000000..f22cffb10 --- /dev/null +++ b/src/annotation/polygon.cpp @@ -0,0 +1,40 @@ +#include "polygon.hpp" + +#include "../conversion/color.hpp" + +namespace mbgl { +namespace android { + +mbgl::FillAnnotation Polygon::toAnnotation(jni::JNIEnv& env, const jni::Object& polygon) { + static auto& javaClass = jni::Class::Singleton(env); + static auto points = javaClass.GetField>(env, "points"); + static auto holes = javaClass.GetField>(env, "holes"); + static auto alpha = javaClass.GetField(env, "alpha"); + static auto fillColor = javaClass.GetField(env, "fillColor"); + static auto strokeColor = javaClass.GetField(env, "strokeColor"); + + mbgl::Polygon geometry { + MultiPoint::toGeometry>(env, polygon.Get(env, points)) + }; + + auto jHoleListsArray = java::util::List::toArray(env, polygon.Get(env, holes)); + + std::size_t jHoleListsSize = jHoleListsArray.Length(env); + for (std::size_t i = 0; i < jHoleListsSize; i++) { + geometry.push_back(MultiPoint::toGeometry>(env, jHoleListsArray.Get(env, i))); + } + + mbgl::FillAnnotation annotation { geometry }; + annotation.opacity = polygon.Get(env, alpha); + annotation.color = *conversion::convert(env, polygon.Get(env, fillColor)); + annotation.outlineColor = *conversion::convert(env, polygon.Get(env, strokeColor)); + + return annotation; +} + +void Polygon::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/annotation/polygon.hpp b/src/annotation/polygon.hpp new file mode 100644 index 000000000..bc21878ef --- /dev/null +++ b/src/annotation/polygon.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include "multi_point.hpp" + +namespace mbgl { +namespace android { + +class Polygon : private MultiPoint { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/annotations/Polygon"; }; + + static mbgl::FillAnnotation toAnnotation(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/annotation/polyline.cpp b/src/annotation/polyline.cpp new file mode 100644 index 000000000..d6161e856 --- /dev/null +++ b/src/annotation/polyline.cpp @@ -0,0 +1,31 @@ +#include "polyline.hpp" + +#include "../conversion/color.hpp" + +namespace mbgl { +namespace android { + +mbgl::LineAnnotation Polyline::toAnnotation(jni::JNIEnv& env, const jni::Object& polyline) { + static auto& javaClass = jni::Class::Singleton(env); + static auto points = javaClass.GetField>(env, "points"); + static auto alpha = javaClass.GetField(env, "alpha"); + static auto color = javaClass.GetField(env, "color"); + static auto width = javaClass.GetField(env, "width"); + + mbgl::LineAnnotation annotation { + MultiPoint::toGeometry>(env, polyline.Get(env, points)) + }; + + annotation.opacity = polyline.Get(env, alpha); + annotation.color = *conversion::convert(env, polyline.Get(env, color)); + annotation.width = polyline.Get(env, width); + + return annotation; +} + +void Polyline::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/annotation/polyline.hpp b/src/annotation/polyline.hpp new file mode 100644 index 000000000..6178e98cf --- /dev/null +++ b/src/annotation/polyline.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include "multi_point.hpp" + +namespace mbgl { +namespace android { + +class Polyline : private MultiPoint { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/annotations/Polyline"; }; + + static mbgl::LineAnnotation toAnnotation(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/asset_manager.hpp b/src/asset_manager.hpp new file mode 100644 index 000000000..b87d18951 --- /dev/null +++ b/src/asset_manager.hpp @@ -0,0 +1,14 @@ +#pragma once + +namespace mbgl { +namespace android { + +class AssetManager { +public: + static constexpr auto Name() { + return "android/content/res/AssetManager"; + }; +}; + +} // namespace android +} // namespace mbgl diff --git a/src/asset_manager_file_source.cpp b/src/asset_manager_file_source.cpp new file mode 100644 index 000000000..b9150deef --- /dev/null +++ b/src/asset_manager_file_source.cpp @@ -0,0 +1,57 @@ +#include "asset_manager_file_source.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace mbgl { + +class AssetManagerFileSource::Impl { +public: + Impl(ActorRef, AAssetManager* assetManager_) : assetManager(assetManager_) { + } + + void request(const std::string& url, ActorRef req) { + // Note: AssetManager already prepends "assets" to the filename. + const std::string path = mbgl::util::percentDecode(url.substr(8)); + + Response response; + + if (AAsset* asset = AAssetManager_open(assetManager, path.c_str(), AASSET_MODE_BUFFER)) { + response.data = std::make_shared( + reinterpret_cast(AAsset_getBuffer(asset)), AAsset_getLength64(asset)); + AAsset_close(asset); + } else { + response.error = std::make_unique(Response::Error::Reason::NotFound, + "Could not read asset"); + } + + req.invoke(&FileSourceRequest::setResponse, response); + } + +private: + AAssetManager* assetManager; +}; + +AssetManagerFileSource::AssetManagerFileSource(jni::JNIEnv& env, const jni::Object& assetManager_) + : assetManager(jni::NewGlobal(env, assetManager_)), + impl(std::make_unique>("AssetManagerFileSource", + AAssetManager_fromJava(&env, jni::Unwrap(assetManager.get())))) { +} + +AssetManagerFileSource::~AssetManagerFileSource() = default; + +std::unique_ptr AssetManagerFileSource::request(const Resource& resource, Callback callback) { + auto req = std::make_unique(std::move(callback)); + + impl->actor().invoke(&Impl::request, resource.url, req->actor()); + + return std::move(req); +} + +} // namespace mbgl diff --git a/src/asset_manager_file_source.hpp b/src/asset_manager_file_source.hpp new file mode 100644 index 000000000..4ec5b4c30 --- /dev/null +++ b/src/asset_manager_file_source.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "asset_manager.hpp" + +#include + +namespace mbgl { + +namespace util { +template class Thread; +} // namespace util + +class AssetManagerFileSource : public FileSource { +public: + AssetManagerFileSource(jni::JNIEnv&, const jni::Object&); + ~AssetManagerFileSource() override; + + std::unique_ptr request(const Resource&, Callback) override; + +private: + class Impl; + + jni::Global> assetManager; + std::unique_ptr> impl; +}; + +} // namespace mbgl diff --git a/src/async_task.cpp b/src/async_task.cpp new file mode 100644 index 000000000..6c14e96fa --- /dev/null +++ b/src/async_task.cpp @@ -0,0 +1,62 @@ +#include "run_loop_impl.hpp" + +#include +#include + +#include +#include + +namespace mbgl { +namespace util { + +class AsyncTask::Impl : public RunLoop::Impl::Runnable { +public: + Impl(std::function&& fn) + : queued(true), task(std::move(fn)) { + } + + ~Impl() { + loop->removeRunnable(this); + } + + void maySend() { + if (queued) { + queued = false; + loop->addRunnable(this); + } + } + + TimePoint dueTime() const override { + return due; + } + + void runTask() override { + loop->removeRunnable(this); + queued = true; + task(); + } + +private: + // Always expired, run immediately. + const TimePoint due = Clock::now(); + + RunLoop::Impl* loop = reinterpret_cast(RunLoop::getLoopHandle()); + + // TODO: Use std::atomic_flag if we ever drop + // support for ARMv5 + std::atomic queued; + std::function task; +}; + +AsyncTask::AsyncTask(std::function&& fn) + : impl(std::make_unique(std::move(fn))) { +} + +AsyncTask::~AsyncTask() = default; + +void AsyncTask::send() { + impl->maySend(); +} + +} // namespace util +} // namespace mbgl diff --git a/src/attach_env.cpp b/src/attach_env.cpp new file mode 100644 index 000000000..6da075ee3 --- /dev/null +++ b/src/attach_env.cpp @@ -0,0 +1,22 @@ +#include "attach_env.hpp" +#include "jni.hpp" + +namespace mbgl { +namespace android { + +UniqueEnv AttachEnv() { + JNIEnv* env = nullptr; + jint err = theJVM->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); + + switch (err) { + case JNI_OK: + return UniqueEnv(env, JNIEnvDeleter(*theJVM, false)); + case JNI_EDETACHED: + return UniqueEnv(jni::AttachCurrentThread(*theJVM).release(), JNIEnvDeleter(*theJVM, true)); + default: + throw std::system_error(err, jni::ErrorCategory()); + } +} + +} +} diff --git a/src/attach_env.hpp b/src/attach_env.hpp new file mode 100644 index 000000000..8a9f4a7f8 --- /dev/null +++ b/src/attach_env.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +namespace mbgl { +namespace android { + +class JNIEnvDeleter { +private: + jni::JavaVM* vm = nullptr; + bool detach = false; + +public: + JNIEnvDeleter() = default; + JNIEnvDeleter(jni::JavaVM& v, bool d) + : vm(&v), detach(d) {} + + void operator()(jni::JNIEnv* p) const { + if (p && detach) { + assert(vm); + vm->DetachCurrentThread(); + } + } +}; + +using UniqueEnv = std::unique_ptr; + +UniqueEnv AttachEnv(); + +} +} diff --git a/src/bitmap.cpp b/src/bitmap.cpp new file mode 100644 index 000000000..eb7c676b1 --- /dev/null +++ b/src/bitmap.cpp @@ -0,0 +1,124 @@ +#include "bitmap.hpp" + +#include +#include + +namespace mbgl { +namespace android { + +class PixelGuard { +public: + PixelGuard(jni::JNIEnv& env_, const jni::Object& bitmap_) : env(env_), bitmap(bitmap_) { + const int result = AndroidBitmap_lockPixels(&env, jni::Unwrap(*bitmap), + reinterpret_cast(&address)); + if (result != ANDROID_BITMAP_RESULT_SUCCESS) { + throw std::runtime_error("bitmap decoding: could not lock pixels"); + } + } + + ~PixelGuard() { + const int result = AndroidBitmap_unlockPixels(&env, jni::Unwrap(*bitmap)); + if (result != ANDROID_BITMAP_RESULT_SUCCESS) { + Log::Warning(mbgl::Event::General, "Bitmap decoding: could not unlock pixels"); + } + } + + auto* get() { + return address; + } + +private: + jni::JNIEnv& env; + const jni::Object& bitmap; + uint8_t* address; +}; + +jni::Local> Bitmap::Config::Create(jni::JNIEnv& env, Value value) { + static auto& _class = jni::Class::Singleton(env); + switch (value) { + case ALPHA_8: + return _class.Get(env, _class.GetStaticField>(env, "ALPHA_8")); + case ARGB_4444: + return _class.Get(env, _class.GetStaticField>(env, "ARGB_4444")); + case ARGB_8888: + return _class.Get(env, _class.GetStaticField>(env, "ARGB_8888")); + case RGB_565: + return _class.Get(env, _class.GetStaticField>(env, "RGB_565")); + default: + throw std::runtime_error("invalid enum value for Bitmap.Config"); + } +} + +void Bitmap::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); + jni::Class::Singleton(env); +} + +jni::Local> Bitmap::CreateBitmap(jni::JNIEnv& env, + jni::jint width, + jni::jint height, + const jni::Object& config) { + static auto& _class = jni::Class::Singleton(env); + static auto method = _class.GetStaticMethod (jni::jint, jni::jint, jni::Object)>(env, "createBitmap"); + + return _class.Call(env, method, width, height, config); +} + +PremultipliedImage Bitmap::GetImage(jni::JNIEnv& env, const jni::Object& bitmap) { + AndroidBitmapInfo info; + const int result = AndroidBitmap_getInfo(&env, jni::Unwrap(*bitmap), &info); + if (result != ANDROID_BITMAP_RESULT_SUCCESS) { + throw std::runtime_error("bitmap decoding: couldn't get bitmap info"); + } + if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) { + return Bitmap::GetImage(env, Bitmap::Copy(env, bitmap)); + } + + PixelGuard guard(env, bitmap); + + // Copy the Android Bitmap into the PremultipliedImage. + auto pixels = + std::make_unique(info.width * info.height * PremultipliedImage::channels); + for (uint32_t y = 0; y < info.height; y++) { + auto begin = guard.get() + y * info.stride; + std::copy(begin, begin + info.width * PremultipliedImage::channels, + pixels.get() + y * info.width * PremultipliedImage::channels); + } + + return { Size{ info.width, info.height }, std::move(pixels) }; +} + +jni::Local> Bitmap::CreateBitmap(jni::JNIEnv& env, const PremultipliedImage& image) { + auto bitmap = CreateBitmap(env, image.size.width, image.size.height, Config::ARGB_8888); + + AndroidBitmapInfo info; + const int result = AndroidBitmap_getInfo(&env, jni::Unwrap(*bitmap), &info); + if (result != ANDROID_BITMAP_RESULT_SUCCESS) { + // TODO: more specific information + throw std::runtime_error("bitmap creation: couldn't get bitmap info"); + } + + assert(info.width == image.size.width); + assert(info.height == image.size.height); + assert(info.format == ANDROID_BITMAP_FORMAT_RGBA_8888); + + PixelGuard guard(env, bitmap); + + // Copy the PremultipliedImage into the Android Bitmap + for (uint32_t y = 0; y < image.size.height; y++) { + auto begin = image.data.get() + y * image.stride(); + std::copy(begin, begin + image.stride(), guard.get() + y * info.stride); + } + + return bitmap; +} + +jni::Local> Bitmap::Copy(jni::JNIEnv& env, const jni::Object& bitmap) { + static auto& klass = jni::Class::Singleton(env); + static auto copy = klass.GetMethod (jni::Object, jni::jboolean)>(env, "copy"); + + return bitmap.Call(env, copy, Bitmap::Config::Create(env, Bitmap::Config::Value::ARGB_8888), jni::jni_false); +} + +} // namespace android +} // namespace mbgl diff --git a/src/bitmap.hpp b/src/bitmap.hpp new file mode 100644 index 000000000..9d96405c2 --- /dev/null +++ b/src/bitmap.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include + +namespace mbgl { +namespace android { + +class Bitmap { +public: + class Config { + public: + static constexpr auto Name() { return "android/graphics/Bitmap$Config"; }; + + enum Value { + ALPHA_8, + ARGB_4444, + ARGB_8888, + RGB_565, + }; + + static jni::Local> Create(jni::JNIEnv&, Value); + }; + + static constexpr auto Name() { return "android/graphics/Bitmap"; }; + static void registerNative(jni::JNIEnv&); + + static jni::Local> + CreateBitmap(jni::JNIEnv&, jni::jint width, jni::jint height, const jni::Object&); + + static jni::Local> + CreateBitmap(jni::JNIEnv& env, jni::jint width, jni::jint height, Config::Value config) { + return CreateBitmap(env, width, height, Config::Create(env, config)); + } + + static PremultipliedImage GetImage(jni::JNIEnv&, const jni::Object&); + static jni::Local> CreateBitmap(jni::JNIEnv&, const PremultipliedImage&); + static jni::Local> Copy(jni::JNIEnv&, const jni::Object&); +}; + +} // namespace android +} // namespace mbgl diff --git a/src/bitmap_factory.cpp b/src/bitmap_factory.cpp new file mode 100644 index 000000000..170bd52ea --- /dev/null +++ b/src/bitmap_factory.cpp @@ -0,0 +1,25 @@ +#include "bitmap_factory.hpp" + +namespace mbgl { +namespace android { + +void BitmapFactory::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +jni::Local> BitmapFactory::DecodeByteArray(jni::JNIEnv& env, + jni::Array& data, + jni::jint offset, + jni::jint length) { + static auto& _class = jni::Class::Singleton(env); + + // Images are loaded with ARGB_8888 config, and premultiplied by default, which is exactly + // what we want, so we're not providing a BitmapFactory.Options object. + using Signature = jni::Object (jni::Array, jni::jint, jni::jint); + static auto method = _class.GetStaticMethod(env, "decodeByteArray"); + + return _class.Call(env, method, data, offset, length); +} + +} // namespace android +} // namespace mbgl diff --git a/src/bitmap_factory.hpp b/src/bitmap_factory.hpp new file mode 100644 index 000000000..f73b7a290 --- /dev/null +++ b/src/bitmap_factory.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "bitmap.hpp" + +namespace mbgl { +namespace android { + +class BitmapFactory { +public: + static constexpr auto Name() { return "android/graphics/BitmapFactory"; }; + static void registerNative(jni::JNIEnv&); + + static jni::Local> + DecodeByteArray(jni::JNIEnv&, jni::Array& data, jni::jint offset, jni::jint length); +}; + +} // namespace android +} // namespace mbgl diff --git a/src/connectivity_listener.cpp b/src/connectivity_listener.cpp new file mode 100644 index 000000000..9c13f1f74 --- /dev/null +++ b/src/connectivity_listener.cpp @@ -0,0 +1,41 @@ +#include "connectivity_listener.hpp" + +#include +#include + +#include + +namespace mbgl { +namespace android { + + /** + * Invoked when the construction is initiated from the jvm + */ + ConnectivityListener::ConnectivityListener(jni::JNIEnv&) {} + + ConnectivityListener::~ConnectivityListener() {} + + void ConnectivityListener::onConnectivityStateChanged(jni::JNIEnv&, jni::jboolean connected) { + NetworkStatus::Set(connected ? NetworkStatus::Status::Online : NetworkStatus::Status::Offline); + } + + void ConnectivityListener::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, + javaClass, + "nativePtr", + std::make_unique, + "initialize", + "finalize", + METHOD(&ConnectivityListener::onConnectivityStateChanged, "nativeOnConnectivityStateChanged") + ); + } + +} // namespace android +} // namespace mbgl diff --git a/src/connectivity_listener.hpp b/src/connectivity_listener.hpp new file mode 100644 index 000000000..a09507c0f --- /dev/null +++ b/src/connectivity_listener.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include + +namespace mbgl { +namespace android { + +class ConnectivityListener : private mbgl::util::noncopyable { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/net/NativeConnectivityListener"; }; + + static void registerNative(jni::JNIEnv&); + + /* + * Called when a Java object was created from the jvm side + */ + ConnectivityListener(jni::JNIEnv&); + + ~ConnectivityListener(); + + void onConnectivityStateChanged(jni::JNIEnv&, jni::jboolean); + +}; + +} // namespace android +} // namespace mbgl diff --git a/src/conversion/collection.cpp b/src/conversion/collection.cpp new file mode 100644 index 000000000..954a161ba --- /dev/null +++ b/src/conversion/collection.cpp @@ -0,0 +1,32 @@ +#include "collection.hpp" +#include "constant.hpp" + +namespace mbgl { +namespace android { +namespace conversion { + +std::vector toVector(JNIEnv& env, const jni::Array& array) { + std::size_t len = array.Length(env); + std::vector vector; + vector.reserve(len); + + for (std::size_t i = 0; i < len; i++) { + vector.push_back(jni::Make(env, array.Get(env, i))); + } + + return vector; +} + +jni::Local> toArray(JNIEnv& env, const std::vector& vector) { + auto result = jni::Array::New(env, vector.size()); + + for (std::size_t i = 0; i < vector.size(); i++) { + result.Set(env, i, jni::Make(env, vector.at(i))); + } + + return result; +} + +} +} +} diff --git a/src/conversion/collection.hpp b/src/conversion/collection.hpp new file mode 100644 index 000000000..322ebb62e --- /dev/null +++ b/src/conversion/collection.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "conversion.hpp" + +#include + +#include + +namespace mbgl { +namespace android { +namespace conversion { + +std::vector toVector(JNIEnv& env, const jni::Array& array); +jni::Local> toArray(JNIEnv& env, const std::vector&); + +} +} +} diff --git a/src/conversion/color.cpp b/src/conversion/color.cpp new file mode 100644 index 000000000..ce85943e6 --- /dev/null +++ b/src/conversion/color.cpp @@ -0,0 +1,17 @@ +#include "color.hpp" + +namespace mbgl { +namespace android { +namespace conversion { + +Result Converter::operator()(jni::JNIEnv&, const int& color) const { + float r = (color >> 16) & 0xFF; + float g = (color >> 8) & 0xFF; + float b = (color) & 0xFF; + float a = (color >> 24) & 0xFF; + return { mbgl::Color( r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f ) }; +} + +} // namespace conversion +} // namespace style +} // namespace mbgl diff --git a/src/conversion/color.hpp b/src/conversion/color.hpp new file mode 100644 index 000000000..2b4144b93 --- /dev/null +++ b/src/conversion/color.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "conversion.hpp" + +#include + +namespace mbgl { +namespace android { +namespace conversion { + +template <> +struct Converter { + Result operator()(jni::JNIEnv&, const int& color) const; +}; + +} // namespace conversion +} // namespace style +} // namespace mbgl diff --git a/src/conversion/constant.cpp b/src/conversion/constant.cpp new file mode 100644 index 000000000..a409e5dfd --- /dev/null +++ b/src/conversion/constant.cpp @@ -0,0 +1,57 @@ +#include "constant.hpp" +#include "collection.hpp" +#include "../style/formatted.hpp" + +#include + +namespace mbgl { +namespace android { +namespace conversion { + +Result>> Converter>, bool>::operator()(jni::JNIEnv& env, const bool& value) const { + return jni::Box(env, value ? jni::jni_true : jni::jni_false); +} + +Result>> Converter>, float>::operator()(jni::JNIEnv& env, const float& value) const { + return jni::Box(env, value); +} + +Result>> Converter>, double>::operator()(jni::JNIEnv& env, const double& value) const { + return jni::Box(env, value); +} + +Result>> Converter>, std::string>::operator()(jni::JNIEnv& env, const std::string& value) const { + return jni::Make(env, value); +} + +Result>> Converter>, Color>::operator()(jni::JNIEnv& env, const Color& value) const { + return jni::Make(env, value.stringify()); +} + +Result>> Converter>, style::expression::Formatted>::operator()(jni::JNIEnv& env, const style::expression::Formatted& value) const { + return Formatted::New(env, value); +} + +Result>> Converter>, std::vector>::operator()(jni::JNIEnv& env, const std::vector& value) const { + auto result = jni::Array::New(env, value.size()); + + for (std::size_t i = 0; i < value.size(); i++) { + result.Set(env, i, jni::Make(env, value.at(i))); + } + + return result; +} + +Result>> Converter>, std::vector>::operator()(jni::JNIEnv& env, const std::vector& value) const { + auto result = jni::Array::New(env, value.size()); + + for (std::size_t i = 0; i < value.size(); i++) { + result.Set(env, i, jni::Box(env, value.at(i))); + } + + return result; +} + +} // namespace conversion +} // namespace style +} // namespace mbgl diff --git a/src/conversion/constant.hpp b/src/conversion/constant.hpp new file mode 100644 index 000000000..839e6e84d --- /dev/null +++ b/src/conversion/constant.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include "conversion.hpp" + +#include +#include + +#include + +#include + +#include +#include +#include + +namespace mbgl { +namespace android { +namespace conversion { + +template <> +struct Converter>, bool> { + Result>> operator()(jni::JNIEnv& env, const bool& value) const; +}; + +template <> +struct Converter>, float> { + Result>> operator()(jni::JNIEnv& env, const float& value) const; +}; + +template <> +struct Converter>, double> { + Result>> operator()(jni::JNIEnv& env, const double& value) const; +}; + +/** + * All integrals. java is limited to 64 bit signed, so... + * TODO: use BigDecimal for > 64 / unsigned? + */ +template +struct Converter>, T, typename std::enable_if::value>::type> { + Result>> operator()(jni::JNIEnv& env, const T& value) const { + return jni::Box(env, jni::jlong(value)); + } +}; + +// TODO: convert integral types to primitive jni types + +template <> +struct Converter>, std::string> { + Result>> operator()(jni::JNIEnv& env, const std::string& value) const; +}; + +template <> +struct Converter>, Color> { + Result>> operator()(jni::JNIEnv& env, const Color& value) const; +}; + +template <> +struct Converter>, style::expression::Formatted> { + Result>> operator()(jni::JNIEnv& env, const style::expression::Formatted& value) const; +}; + +template +struct Converter>, std::array> { + Result>> operator()(jni::JNIEnv& env, const std::array& value) const { + std::vector v; + for (const float& id : value) { + v.push_back(id); + } + return convert>, std::vector>(env, v); + } +}; + +template <> +struct Converter>, std::vector> { + Result>> operator()(jni::JNIEnv& env, const std::vector& value) const; +}; + +template <> +struct Converter>, std::vector> { + Result>> operator()(jni::JNIEnv& env, const std::vector& value) const; +}; + +template +struct Converter>, T, typename std::enable_if_t::value>> { + Result>> operator()(jni::JNIEnv& env, const T& value) const { + return convert>, std::string>(env, Enum::toString(value)); + } +}; + +template +struct Converter>, std::vector, typename std::enable_if_t::value>> { + Result>> operator()(jni::JNIEnv& env, const std::vector& value) const { + auto result = jni::Array::New(env, value.size()); + for (std::size_t i = 0; i < value.size(); ++i) { + result.Set(env, i, jni::Make(env, Enum::toString(value.at(i)))); + } + return result; + } +}; + +} // namespace conversion +} // namespace android +} // namespace mbgl diff --git a/src/conversion/conversion.hpp b/src/conversion/conversion.hpp new file mode 100644 index 000000000..d1766f975 --- /dev/null +++ b/src/conversion/conversion.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include + +#include + +namespace mbgl { +namespace android { +namespace conversion { + +struct Error { std::string message; }; + +template +class Result : private variant { +public: + using variant::variant; + + explicit operator bool() const { + return this->template is(); + } + + T& operator*() { + assert(this->template is()); + return this->template get(); + } + + const T& operator*() const { + assert(this->template is()); + return this->template get(); + } + + const Error& error() const { + assert(this->template is()); + return this->template get(); + } +}; + +template +struct Converter; + +template +Result convert(jni::JNIEnv& env, const V& value, Args&&...args) { + return Converter()(env, value, std::forward(args)...); +} + +} // namespace conversion +} // namespace android +} // namespace mbgl diff --git a/src/example_custom_layer.cpp b/src/example_custom_layer.cpp new file mode 100644 index 000000000..e80553254 --- /dev/null +++ b/src/example_custom_layer.cpp @@ -0,0 +1,238 @@ +#include +#include +#include +#include +#include + +// DEBUGGING + +const char* LOG_TAG = "Custom Layer Example"; + +const char* stringFromError(GLenum err) { + switch (err) { + case GL_INVALID_ENUM: + return "GL_INVALID_ENUM"; + + case GL_INVALID_VALUE: + return "GL_INVALID_VALUE"; + + case GL_INVALID_OPERATION: + return "GL_INVALID_OPERATION"; + + case GL_INVALID_FRAMEBUFFER_OPERATION: + return "GL_INVALID_FRAMEBUFFER_OPERATION"; + + case GL_OUT_OF_MEMORY: + return "GL_OUT_OF_MEMORY"; + +#ifdef GL_TABLE_TOO_LARGE + case GL_TABLE_TOO_LARGE: + return "GL_TABLE_TOO_LARGE"; +#endif + +#ifdef GL_STACK_OVERFLOW + case GL_STACK_OVERFLOW: + return "GL_STACK_OVERFLOW"; +#endif + +#ifdef GL_STACK_UNDERFLOW + case GL_STACK_UNDERFLOW: + return "GL_STACK_UNDERFLOW"; +#endif + +#ifdef GL_CONTEXT_LOST + case GL_CONTEXT_LOST: + return "GL_CONTEXT_LOST"; +#endif + + default: + return "GL_UNKNOWN"; + } +} + +struct Error : std::runtime_error { + using std::runtime_error::runtime_error; +}; + +void checkError(const char *cmd, const char *file, int line) { + + GLenum err = GL_NO_ERROR; + if ((err = glGetError()) != GL_NO_ERROR) { + std::ostringstream message; + message << cmd << ": Error " << stringFromError(err); + + // Check for further errors + while ((err = glGetError()) != GL_NO_ERROR) { + message << ", " << stringFromError(err); + } + + message << " at " << file << ":" << line; + __android_log_write(ANDROID_LOG_ERROR, LOG_TAG, message.str().c_str()); + throw Error(message.str()); + } +} + +#ifndef NDEBUG +#define GL_CHECK_ERROR(cmd) ([&]() { struct __MBGL_C_E { ~__MBGL_C_E() noexcept(false) { checkError(#cmd, __FILE__, __LINE__); } } __MBGL_C_E; return cmd; }()) +#else +#define GL_CHECK_ERROR(cmd) (cmd) +#endif + +void checkLinkStatus(GLuint program) { + GLint isLinked = 0; + glGetProgramiv(program, GL_LINK_STATUS, &isLinked); + if (isLinked == GL_FALSE) { + GLint maxLength = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &maxLength); + GLchar infoLog[maxLength]; + glGetProgramInfoLog(program, maxLength, &maxLength, &infoLog[0]); + __android_log_write(ANDROID_LOG_ERROR, LOG_TAG, &infoLog[0]); + throw Error(infoLog); + } + +} + +void checkCompileStatus(GLuint shader) { + GLint isCompiled = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled); + if (isCompiled == GL_FALSE) { + GLint maxLength = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength); + + // The maxLength includes the NULL character + GLchar errorLog[maxLength]; + glGetShaderInfoLog(shader, maxLength, &maxLength, &errorLog[0]); + __android_log_write(ANDROID_LOG_ERROR, LOG_TAG, &errorLog[0]); + throw Error(errorLog); + } +} + +// /DEBUGGING + +static const GLchar * vertexShaderSource = "attribute vec2 a_pos; void main() { gl_Position = vec4(a_pos, 0, 1); }"; +static const GLchar * fragmentShaderSource = "uniform highp vec4 fill_color; void main() { gl_FragColor = fill_color; }"; + +class ExampleCustomLayer: mbgl::style::CustomLayerHost { +public: + ~ExampleCustomLayer() { + } + + void initialize() { + __android_log_write(ANDROID_LOG_INFO, LOG_TAG, "Initialize"); + + // Debug info + int maxAttrib; + GL_CHECK_ERROR(glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxAttrib)); + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Max vertex attributes: %i", maxAttrib); + + program = GL_CHECK_ERROR(glCreateProgram()); + vertexShader = GL_CHECK_ERROR(glCreateShader(GL_VERTEX_SHADER)); + fragmentShader = GL_CHECK_ERROR(glCreateShader(GL_FRAGMENT_SHADER)); + + GL_CHECK_ERROR(glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr)); + GL_CHECK_ERROR(glCompileShader(vertexShader)); + checkCompileStatus(vertexShader); + GL_CHECK_ERROR(glAttachShader(program, vertexShader)); + GL_CHECK_ERROR(glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr)); + GL_CHECK_ERROR(glCompileShader(fragmentShader)); + checkCompileStatus(fragmentShader); + GL_CHECK_ERROR(glAttachShader(program, fragmentShader)); + GL_CHECK_ERROR(glLinkProgram(program)); + checkLinkStatus(program); + + a_pos = GL_CHECK_ERROR(glGetAttribLocation(program, "a_pos")); + fill_color = GL_CHECK_ERROR(glGetUniformLocation(program, "fill_color")); + + GLfloat background[] = { -1, -1, 1, -1, -1, 1, 1, 1 }; + GL_CHECK_ERROR(glGenBuffers(1, &buffer)); + GL_CHECK_ERROR(glBindBuffer(GL_ARRAY_BUFFER, buffer)); + GL_CHECK_ERROR(glBufferData(GL_ARRAY_BUFFER, 8 * sizeof(GLfloat), background, GL_STATIC_DRAW)); + } + + void render(const mbgl::style::CustomLayerRenderParameters&) { + __android_log_write(ANDROID_LOG_INFO, LOG_TAG, "Render"); + glUseProgram(program); + glBindBuffer(GL_ARRAY_BUFFER, buffer); + glEnableVertexAttribArray(a_pos); + glVertexAttribPointer(a_pos, 2, GL_FLOAT, GL_FALSE, 0, NULL); + glDisable(GL_STENCIL_TEST); + glDisable(GL_DEPTH_TEST); + glUniform4fv(fill_color, 1, color); + + GL_CHECK_ERROR(glUseProgram(program)); + GL_CHECK_ERROR(glBindBuffer(GL_ARRAY_BUFFER, buffer)); + GL_CHECK_ERROR(glEnableVertexAttribArray(a_pos)); + GL_CHECK_ERROR(glVertexAttribPointer(a_pos, 2, GL_FLOAT, GL_FALSE, 0, NULL)); + GL_CHECK_ERROR(glDisable(GL_STENCIL_TEST)); + GL_CHECK_ERROR(glDisable(GL_DEPTH_TEST)); + GL_CHECK_ERROR(glUniform4fv(fill_color, 1, color)); + GL_CHECK_ERROR(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)); + + } + + void contextLost() { + __android_log_write(ANDROID_LOG_INFO, LOG_TAG, "ContextLost"); + program = 0; + } + + void deinitialize() { + __android_log_write(ANDROID_LOG_INFO, LOG_TAG, "DeInitialize"); + if (program) { + glDeleteBuffers(1, &buffer); + glDetachShader(program, vertexShader); + glDetachShader(program, fragmentShader); + glDeleteShader(vertexShader); + glDeleteShader(fragmentShader); + glDeleteProgram(program); + } + } + + GLuint program = 0; + GLuint vertexShader = 0; + GLuint fragmentShader = 0; + GLuint buffer = 0; + GLuint a_pos = 0; + GLuint fill_color = 0; + + static GLfloat color[]; +}; + +GLfloat ExampleCustomLayer::color[] = { 0.0f, 1.0f, 0.0f, 1.0f }; + +jlong JNICALL nativeCreateContext(JNIEnv*, jobject) { + __android_log_write(ANDROID_LOG_INFO, LOG_TAG, "nativeCreateContext"); + auto exampleCustomLayer = std::make_unique(); + return reinterpret_cast(exampleCustomLayer.release()); +} + +void JNICALL nativeSetColor(JNIEnv*, jobject, jfloat red, jfloat green, jfloat blue, jfloat alpha) { + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "nativeSetColor: %.2f, %.2f, %.2f, %.2f", red, green, blue, alpha); + ExampleCustomLayer::color[0] = red; + ExampleCustomLayer::color[1] = green; + ExampleCustomLayer::color[2] = blue; + ExampleCustomLayer::color[3] = alpha; +} + +extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { + __android_log_write(ANDROID_LOG_INFO, LOG_TAG, "OnLoad"); + + JNIEnv *env = nullptr; + vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); + + jclass customLayerClass = env->FindClass("com/mapbox/mapboxsdk/testapp/model/customlayer/ExampleCustomLayer"); + + JNINativeMethod methods[] = { + {"createContext", "()J", reinterpret_cast(&nativeCreateContext)}, + {"setColor", "(FFFF)V", reinterpret_cast(&nativeSetColor)} + }; + + if (env->RegisterNatives(customLayerClass, methods, 2) < 0) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + return JNI_VERSION_1_6; +} + +extern "C" JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *, void *) { +} diff --git a/src/file_source.cpp b/src/file_source.cpp new file mode 100644 index 000000000..05b57e5d1 --- /dev/null +++ b/src/file_source.cpp @@ -0,0 +1,202 @@ +#include "file_source.hpp" +#include "attach_env.hpp" + +#include +#include +#include +#include +#include + +#include + +#include "asset_manager_file_source.hpp" + +namespace mbgl { + +std::shared_ptr FileSource::createPlatformFileSource(const ResourceOptions& options) { + auto* assetFileSource = reinterpret_cast(options.platformContext()); + auto fileSource = std::make_shared(options.cachePath(), std::unique_ptr(assetFileSource)); + fileSource->setAccessToken(options.accessToken()); + return fileSource; +} + +namespace android { + +// FileSource // + +FileSource::FileSource(jni::JNIEnv& _env, + const jni::String& accessToken, + const jni::String& _cachePath, + const jni::Object& assetManager) { + std::string path = jni::Make(_env, _cachePath); + mapbox::sqlite::setTempPath(path); + + resourceOptions + .withAccessToken(accessToken ? jni::Make(_env, accessToken) : "") + .withCachePath(path + DATABASE_FILE) + .withPlatformContext(reinterpret_cast(new AssetManagerFileSource(_env, assetManager))); + + // Create a core default file source + fileSource = std::static_pointer_cast(mbgl::FileSource::getSharedFileSource(resourceOptions)); +} + +FileSource::~FileSource() { +} + +jni::Local FileSource::getAccessToken(jni::JNIEnv& env) { + return jni::Make(env, fileSource->getAccessToken()); +} + +void FileSource::setAccessToken(jni::JNIEnv& env, const jni::String& token) { + fileSource->setAccessToken(token ? jni::Make(env, token) : ""); +} + +void FileSource::setAPIBaseUrl(jni::JNIEnv& env, const jni::String& url) { + fileSource->setAPIBaseURL(jni::Make(env, url)); +} + +void FileSource::setResourceTransform(jni::JNIEnv& env, const jni::Object& transformCallback) { + if (transformCallback) { + auto global = jni::NewGlobal(env, transformCallback); + resourceTransform = std::make_unique>(*Scheduler::GetCurrent(), + // Capture the ResourceTransformCallback object as a managed global into + // the lambda. It is released automatically when we're setting a new ResourceTransform in + // a subsequent call. + // Note: we're converting it to shared_ptr because this lambda is converted to a std::function, + // which requires copyability of its captured variables. + [callback = std::make_shared(std::move(global))] + (mbgl::Resource::Kind kind, const std::string&& url_) { + android::UniqueEnv _env = android::AttachEnv(); + return FileSource::ResourceTransformCallback::onURL(*_env, *callback, int(kind), url_); + }); + fileSource->setResourceTransform(resourceTransform->self()); + } else { + // Reset the callback + resourceTransform.reset(); + fileSource->setResourceTransform({}); + } +} + +void FileSource::setResourceCachePath(jni::JNIEnv& env, const jni::String& path, + const jni::Object& _callback) { + if (pathChangeCallback) { + FileSource::ResourcesCachePathChangeCallback::onError(env, _callback, jni::Make(env, "Another resources cache path change is in progress")); + return; + } + + std::string newPath = jni::Make(env, path); + mapbox::sqlite::setTempPath(newPath); + + auto global = jni::NewGlobal(env, _callback); + pathChangeCallback = std::make_unique>(*Scheduler::GetCurrent(), + [this, callback = std::make_shared(std::move(global)), newPath] { + android::UniqueEnv _env = android::AttachEnv(); + FileSource::ResourcesCachePathChangeCallback::onSuccess(*_env, *callback, jni::Make(*_env, newPath)); + pathChangeCallback.reset(); + }); + + fileSource->setResourceCachePath(newPath + DATABASE_FILE, pathChangeCallback->self()); +} + +void FileSource::resume(jni::JNIEnv&) { + if (!activationCounter) { + activationCounter = optional(1) ; + return; + } + + activationCounter.value()++; + if (activationCounter == 1) { + fileSource->resume(); + } +} + +void FileSource::pause(jni::JNIEnv&) { + if (activationCounter) { + activationCounter.value()--; + if (activationCounter == 0) { + fileSource->pause(); + } + } +} + +jni::jboolean FileSource::isResumed(jni::JNIEnv&) { + if (activationCounter) { + return (jboolean) (activationCounter > 0); + } + return (jboolean) false; +} + +FileSource* FileSource::getNativePeer(jni::JNIEnv& env, const jni::Object& jFileSource) { + static auto& javaClass = jni::Class::Singleton(env); + static auto field = javaClass.GetField(env, "nativePtr"); + return reinterpret_cast(jFileSource.Get(env, field)); +} + +mbgl::ResourceOptions FileSource::getSharedResourceOptions(jni::JNIEnv& env, const jni::Object& jFileSource) { + FileSource* fileSource = FileSource::getNativePeer(env, jFileSource); + assert(fileSource != nullptr); + return fileSource->resourceOptions.clone(); +} + +// FileSource::ResourcesCachePathChangeCallback // + +void FileSource::ResourcesCachePathChangeCallback::onSuccess(jni::JNIEnv& env, + const jni::Object& callback, + const jni::String& path) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onSuccess"); + + callback.Call(env, method, path); +} + +void FileSource::ResourcesCachePathChangeCallback::onError(jni::JNIEnv& env, + const jni::Object& callback, + const jni::String& message) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onError"); + + callback.Call(env, method, message); +} + +void FileSource::registerNative(jni::JNIEnv& env) { + // Ensure the classes are cached. If they're requested for the + // first time on a background thread, Android's class loader heuristics will fail. + // https://developer.android.com/training/articles/perf-jni#faq_FindClass + jni::Class::Singleton(env); + jni::Class::Singleton(env); + + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer&>, + "initialize", + "finalize", + METHOD(&FileSource::getAccessToken, "getAccessToken"), + METHOD(&FileSource::setAccessToken, "setAccessToken"), + METHOD(&FileSource::setAPIBaseUrl, "setApiBaseUrl"), + METHOD(&FileSource::setResourceTransform, "setResourceTransform"), + METHOD(&FileSource::setResourceCachePath, "setResourceCachePath"), + METHOD(&FileSource::resume, "activate"), + METHOD(&FileSource::pause, "deactivate"), + METHOD(&FileSource::isResumed, "isActivated") + ); +} + + +// FileSource::ResourceTransformCallback // + +std::string FileSource::ResourceTransformCallback::onURL(jni::JNIEnv& env, const jni::Object& callback, int kind, std::string url_) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onURL"); + + return jni::Make(env, + callback.Call(env, method, kind, + jni::Make(env, url_))); +} + +} // namespace android +} // namespace mbgl diff --git a/src/file_source.hpp b/src/file_source.hpp new file mode 100644 index 000000000..f3ad33eb3 --- /dev/null +++ b/src/file_source.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +#include + +#include "asset_manager.hpp" + +#include + +namespace mbgl { + +template class Actor; +class ResourceTransform; +using mbgl::PathChangeCallback; + +namespace android { + +/** + * Peer class for the Android FileSource holder. Ensures that a single DefaultFileSource is used + */ +class FileSource { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/storage/FileSource"; }; + + struct ResourceTransformCallback { + static constexpr auto Name() { return "com/mapbox/mapboxsdk/storage/FileSource$ResourceTransformCallback"; } + + static std::string onURL(jni::JNIEnv&, const jni::Object&, int, std::string); + }; + + struct ResourcesCachePathChangeCallback { + static constexpr auto Name() { return "com/mapbox/mapboxsdk/storage/FileSource$ResourcesCachePathChangeCallback";} + + static void onSuccess(jni::JNIEnv&, + const jni::Object&, + const jni::String&); + + static void onError(jni::JNIEnv&, + const jni::Object&, + const jni::String&); + }; + + FileSource(jni::JNIEnv&, const jni::String&, const jni::String&, const jni::Object&); + + ~FileSource(); + + jni::Local getAccessToken(jni::JNIEnv&); + + void setAccessToken(jni::JNIEnv&, const jni::String&); + + void setAPIBaseUrl(jni::JNIEnv&, const jni::String&); + + void setResourceTransform(jni::JNIEnv&, const jni::Object&); + + void setResourceCachePath(jni::JNIEnv&, const jni::String&, const jni::Object&); + + void resume(jni::JNIEnv&); + + void pause(jni::JNIEnv&); + + jni::jboolean isResumed(jni::JNIEnv&); + + static FileSource* getNativePeer(jni::JNIEnv&, const jni::Object&); + + static mbgl::ResourceOptions getSharedResourceOptions(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); + +private: + const std::string DATABASE_FILE = "/mbgl-offline.db"; + optional activationCounter; + mbgl::ResourceOptions resourceOptions; + std::unique_ptr> resourceTransform; + std::unique_ptr> pathChangeCallback; + std::shared_ptr fileSource; +}; + + +} // namespace android +} // namespace mbgl diff --git a/src/geojson/feature.cpp b/src/geojson/feature.cpp new file mode 100644 index 000000000..8d30404a5 --- /dev/null +++ b/src/geojson/feature.cpp @@ -0,0 +1,79 @@ +#include "feature.hpp" +#include "geometry.hpp" +#include "../gson/json_object.hpp" + +#include + +namespace mbgl { +namespace android { +namespace geojson { + +using namespace gson; + +mbgl::Feature Feature::convert(jni::JNIEnv& env, const jni::Object& jFeature) { + static auto& javaClass = jni::Class::Singleton(env); + static auto id = javaClass.GetMethod(env, "id"); + static auto geometry = javaClass.GetMethod ()>(env, "geometry"); + static auto properties = javaClass.GetMethod ()>(env, "properties"); + + auto jId = jFeature.Call(env, id); + + using mbid = mapbox::feature::identifier; + + return mbgl::Feature { + Geometry::convert(env, jFeature.Call(env, geometry)), + JsonObject::convert(env, jFeature.Call(env, properties)), + jId ? mbid { jni::Make(env, jId) } : mbid { mapbox::feature::null_value } + }; +} + +/** + * Turn feature identifier into std::string + */ +class FeatureIdVisitor { +public: + template + std::string operator()(const T& i) const { + return util::toString(i); + } + + std::string operator()(const std::string& i) const { + return i; + } + + std::string operator()(const std::nullptr_t&) const { + return {}; + } + + std::string operator()(const mapbox::feature::null_value_t&) const { + return {}; + } +}; + +jni::Local> convertFeature(jni::JNIEnv& env, const mbgl::Feature& value) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jni::Object, jni::Object, jni::String)>(env, "fromGeometry"); + + return javaClass.Call(env, method, + Geometry::New(env, value.geometry), + JsonObject::New(env, value.properties), + jni::Make(env, value.id.is() ? std::string {} : value.id.match(FeatureIdVisitor()))); +} + +jni::Local>> Feature::convert(jni::JNIEnv& env, const std::vector& value) { + auto features = jni::Array>::New(env, value.size()); + + for (size_t i = 0; i < value.size(); i = i + 1) { + features.Set(env, i, convertFeature(env, value.at(i))); + } + + return features; +} + +void Feature::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace geojson +} // namespace android +} // namespace mbgl diff --git a/src/geojson/feature.hpp b/src/geojson/feature.hpp new file mode 100644 index 000000000..fdf5d977b --- /dev/null +++ b/src/geojson/feature.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include + +namespace mbgl { +namespace android { +namespace geojson { + +class Feature { +public: + static constexpr auto Name() { return "com/mapbox/geojson/Feature"; }; + + static mbgl::Feature convert(jni::JNIEnv&, const jni::Object&); + static jni::Local>> convert(jni::JNIEnv&, const std::vector&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/feature_collection.cpp b/src/geojson/feature_collection.cpp new file mode 100644 index 000000000..f90d5226b --- /dev/null +++ b/src/geojson/feature_collection.cpp @@ -0,0 +1,37 @@ +#include "feature_collection.hpp" + +#include "feature.hpp" + +namespace mbgl { +namespace android { +namespace geojson { + +mbgl::FeatureCollection FeatureCollection::convert(jni::JNIEnv& env, const jni::Object& jCollection) { + auto collection = mbgl::FeatureCollection(); + + if (jCollection) { + auto jFeatureList = FeatureCollection::features(env, jCollection); + auto jFeatures = java::util::List::toArray(env, jFeatureList); + auto size = size_t(jFeatures.Length(env)); + collection.reserve(size); + + for (size_t i = 0; i < size; i++) { + collection.push_back(Feature::convert(env, jFeatures.Get(env, i))); + } + } + return collection; +} + +jni::Local> FeatureCollection::features(jni::JNIEnv& env, const jni::Object& jCollection) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod ()>(env, "features"); + return jCollection.Call(env, method); +} + +void FeatureCollection::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/feature_collection.hpp b/src/geojson/feature_collection.hpp new file mode 100644 index 000000000..66e92eaca --- /dev/null +++ b/src/geojson/feature_collection.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "../java/util.hpp" + +#include + +#include + +namespace mbgl { +namespace android { +namespace geojson { + +class FeatureCollection { +public: + static constexpr auto Name() { return "com/mapbox/geojson/FeatureCollection"; }; + + static mbgl::FeatureCollection convert(jni::JNIEnv&, const jni::Object&); + + static jni::Local> features(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/geometry.cpp b/src/geojson/geometry.cpp new file mode 100644 index 000000000..0598b9fc5 --- /dev/null +++ b/src/geojson/geometry.cpp @@ -0,0 +1,97 @@ +#include "geometry.hpp" + +#include "point.hpp" +#include "multi_point.hpp" +#include "line_string.hpp" +#include "multi_line_string.hpp" +#include "polygon.hpp" +#include "multi_polygon.hpp" +#include "geometry_collection.hpp" + +#include + +namespace mbgl { +namespace android { +namespace geojson { + +/** + * Turn mapbox::geometry type into Java GeoJson Geometries + */ +class GeometryEvaluator { +public: + + jni::JNIEnv& env; + + jni::Local> operator()(const mbgl::EmptyGeometry &) const { + // FIXME: mapbox-java needs to have its own Empty type. + return GeometryCollection::New(env, {}); + } + + jni::Local> operator()(const mbgl::Point &geometry) const { + return Point::New(env, geometry); + } + + jni::Local> operator()(const mbgl::LineString &geometry) const { + return LineString::New(env, geometry); + } + + jni::Local> operator()(const mbgl::MultiLineString &geometry) const { + return MultiLineString::New(env, geometry); + } + + jni::Local> operator()(const mbgl::MultiPoint &geometry) const { + return MultiPoint::New(env, geometry); + } + + jni::Local> operator()(const mbgl::Polygon &geometry) const { + return Polygon::New(env, geometry); + } + + jni::Local> operator()(const mbgl::MultiPolygon &geometry) const { + return MultiPolygon::New(env, geometry); + } + + jni::Local> operator()(const mapbox::geometry::geometry_collection &geometry) const { + return GeometryCollection::New(env, geometry); + } +}; + +jni::Local> Geometry::New(jni::JNIEnv& env, mbgl::Geometry geometry) { + GeometryEvaluator evaluator { env } ; + return mbgl::Geometry::visit(geometry, evaluator); +} + +mbgl::Geometry Geometry::convert(jni::JNIEnv &env, const jni::Object& jGeometry) { + auto type = Geometry::getType(env, jGeometry); + if (type == Point::Type()) { + return { Point::convert(env, jni::Cast(env, jni::Class::Singleton(env), jGeometry)) }; + } else if (type == MultiPoint::Type()) { + return { MultiPoint::convert(env, jni::Cast(env, jni::Class::Singleton(env), jGeometry)) }; + } else if (type == LineString::Type()) { + return { LineString::convert(env, jni::Cast(env, jni::Class::Singleton(env), jGeometry)) }; + } else if (type == MultiLineString::Type()) { + return { MultiLineString::convert(env, jni::Cast(env, jni::Class::Singleton(env), jGeometry)) }; + } else if (type == Polygon::Type()) { + return { Polygon::convert(env, jni::Cast(env, jni::Class::Singleton(env), jGeometry)) }; + } else if (type == MultiPolygon::Type()) { + return { MultiPolygon::convert(env, jni::Cast(env, jni::Class::Singleton(env), jGeometry)) }; + } else if (type == GeometryCollection::Type()) { + return { GeometryCollection::convert(env, jni::Cast(env, jni::Class::Singleton(env), jGeometry)) }; + } + + throw std::runtime_error(std::string {"Unsupported GeoJSON type: " } + type); +} + +std::string Geometry::getType(jni::JNIEnv &env, const jni::Object& jGeometry) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "type"); + return jni::Make(env, jGeometry.Call(env, method)); +} + +void Geometry::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace geojson +} // namespace android +} // namespace mbgl diff --git a/src/geojson/geometry.hpp b/src/geojson/geometry.hpp new file mode 100644 index 000000000..4bf55c858 --- /dev/null +++ b/src/geojson/geometry.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "../java/util.hpp" + +#include + +namespace mbgl { +namespace android { +namespace geojson { + +class Geometry { +public: + static constexpr auto Name() { return "com/mapbox/geojson/Geometry"; }; + + static jni::Local> New(jni::JNIEnv&, mbgl::Geometry); + + static mbgl::Geometry convert(jni::JNIEnv&, const jni::Object&); + + static std::string getType(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/geometry_collection.cpp b/src/geojson/geometry_collection.cpp new file mode 100644 index 000000000..cca909126 --- /dev/null +++ b/src/geojson/geometry_collection.cpp @@ -0,0 +1,47 @@ +#include "geometry_collection.hpp" +#include "../java/util.hpp" + +namespace mbgl { +namespace android { +namespace geojson { + +jni::Local> GeometryCollection::New(jni::JNIEnv& env, const mapbox::geometry::geometry_collection& collection) { + // Create an array of geometries + auto jarray = jni::Array>::New(env, collection.size()); + + for (size_t i = 0; i < collection.size(); i++) { + jarray.Set(env, i, Geometry::New(env, collection.at(i))); + } + + // create the GeometryCollection + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jni::Object)>(env, "fromGeometries"); + return javaClass.Call(env, method, java::util::Arrays::asList(env, jarray)); +} + +mapbox::geometry::geometry_collection GeometryCollection::convert(jni::JNIEnv &env, const jni::Object& jCollection) { + // Get geometries + static auto& javaClass = jni::Class::Singleton(env); + static auto getGeometries = javaClass.GetMethod ()>(env, "geometries"); + + // Turn into array + auto jarray = java::util::List::toArray(env, jCollection.Call(env, getGeometries)); + + // Convert each geometry + mapbox::geometry::geometry_collection collection{}; + + auto size = jarray.Length(env); + for (jni::jsize i = 0; i < size; i++) { + collection.push_back(Geometry::convert(env, jarray.Get(env, i))); + } + + return collection; +} + +void GeometryCollection::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/geometry_collection.hpp b/src/geojson/geometry_collection.hpp new file mode 100644 index 000000000..9f02ac848 --- /dev/null +++ b/src/geojson/geometry_collection.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "geometry.hpp" + +#include + +namespace mbgl { +namespace android { +namespace geojson { + +class GeometryCollection { +public: + using SuperTag = Geometry; + static constexpr auto Name() { return "com/mapbox/geojson/GeometryCollection"; }; + static constexpr auto Type() { return "GeometryCollection"; }; + + static jni::Local> New(jni::JNIEnv&, const mapbox::geometry::geometry_collection&); + + static mapbox::geometry::geometry_collection convert(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/line_string.cpp b/src/geojson/line_string.cpp new file mode 100644 index 000000000..ae1345c95 --- /dev/null +++ b/src/geojson/line_string.cpp @@ -0,0 +1,55 @@ +#include "line_string.hpp" +#include "point.hpp" +#include "util.hpp" +#include "../java/util.hpp" + +namespace mbgl { +namespace android { +namespace geojson { + +jni::Local> LineString::New(jni::JNIEnv& env, const mbgl::LineString& lineString) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod(jni::Object)>(env, "fromLngLats"); + + return javaClass.Call(env, method, asPointsList(env, lineString)); +} + +mapbox::geojson::line_string LineString::convert(jni::JNIEnv &env, const jni::Object& jLineString) { + mapbox::geojson::line_string lineString; + + if (jLineString) { + lineString = LineString::convert(env, LineString::coordinates(env, jLineString)); + } + + return lineString; +} + +mapbox::geojson::line_string LineString::convert(jni::JNIEnv &env, const jni::Object*/>& jPointList) { + mapbox::geojson::line_string lineString; + + if (jPointList) { + auto jPointArray = java::util::List::toArray(env, jPointList); + auto size = jPointArray.Length(env); + lineString.reserve(size); + + for (std::size_t i = 0; i < size; i++) { + lineString.push_back(Point::convert(env, jPointArray.Get(env, i))); + } + } + + return lineString; +} + +jni::Local> LineString::coordinates(jni::JNIEnv &env, const jni::Object& jLineString) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod ()>(env, "coordinates"); + return jLineString.Call(env, method); +} + +void LineString::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/line_string.hpp b/src/geojson/line_string.hpp new file mode 100644 index 000000000..906d809fa --- /dev/null +++ b/src/geojson/line_string.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +#include "geometry.hpp" +#include "../java/util.hpp" + +#include + +namespace mbgl { +namespace android { +namespace geojson { + + +class LineString { +public: + using SuperTag = Geometry; + static constexpr auto Name() { return "com/mapbox/geojson/LineString"; }; + static constexpr auto Type() { return "LineString"; }; + + static jni::Local> New(jni::JNIEnv&, const mbgl::LineString&); + + static mapbox::geojson::line_string convert(jni::JNIEnv&, const jni::Object&); + + static mapbox::geojson::line_string convert(jni::JNIEnv&, const jni::Object*/>&); + + static jni::Local> coordinates(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/multi_line_string.cpp b/src/geojson/multi_line_string.cpp new file mode 100644 index 000000000..1aaf53c01 --- /dev/null +++ b/src/geojson/multi_line_string.cpp @@ -0,0 +1,56 @@ +#include "multi_line_string.hpp" + +#include "line_string.hpp" +#include "util.hpp" + +namespace mbgl { +namespace android { +namespace geojson { + +jni::Local> MultiLineString::New(jni::JNIEnv& env, const mbgl::MultiLineString& multiLineString) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jni::Object)>(env, "fromLngLats"); + + return javaClass.Call(env, method, asPointsListsList(env, multiLineString)); +} + +mapbox::geojson::multi_line_string MultiLineString::convert(jni::JNIEnv &env, const jni::Object& jMultiLineString) { + mapbox::geojson::multi_line_string multiLineString; + + if (jMultiLineString) { + multiLineString = MultiLineString::convert(env, MultiLineString::coordinates(env, jMultiLineString)); + } + + return multiLineString; +} + +mapbox::geojson::multi_line_string MultiLineString::convert(jni::JNIEnv &env, const jni::Object>*/>& jPointListsList) { + mapbox::geojson::multi_line_string multiLineString; + + if (jPointListsList) { + auto jPositionListsArray = java::util::List::toArray(env, jPointListsList); + + auto size = jPositionListsArray.Length(env); + multiLineString.reserve(size); + + for (std::size_t i = 0; i < size; i++) { + multiLineString.push_back(LineString::convert(env, jPositionListsArray.Get(env, i))); + } + } + + return multiLineString; +} + +jni::Local> MultiLineString::coordinates(jni::JNIEnv &env, const jni::Object& jLineString) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod ()>(env, "coordinates"); + return jLineString.Call(env, method); +} + +void MultiLineString::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/multi_line_string.hpp b/src/geojson/multi_line_string.hpp new file mode 100644 index 000000000..65ce43bd1 --- /dev/null +++ b/src/geojson/multi_line_string.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "../java/util.hpp" +#include "geometry.hpp" + +#include + +namespace mbgl { +namespace android { +namespace geojson { + +class MultiLineString { +public: + using SuperTag = Geometry; + static constexpr auto Name() { return "com/mapbox/geojson/MultiLineString"; }; + static constexpr auto Type() { return "MultiLineString"; }; + + static jni::Local> New(jni::JNIEnv&, const mbgl::MultiLineString&); + + static mapbox::geojson::multi_line_string convert(jni::JNIEnv&, const jni::Object&); + + static mapbox::geojson::multi_line_string convert(jni::JNIEnv&, const jni::Object>*/>&); + + static jni::Local> coordinates(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/multi_point.cpp b/src/geojson/multi_point.cpp new file mode 100644 index 000000000..d530ef721 --- /dev/null +++ b/src/geojson/multi_point.cpp @@ -0,0 +1,41 @@ +#include "multi_point.hpp" + +#include "line_string.hpp" + +#include "util.hpp" + +namespace mbgl { +namespace android { +namespace geojson { + +jni::Local> MultiPoint::New(JNIEnv& env, const mbgl::MultiPoint& multiPoint) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod(jni::Object)>(env, "fromLngLats"); + + return javaClass.Call(env, method, asPointsList(env, multiPoint)); +} + +mapbox::geojson::multi_point MultiPoint::convert(jni::JNIEnv& env, const jni::Object& jMultiPoint) { + mapbox::geojson::multi_point multiPoint; + + if (jMultiPoint) { + multiPoint = convertExplicit( + LineString::convert(env, MultiPoint::coordinates(env, jMultiPoint))); + } + + return multiPoint; +} + +jni::Local> MultiPoint::coordinates(jni::JNIEnv& env, const jni::Object& jMultiPoint) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod ()>(env, "coordinates"); + return jMultiPoint.Call(env, method); +} + +void MultiPoint::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/multi_point.hpp b/src/geojson/multi_point.hpp new file mode 100644 index 000000000..a3ca49b73 --- /dev/null +++ b/src/geojson/multi_point.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +#include "geometry.hpp" +#include "../java/util.hpp" + +#include + +namespace mbgl { +namespace android { +namespace geojson { + +class MultiPoint { +public: + using SuperTag = Geometry; + static constexpr auto Name() { return "com/mapbox/geojson/MultiPoint"; }; + static constexpr auto Type() { return "MultiPoint"; }; + + static jni::Local> New(jni::JNIEnv&, const mbgl::MultiPoint&); + + static mapbox::geojson::multi_point convert(jni::JNIEnv&, const jni::Object&); + + static jni::Local> coordinates(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/multi_polygon.cpp b/src/geojson/multi_polygon.cpp new file mode 100644 index 000000000..3c5c3ec33 --- /dev/null +++ b/src/geojson/multi_polygon.cpp @@ -0,0 +1,53 @@ +#include "multi_polygon.hpp" + +#include "polygon.hpp" +#include "util.hpp" + +namespace mbgl { +namespace android { +namespace geojson { + +jni::Local> MultiPolygon::New(JNIEnv& env, const mbgl::MultiPolygon& multiPolygon) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jni::Object)>(env, "fromLngLats"); + + auto jarray = jni::Array>::New(env, multiPolygon.size()); + + for (size_t i = 0; i < multiPolygon.size(); i++) { + jarray.Set(env, i, asPointsListsList(env, multiPolygon.at(i))); + } + + return javaClass.Call(env, method, java::util::Arrays::asList(env, jarray)); +} + +mapbox::geojson::multi_polygon MultiPolygon::convert(jni::JNIEnv& env, const jni::Object& jMultiPolygon) { + mapbox::geojson::multi_polygon multiPolygon; + + if (jMultiPolygon) { + auto jPointListsListList = MultiPolygon::coordinates(env, jMultiPolygon); + auto jPointListsListArray = java::util::List::toArray(env, jPointListsListList); + + auto size = jPointListsListArray.Length(env); + multiPolygon.reserve(size); + + for (size_t i = 0; i < size; i++) { + multiPolygon.push_back(Polygon::convert(env, jPointListsListArray.Get(env, i))); + } + } + + return multiPolygon; +} + +jni::Local> MultiPolygon::coordinates(jni::JNIEnv& env, const jni::Object& jPolygon) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod ()>(env, "coordinates"); + return jPolygon.Call(env, method); +} + +void MultiPolygon::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/multi_polygon.hpp b/src/geojson/multi_polygon.hpp new file mode 100644 index 000000000..fd7b08016 --- /dev/null +++ b/src/geojson/multi_polygon.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "../java/util.hpp" +#include "geometry.hpp" + +#include + +namespace mbgl { +namespace android { +namespace geojson { + +class MultiPolygon { +public: + using SuperTag = Geometry; + static constexpr auto Name() { return "com/mapbox/geojson/MultiPolygon"; }; + static constexpr auto Type() { return "MultiPolygon"; }; + + static jni::Local> New(jni::JNIEnv&, const mbgl::MultiPolygon&); + + static mapbox::geojson::multi_polygon convert(jni::JNIEnv&, const jni::Object&); + + static jni::Local> coordinates(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/point.cpp b/src/geojson/point.cpp new file mode 100644 index 000000000..2000447c0 --- /dev/null +++ b/src/geojson/point.cpp @@ -0,0 +1,34 @@ +#include "point.hpp" + +namespace mbgl { +namespace android { +namespace geojson { + +jni::Local> Point::New(jni::JNIEnv& env, const mbgl::Point& point) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jni::jdouble, jni::jdouble)>(env, "fromLngLat"); + return javaClass.Call(env, method, point.x, point.y); +} + +mbgl::Point Point::convert(jni::JNIEnv &env, const jni::Object& jPoint) { + static auto& javaClass = jni::Class::Singleton(env); + static auto longitude = javaClass.GetMethod(env, "longitude"); + static auto latitude = javaClass.GetMethod(env, "latitude"); + + if (!jPoint) { + return {}; + } + + return { + jPoint.Call(env, longitude), + jPoint.Call(env, latitude) + }; +} + +void Point::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace geojson +} // namespace android +} // namespace mbgl diff --git a/src/geojson/point.hpp b/src/geojson/point.hpp new file mode 100644 index 000000000..b5d71ecae --- /dev/null +++ b/src/geojson/point.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "geometry.hpp" + +#include + +namespace mbgl { +namespace android { +namespace geojson { + +class Point { +public: + using SuperTag = Geometry; + static constexpr auto Name() { return "com/mapbox/geojson/Point"; }; + static constexpr auto Type() { return "Point"; }; + + static jni::Local> New(jni::JNIEnv&, const mbgl::Point&); + static mbgl::Point convert(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/polygon.cpp b/src/geojson/polygon.cpp new file mode 100644 index 000000000..4437d134a --- /dev/null +++ b/src/geojson/polygon.cpp @@ -0,0 +1,55 @@ +#include "polygon.hpp" + +#include "multi_line_string.hpp" + +#include "util.hpp" + +namespace mbgl { +namespace android { +namespace geojson { + +jni::Local> Polygon::New(jni::JNIEnv& env, const mbgl::Polygon& polygon) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jni::Object)>(env, "fromLngLats"); + + return javaClass.Call(env, method, asPointsListsList(env, polygon)); +} + +mapbox::geojson::polygon Polygon::convert(jni::JNIEnv &env, const jni::Object& jPolygon) { + mapbox::geojson::polygon polygon; + + if (jPolygon) { + polygon = Polygon::convert(env, Polygon::coordinates(env, jPolygon)); + } + + return polygon; +} + +mapbox::geojson::polygon Polygon::convert(jni::JNIEnv &env, const jni::Object>*/>& jPointListsList) { + mapbox::geojson::polygon polygon; + + if (jPointListsList) { + auto multiLine = MultiLineString::convert(env, jPointListsList); + polygon.reserve(multiLine.size()); + for (auto&& line : multiLine) { + polygon.emplace_back(convertExplicit(std::move(line))); + } + } + + return polygon; +} + + +jni::Local> Polygon::coordinates(jni::JNIEnv &env, const jni::Object& jPolygon) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod ()>(env, "coordinates"); + return jPolygon.Call(env, method); +} + +void Polygon::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/polygon.hpp b/src/geojson/polygon.hpp new file mode 100644 index 000000000..8e1761fed --- /dev/null +++ b/src/geojson/polygon.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include "geometry.hpp" +#include "../java/util.hpp" + +#include + + +namespace mbgl { +namespace android { +namespace geojson { + +class Polygon { +public: + using SuperTag = Geometry; + static constexpr auto Name() { return "com/mapbox/geojson/Polygon"; }; + static constexpr auto Type() { return "Polygon"; }; + + static jni::Local> New(jni::JNIEnv&, const mbgl::Polygon&); + + static mapbox::geojson::polygon convert(jni::JNIEnv &, const jni::Object&); + + static mapbox::geojson::polygon convert(jni::JNIEnv&, const jni::Object>*/>&); + + static jni::Local> coordinates(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv &); +}; + +} // namespace geojson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geojson/util.hpp b/src/geojson/util.hpp new file mode 100644 index 000000000..59154516f --- /dev/null +++ b/src/geojson/util.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "point.hpp" + +#include + +namespace mbgl { +namespace android { +namespace geojson { + +// Clang 3.8 fails to implicitly convert matching types, so we'll have to do it explicitly. +template +To convertExplicit(From&& src) { + static_assert(std::is_same::container_type, + typename To::container_type>::value, + "container types do not match"); + static_assert(std::is_rvalue_reference::value, + "argument must be rvalue reference"); + return *reinterpret_cast>(&src); +} + +/** + * Geometry -> List + */ +template +static jni::Local> asPointsList(jni::JNIEnv& env, const T& pointsList) { + auto jarray = jni::Array>::New(env, pointsList.size()); + + for (jni::jsize i = 0; i < pointsList.size(); i++) { + jarray.Set(env, i, Point::New(env, pointsList.at(i))); + } + + return java::util::Arrays::asList(env, jarray); +} + +/** + * Geometry -> List> + */ +template +static jni::Local> asPointsListsList(JNIEnv& env, const SHAPE& value) { + auto jarray = jni::Array>::New(env, value.size()); + + for (size_t i = 0; i < value.size(); i++) { + jarray.Set(env, i, asPointsList(env, value[i])); + } + + return java::util::Arrays::asList(env, jarray); +} + +} // namespace geojson +} // namespace android +} // namespace mbgl diff --git a/src/geometry/lat_lng.cpp b/src/geometry/lat_lng.cpp new file mode 100644 index 000000000..5d8313cb2 --- /dev/null +++ b/src/geometry/lat_lng.cpp @@ -0,0 +1,29 @@ +#include "lat_lng.hpp" + +namespace mbgl { +namespace android { + +jni::Local> LatLng::New(jni::JNIEnv& env, const mbgl::LatLng& latLng) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, latLng.latitude(), latLng.longitude()); +} + +mbgl::Point LatLng::getGeometry(jni::JNIEnv& env, const jni::Object& latLng) { + static auto& javaClass = jni::Class::Singleton(env); + static auto latitudeField = javaClass.GetField(env, "latitude"); + static auto longitudeField = javaClass.GetField(env, "longitude"); + return mbgl::Point(latLng.Get(env, longitudeField), latLng.Get(env, latitudeField)); +} + +mbgl::LatLng LatLng::getLatLng(jni::JNIEnv& env, const jni::Object& latLng) { + auto point = LatLng::getGeometry(env, latLng); + return mbgl::LatLng(point.y, point.x); +} + +void LatLng::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geometry/lat_lng.hpp b/src/geometry/lat_lng.hpp new file mode 100644 index 000000000..30b7db1ef --- /dev/null +++ b/src/geometry/lat_lng.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +#include + +namespace mbgl { +namespace android { + +class LatLng : private mbgl::util::noncopyable { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/geometry/LatLng"; }; + + static jni::Local> New(jni::JNIEnv&, const mbgl::LatLng&); + + static mbgl::Point getGeometry(jni::JNIEnv&, const jni::Object&); + + static mbgl::LatLng getLatLng(jni::JNIEnv&, const jni::Object&); + + + static void registerNative(jni::JNIEnv&); +}; + + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geometry/lat_lng_bounds.cpp b/src/geometry/lat_lng_bounds.cpp new file mode 100644 index 000000000..cf534a06e --- /dev/null +++ b/src/geometry/lat_lng_bounds.cpp @@ -0,0 +1,30 @@ +#include "lat_lng_bounds.hpp" + +namespace mbgl { +namespace android { + +jni::Local> LatLngBounds::New(jni::JNIEnv& env, mbgl::LatLngBounds bounds) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, bounds.north(), bounds.east(), bounds.south(), bounds.west()); +} + +mbgl::LatLngBounds LatLngBounds::getLatLngBounds(jni::JNIEnv& env, const jni::Object& bounds) { + static auto& javaClass = jni::Class::Singleton(env); + static auto swLatField = javaClass.GetField(env, "latitudeSouth"); + static auto swLonField = javaClass.GetField(env, "longitudeWest"); + static auto neLatField = javaClass.GetField(env, "latitudeNorth"); + static auto neLonField = javaClass.GetField(env, "longitudeEast"); + + mbgl::LatLng sw = { bounds.Get(env, swLatField), bounds.Get(env, swLonField) }; + mbgl::LatLng ne = { bounds.Get(env, neLatField), bounds.Get(env, neLonField) }; + + return mbgl::LatLngBounds::hull(sw, ne); +} + +void LatLngBounds::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geometry/lat_lng_bounds.hpp b/src/geometry/lat_lng_bounds.hpp new file mode 100644 index 000000000..d51026711 --- /dev/null +++ b/src/geometry/lat_lng_bounds.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +#include + +namespace mbgl { +namespace android { + +class LatLngBounds : private mbgl::util::noncopyable { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/geometry/LatLngBounds"; }; + + static jni::Local> New(jni::JNIEnv&, mbgl::LatLngBounds); + + static mbgl::LatLngBounds getLatLngBounds(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geometry/lat_lng_quad.cpp b/src/geometry/lat_lng_quad.cpp new file mode 100644 index 000000000..1a1e49048 --- /dev/null +++ b/src/geometry/lat_lng_quad.cpp @@ -0,0 +1,37 @@ +#include "lat_lng_quad.hpp" +#include "lat_lng.hpp" + +namespace mbgl { +namespace android { + +jni::Local> LatLngQuad::New(jni::JNIEnv& env, std::array coordinates) { + static auto& javaClass = jni::Class::Singleton(env); + static auto quadConstructor = javaClass.GetConstructor, jni::Object, jni::Object, jni::Object>(env); + return javaClass.New(env, quadConstructor, + LatLng::New(env, coordinates[0]), + LatLng::New(env, coordinates[1]), + LatLng::New(env, coordinates[2]), + LatLng::New(env, coordinates[3])); +} + +std::array LatLngQuad::getLatLngArray(jni::JNIEnv& env, const jni::Object& quad) { + static auto& javaClass = jni::Class::Singleton(env); + static auto topLeftField = javaClass.GetField >(env, "topLeft"); + static auto topRightField = javaClass.GetField >(env, "topRight"); + static auto bottomRightField = javaClass.GetField >(env, "bottomRight"); + static auto bottomLeftField = javaClass.GetField >(env, "bottomLeft"); + + return std::array < mbgl::LatLng, 4 > {{ + LatLng::getLatLng(env, quad.Get(env, topLeftField)), + LatLng::getLatLng(env, quad.Get(env, topRightField)), + LatLng::getLatLng(env, quad.Get(env, bottomRightField)), + LatLng::getLatLng(env, quad.Get(env, bottomLeftField)) + }}; +} + +void LatLngQuad::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geometry/lat_lng_quad.hpp b/src/geometry/lat_lng_quad.hpp new file mode 100644 index 000000000..f6560b57b --- /dev/null +++ b/src/geometry/lat_lng_quad.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace mbgl { +namespace android { + +class LatLngQuad : private mbgl::util::noncopyable { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/geometry/LatLngQuad"; }; + + static jni::Local> New(jni::JNIEnv&, std::array); + + static std::array getLatLngArray(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geometry/projected_meters.cpp b/src/geometry/projected_meters.cpp new file mode 100644 index 000000000..e079de006 --- /dev/null +++ b/src/geometry/projected_meters.cpp @@ -0,0 +1,17 @@ +#include "projected_meters.hpp" + +namespace mbgl { +namespace android { + +jni::Local> ProjectedMeters::New(jni::JNIEnv& env, double northing, double easting) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, northing, easting); +} + +void ProjectedMeters::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/geometry/projected_meters.hpp b/src/geometry/projected_meters.hpp new file mode 100644 index 000000000..baad1efc8 --- /dev/null +++ b/src/geometry/projected_meters.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +#include + +namespace mbgl { +namespace android { + +class ProjectedMeters : private mbgl::util::noncopyable { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/geometry/ProjectedMeters"; }; + + static jni::Local> New(jni::JNIEnv&, double, double); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/gl_functions.cpp b/src/gl_functions.cpp new file mode 100644 index 000000000..637788e4a --- /dev/null +++ b/src/gl_functions.cpp @@ -0,0 +1,156 @@ +#include + +#define GL_GLEXT_PROTOTYPES +#include +#include + +namespace mbgl { +namespace platform { + +void (* const glActiveTexture)(GLenum) = ::glActiveTexture; +void (* const glAttachShader)(GLuint, GLuint) = ::glAttachShader; +void (* const glBindAttribLocation)(GLuint, GLuint, const GLchar *) = ::glBindAttribLocation; +void (* const glBindBuffer)(GLenum, GLuint) = ::glBindBuffer; +void (* const glBindFramebuffer)(GLenum, GLuint) = ::glBindFramebuffer; +void (* const glBindRenderbuffer)(GLenum, GLuint) = ::glBindRenderbuffer; +void (* const glBindTexture)(GLenum, GLuint) = ::glBindTexture; +void (* const glBlendColor)(GLfloat, GLfloat, GLfloat, GLfloat) = ::glBlendColor; +void (* const glBlendEquation)(GLenum) = ::glBlendEquation; +void (* const glBlendEquationSeparate)(GLenum, GLenum) = ::glBlendEquationSeparate; +void (* const glBlendFunc)(GLenum, GLenum) = ::glBlendFunc; +void (* const glBlendFuncSeparate)(GLenum, GLenum, GLenum, GLenum) = ::glBlendFuncSeparate; +void (* const glBufferData)(GLenum, GLsizeiptr, const void *, GLenum) = ::glBufferData; +void (* const glBufferSubData)(GLenum, GLintptr, GLsizeiptr, const void *) = ::glBufferSubData; +GLenum (* const glCheckFramebufferStatus)(GLenum) = ::glCheckFramebufferStatus; +void (* const glClear)(GLbitfield) = ::glClear; +void (* const glClearColor)(GLfloat, GLfloat, GLfloat, GLfloat) = ::glClearColor; +void (* const glClearDepthf)(GLfloat) = ::glClearDepthf; +void (* const glClearStencil)(GLint) = ::glClearStencil; +void (* const glColorMask)(GLboolean, GLboolean, GLboolean, GLboolean) = ::glColorMask; +void (* const glCompileShader)(GLuint) = ::glCompileShader; +void (* const glCompressedTexImage2D)(GLenum, GLint, GLenum, GLsizei, GLsizei, GLint, GLsizei, const void *) = ::glCompressedTexImage2D; +void (* const glCompressedTexSubImage2D)(GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLsizei, const void *) = ::glCompressedTexSubImage2D; +void (* const glCopyTexImage2D)(GLenum, GLint, GLenum, GLint, GLint, GLsizei, GLsizei, GLint) = ::glCopyTexImage2D; +void (* const glCopyTexSubImage2D)(GLenum, GLint, GLint, GLint, GLint, GLint, GLsizei, GLsizei) = ::glCopyTexSubImage2D; +GLuint (* const glCreateProgram)() = ::glCreateProgram; +GLuint (* const glCreateShader)(GLenum) = ::glCreateShader; +void (* const glCullFace)(GLenum) = ::glCullFace; +void (* const glDeleteBuffers)(GLsizei, const GLuint *) = ::glDeleteBuffers; +void (* const glDeleteFramebuffers)(GLsizei, const GLuint *) = ::glDeleteFramebuffers; +void (* const glDeleteProgram)(GLuint) = ::glDeleteProgram; +void (* const glDeleteRenderbuffers)(GLsizei, const GLuint *) = ::glDeleteRenderbuffers; +void (* const glDeleteShader)(GLuint) = ::glDeleteShader; +void (* const glDeleteTextures)(GLsizei, const GLuint *) = ::glDeleteTextures; +void (* const glDepthFunc)(GLenum) = ::glDepthFunc; +void (* const glDepthMask)(GLboolean) = ::glDepthMask; +void (* const glDepthRangef)(GLfloat, GLfloat) = ::glDepthRangef; +void (* const glDetachShader)(GLuint, GLuint) = ::glDetachShader; +void (* const glDisable)(GLenum) = ::glDisable; +void (* const glDisableVertexAttribArray)(GLuint) = ::glDisableVertexAttribArray; +void (* const glDrawArrays)(GLenum, GLint, GLsizei) = ::glDrawArrays; +void (* const glDrawElements)(GLenum, GLsizei, GLenum, const void *) = ::glDrawElements; +void (* const glEnable)(GLenum) = ::glEnable; +void (* const glEnableVertexAttribArray)(GLuint) = ::glEnableVertexAttribArray; +void (* const glFinish)() = ::glFinish; +void (* const glFlush)() = ::glFlush; +void (* const glFramebufferRenderbuffer)(GLenum, GLenum, GLenum, GLuint) = ::glFramebufferRenderbuffer; +void (* const glFramebufferTexture2D)(GLenum, GLenum, GLenum, GLuint, GLint) = ::glFramebufferTexture2D; +void (* const glFrontFace)(GLenum) = ::glFrontFace; +void (* const glGenBuffers)(GLsizei, GLuint *) = ::glGenBuffers; +void (* const glGenerateMipmap)(GLenum) = ::glGenerateMipmap; +void (* const glGenFramebuffers)(GLsizei, GLuint *) = ::glGenFramebuffers; +void (* const glGenRenderbuffers)(GLsizei, GLuint *) = ::glGenRenderbuffers; +void (* const glGenTextures)(GLsizei, GLuint *) = ::glGenTextures; +void (* const glGetActiveAttrib)(GLuint, GLuint, GLsizei, GLsizei *, GLint *, GLenum *, GLchar *) = ::glGetActiveAttrib; +void (* const glGetActiveUniform)(GLuint, GLuint, GLsizei, GLsizei *, GLint *, GLenum *, GLchar *) = ::glGetActiveUniform; +void (* const glGetAttachedShaders)(GLuint, GLsizei, GLsizei *, GLuint *) = ::glGetAttachedShaders; +GLint (* const glGetAttribLocation)(GLuint, const GLchar *) = ::glGetAttribLocation; +void (* const glGetBooleanv)(GLenum, GLboolean *) = ::glGetBooleanv; +void (* const glGetBufferParameteriv)(GLenum, GLenum, GLint *) = ::glGetBufferParameteriv; +GLenum (* const glGetError)() = ::glGetError; +void (* const glGetFloatv)(GLenum, GLfloat *) = ::glGetFloatv; +void (* const glGetFramebufferAttachmentParameteriv)(GLenum, GLenum, GLenum, GLint *) = ::glGetFramebufferAttachmentParameteriv; +void (* const glGetIntegerv)(GLenum, GLint *) = ::glGetIntegerv; +void (* const glGetProgramInfoLog)(GLuint, GLsizei, GLsizei *, GLchar *) = ::glGetProgramInfoLog; +void (* const glGetProgramiv)(GLuint, GLenum, GLint *) = ::glGetProgramiv; +void (* const glGetRenderbufferParameteriv)(GLenum, GLenum, GLint *) = ::glGetRenderbufferParameteriv; +void (* const glGetShaderInfoLog)(GLuint, GLsizei, GLsizei *, GLchar *) = ::glGetShaderInfoLog; +void (* const glGetShaderiv)(GLuint, GLenum, GLint *) = ::glGetShaderiv; +void (* const glGetShaderSource)(GLuint, GLsizei, GLsizei *, GLchar *) = ::glGetShaderSource; +const GLubyte *(*glGetString)(GLenum) = ::glGetString; +void (* const glGetTexParameterfv)(GLenum, GLenum, GLfloat *) = ::glGetTexParameterfv; +void (* const glGetTexParameteriv)(GLenum, GLenum, GLint *) = ::glGetTexParameteriv; +void (* const glGetUniformfv)(GLuint, GLint, GLfloat *) = ::glGetUniformfv; +void (* const glGetUniformiv)(GLuint, GLint, GLint *) = ::glGetUniformiv; +GLint (* const glGetUniformLocation)(GLuint, const GLchar *) = ::glGetUniformLocation; +void (* const glGetVertexAttribfv)(GLuint, GLenum, GLfloat *) = ::glGetVertexAttribfv; +void (* const glGetVertexAttribiv)(GLuint, GLenum, GLint *) = ::glGetVertexAttribiv; +void (* const glGetVertexAttribPointerv)(GLuint, GLenum, void **) = ::glGetVertexAttribPointerv; +void (* const glHint)(GLenum, GLenum) = ::glHint; +GLboolean (* const glIsBuffer)(GLuint) = ::glIsBuffer; +GLboolean (* const glIsEnabled)(GLenum) = ::glIsEnabled; +GLboolean (* const glIsFramebuffer)(GLuint) = ::glIsFramebuffer; +GLboolean (* const glIsProgram)(GLuint) = ::glIsProgram; +GLboolean (* const glIsRenderbuffer)(GLuint) = ::glIsRenderbuffer; +GLboolean (* const glIsShader)(GLuint) = ::glIsShader; +GLboolean (* const glIsTexture)(GLuint) = ::glIsTexture; +void (* const glLineWidth)(GLfloat) = ::glLineWidth; +void (* const glLinkProgram)(GLuint) = ::glLinkProgram; +void (* const glPixelStorei)(GLenum, GLint) = ::glPixelStorei; +void (* const glPolygonOffset)(GLfloat, GLfloat) = ::glPolygonOffset; +void (* const glReadPixels)(GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, void *) = ::glReadPixels; +void (* const glRenderbufferStorage)(GLenum, GLenum, GLsizei, GLsizei) = ::glRenderbufferStorage; +void (* const glSampleCoverage)(GLfloat, GLboolean) = ::glSampleCoverage; +void (* const glScissor)(GLint, GLint, GLsizei, GLsizei) = ::glScissor; + +// Some implementations use `const GLchar* const*` and others `const GLchar**`. +void (* const glShaderSource)(GLuint, GLsizei, const GLchar * const*, const GLint *) = [](GLuint shader, GLsizei count, const GLchar * const * string, const GLint *length) { + ::glShaderSource(shader, count, const_cast(string), length); +}; + +void (* const glStencilFunc)(GLenum, GLint, GLuint) = ::glStencilFunc; +void (* const glStencilFuncSeparate)(GLenum, GLenum, GLint, GLuint) = ::glStencilFuncSeparate; +void (* const glStencilMask)(GLuint) = ::glStencilMask; +void (* const glStencilMaskSeparate)(GLenum, GLuint) = ::glStencilMaskSeparate; +void (* const glStencilOp)(GLenum, GLenum, GLenum) = ::glStencilOp; +void (* const glStencilOpSeparate)(GLenum, GLenum, GLenum, GLenum) = ::glStencilOpSeparate; +void (* const glTexImage2D)(GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const void *) = ::glTexImage2D; +void (* const glTexParameterf)(GLenum, GLenum, GLfloat) = ::glTexParameterf; +void (* const glTexParameterfv)(GLenum, GLenum, const GLfloat *) = ::glTexParameterfv; +void (* const glTexParameteri)(GLenum, GLenum, GLint) = ::glTexParameteri; +void (* const glTexParameteriv)(GLenum, GLenum, const GLint *) = ::glTexParameteriv; +void (* const glTexSubImage2D)(GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const void *) = ::glTexSubImage2D; +void (* const glUniform1f)(GLint, GLfloat) = ::glUniform1f; +void (* const glUniform1fv)(GLint, GLsizei, const GLfloat *) = ::glUniform1fv; +void (* const glUniform1i)(GLint, GLint) = ::glUniform1i; +void (* const glUniform1iv)(GLint, GLsizei, const GLint *) = ::glUniform1iv; +void (* const glUniform2f)(GLint, GLfloat, GLfloat) = ::glUniform2f; +void (* const glUniform2fv)(GLint, GLsizei, const GLfloat *) = ::glUniform2fv; +void (* const glUniform2i)(GLint, GLint, GLint) = ::glUniform2i; +void (* const glUniform2iv)(GLint, GLsizei, const GLint *) = ::glUniform2iv; +void (* const glUniform3f)(GLint, GLfloat, GLfloat, GLfloat) = ::glUniform3f; +void (* const glUniform3fv)(GLint, GLsizei, const GLfloat *) = ::glUniform3fv; +void (* const glUniform3i)(GLint, GLint, GLint, GLint) = ::glUniform3i; +void (* const glUniform3iv)(GLint, GLsizei, const GLint *) = ::glUniform3iv; +void (* const glUniform4f)(GLint, GLfloat, GLfloat, GLfloat, GLfloat) = ::glUniform4f; +void (* const glUniform4fv)(GLint, GLsizei, const GLfloat *) = ::glUniform4fv; +void (* const glUniform4i)(GLint, GLint, GLint, GLint, GLint) = ::glUniform4i; +void (* const glUniform4iv)(GLint, GLsizei, const GLint *) = ::glUniform4iv; +void (* const glUniformMatrix2fv)(GLint, GLsizei, GLboolean, const GLfloat *) = ::glUniformMatrix2fv; +void (* const glUniformMatrix3fv)(GLint, GLsizei, GLboolean, const GLfloat *) = ::glUniformMatrix3fv; +void (* const glUniformMatrix4fv)(GLint, GLsizei, GLboolean, const GLfloat *) = ::glUniformMatrix4fv; +void (* const glUseProgram)(GLuint) = ::glUseProgram; +void (* const glValidateProgram)(GLuint) = ::glValidateProgram; +void (* const glVertexAttrib1f)(GLuint, GLfloat) = ::glVertexAttrib1f; +void (* const glVertexAttrib1fv)(GLuint, const GLfloat *) = ::glVertexAttrib1fv; +void (* const glVertexAttrib2f)(GLuint, GLfloat, GLfloat) = ::glVertexAttrib2f; +void (* const glVertexAttrib2fv)(GLuint, const GLfloat *) = ::glVertexAttrib2fv; +void (* const glVertexAttrib3f)(GLuint, GLfloat, GLfloat, GLfloat) = ::glVertexAttrib3f; +void (* const glVertexAttrib3fv)(GLuint, const GLfloat *) = ::glVertexAttrib3fv; +void (* const glVertexAttrib4f)(GLuint, GLfloat, GLfloat, GLfloat, GLfloat) = ::glVertexAttrib4f; +void (* const glVertexAttrib4fv)(GLuint, const GLfloat *) = ::glVertexAttrib4fv; +void (* const glVertexAttribPointer)(GLuint, GLint, GLenum, GLboolean, GLsizei, const void *) = ::glVertexAttribPointer; +void (* const glViewport)(GLint, GLint, GLsizei, GLsizei) = ::glViewport; + +} // namespace platform +} // namespace mbgl \ No newline at end of file diff --git a/src/graphics/pointf.cpp b/src/graphics/pointf.cpp new file mode 100644 index 000000000..2fc762d6e --- /dev/null +++ b/src/graphics/pointf.cpp @@ -0,0 +1,25 @@ +#include +#include "pointf.hpp" + +namespace mbgl { +namespace android { + +jni::Local> PointF::New(jni::JNIEnv& env, float x, float y) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, x, y); +} + +mbgl::ScreenCoordinate PointF::getScreenCoordinate(jni::JNIEnv& env, const jni::Object& point) { + static auto& javaClass = jni::Class::Singleton(env); + static auto xField = javaClass.GetField(env, "x"); + static auto yField = javaClass.GetField(env, "y"); + return mbgl::ScreenCoordinate{point.Get(env, xField), point.Get(env, yField)}; +} + +void PointF::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/graphics/pointf.hpp b/src/graphics/pointf.hpp new file mode 100644 index 000000000..899468b8e --- /dev/null +++ b/src/graphics/pointf.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include + +namespace mbgl { +namespace android { + +class PointF : private mbgl::util::noncopyable { +public: + + static constexpr auto Name() { return "android/graphics/PointF"; }; + + static jni::Local> New(jni::JNIEnv&, float, float); + + static mbgl::ScreenCoordinate getScreenCoordinate(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/graphics/rectf.cpp b/src/graphics/rectf.cpp new file mode 100644 index 000000000..8f418da03 --- /dev/null +++ b/src/graphics/rectf.cpp @@ -0,0 +1,35 @@ +#include "rectf.hpp" + +namespace mbgl { +namespace android { + +float RectF::getLeft(jni::JNIEnv& env, const jni::Object& rectf) { + static auto& javaClass = jni::Class::Singleton(env); + static auto field = javaClass.GetField(env, "left"); + return rectf.Get(env, field); +} + +float RectF::getTop(jni::JNIEnv& env, const jni::Object& rectf) { + static auto& javaClass = jni::Class::Singleton(env); + static auto field = javaClass.GetField(env, "top"); + return rectf.Get(env, field); +} + +float RectF::getRight(jni::JNIEnv& env, const jni::Object& rectf) { + static auto& javaClass = jni::Class::Singleton(env); + static auto field = javaClass.GetField(env, "right"); + return rectf.Get(env, field); +} + +float RectF::getBottom(jni::JNIEnv& env, const jni::Object& rectf) { + static auto& javaClass = jni::Class::Singleton(env); + static auto field = javaClass.GetField(env, "bottom"); + return rectf.Get(env, field); +} + +void RectF::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/graphics/rectf.hpp b/src/graphics/rectf.hpp new file mode 100644 index 000000000..ba611f226 --- /dev/null +++ b/src/graphics/rectf.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include + +namespace mbgl { +namespace android { + +class RectF : private mbgl::util::noncopyable { +public: + + static constexpr auto Name() { return "android/graphics/RectF"; }; + + static float getLeft(jni::JNIEnv&, const jni::Object&); + + static float getTop(jni::JNIEnv&, const jni::Object&); + + static float getRight(jni::JNIEnv&, const jni::Object&); + + static float getBottom(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/gson/json_array.cpp b/src/gson/json_array.cpp new file mode 100644 index 000000000..8ffa658b7 --- /dev/null +++ b/src/gson/json_array.cpp @@ -0,0 +1,50 @@ +#include "json_array.hpp" +#include "json_element.hpp" + +namespace mbgl { +namespace android { +namespace gson { + +jni::Local> JsonArray::New(jni::JNIEnv& env, const std::vector& values){ + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + static auto addMethod = javaClass.GetMethod)>(env, "add"); + + auto jsonArray = javaClass.New(env, constructor); + + for (const auto &v : values) { + jsonArray.Call(env, addMethod, JsonElement::New(env, v)); + } + + return jsonArray; +} + +std::vector JsonArray::convert(jni::JNIEnv& env, const jni::Object& jsonArray) { + std::vector values; + + if (jsonArray) { + static auto& javaClass = jni::Class::Singleton(env); + static auto getMethod = javaClass.GetMethod (jni::jint)>(env, "get"); + static auto sizeMethod = javaClass.GetMethod(env, "size"); + + int size = jsonArray.Call(env, sizeMethod); + values.reserve(uint(size)); + + for (int i = 0; i < size; i++) { + auto entry = jsonArray.Call(env, getMethod, i); + if (entry) { + values.push_back(JsonElement::convert(env, entry)); + } + } + } + + return values; +} + +void JsonArray::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace gson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/gson/json_array.hpp b/src/gson/json_array.hpp new file mode 100644 index 000000000..24d5d3d29 --- /dev/null +++ b/src/gson/json_array.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "json_element.hpp" + +#include + +namespace mbgl { +namespace android { +namespace gson { + +class JsonArray { +public: + using SuperTag = JsonElement; + static constexpr auto Name() { return "com/google/gson/JsonArray"; }; + + static jni::Local> New(jni::JNIEnv&, const std::vector&); + static std::vector convert(JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace gson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/gson/json_element.cpp b/src/gson/json_element.cpp new file mode 100644 index 000000000..ba22304d0 --- /dev/null +++ b/src/gson/json_element.cpp @@ -0,0 +1,88 @@ +#include "json_element.hpp" +#include "json_array.hpp" +#include "json_object.hpp" +#include "json_primitive.hpp" + +namespace mbgl { +namespace android { +namespace gson { + +jni::Local> JsonElement::New(jni::JNIEnv& env, const mbgl::Value& value) { + static auto& primitive = jni::Class::Singleton(env); + static auto stringConstructor = primitive.GetConstructor(env); + static auto numberConstructor = primitive.GetConstructor(env); + static auto booleanConstructor = primitive.GetConstructor(env); + + return value.match( + [&] (const mbgl::NullValue&) { + return jni::Local>(); + }, + [&] (const std::string& value) { + return primitive.New(env, stringConstructor, jni::Make(env, value)); + }, + [&] (const double value) { + return primitive.New(env, numberConstructor, jni::Box(env, value)); + }, + [&] (const int64_t value) { + return primitive.New(env, numberConstructor, jni::Box(env, value)); + }, + [&] (const uint64_t value) { + return primitive.New(env, numberConstructor, jni::Box(env, int64_t(value))); // TODO: should use BigInteger + }, + [&] (const bool value) { + return primitive.New(env, booleanConstructor, jni::Box(env, value ? jni::jni_true : jni::jni_false)); + }, + [&] (const std::vector& values) { + return JsonArray::New(env, values); + }, + [&] (const mbgl::PropertyMap& values) { + return JsonObject::New(env, values); + } + ); +} + +mbgl::Value JsonElement::convert(jni::JNIEnv &env, const jni::Object& jsonElement) { + if (!jsonElement) { + return mbgl::NullValue(); + } + + static auto& elementClass = jni::Class::Singleton(env); + static auto isJsonObject = elementClass.GetMethod(env, "isJsonObject"); + static auto isJsonArray = elementClass.GetMethod(env, "isJsonArray"); + static auto isJsonPrimitive = elementClass.GetMethod(env, "isJsonPrimitive"); + + static auto& primitiveClass = jni::Class::Singleton(env); + static auto isBoolean = primitiveClass.GetMethod(env, "isBoolean"); + static auto isString = primitiveClass.GetMethod(env, "isString"); + static auto isNumber = primitiveClass.GetMethod(env, "isNumber"); + static auto getAsBoolean = primitiveClass.GetMethod(env, "getAsBoolean"); + static auto getAsString = primitiveClass.GetMethod(env, "getAsString"); + static auto getAsDouble = primitiveClass.GetMethod(env, "getAsDouble"); + + if (jsonElement.Call(env, isJsonPrimitive)) { + auto primitive = jni::Cast(env, primitiveClass, jsonElement); + if (primitive.Call(env, isBoolean)) { + return bool(primitive.Call(env, getAsBoolean)); + } else if (primitive.Call(env, isNumber)) { + return primitive.Call(env, getAsDouble); + } else if (primitive.Call(env, isString)) { + return jni::Make(env, primitive.Call(env, getAsString)); + } else { + return mbgl::NullValue(); + } + } else if (jsonElement.Call(env, isJsonObject)) { + return JsonObject::convert(env, jni::Cast(env, jni::Class::Singleton(env), jsonElement)); + } else if (jsonElement.Call(env, isJsonArray)) { + return JsonArray::convert(env, jni::Cast(env, jni::Class::Singleton(env), jsonElement)); + } else { + return mbgl::NullValue(); + } +} + +void JsonElement::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace gson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/gson/json_element.hpp b/src/gson/json_element.hpp new file mode 100644 index 000000000..e3b8a0268 --- /dev/null +++ b/src/gson/json_element.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include + +namespace mbgl { +namespace android { +namespace gson { + +class JsonElement { +public: + using SuperTag = jni::ObjectTag; + static constexpr auto Name() { return "com/google/gson/JsonElement"; }; + + static jni::Local> New(jni::JNIEnv&, const mbgl::Value&); + static mbgl::Value convert(JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace gson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/gson/json_object.cpp b/src/gson/json_object.cpp new file mode 100644 index 000000000..10d244ee4 --- /dev/null +++ b/src/gson/json_object.cpp @@ -0,0 +1,65 @@ +#include "json_object.hpp" + +#include "json_element.hpp" + +#include "../java/util.hpp" + +namespace mbgl { +namespace android { +namespace gson { + + +jni::Local> JsonObject::New(jni::JNIEnv& env, const mbgl::PropertyMap& values) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + static auto addMethod = javaClass.GetMethod)>(env, "add"); + + auto jsonObject = javaClass.New(env, constructor); + + for (auto &item : values) { + jsonObject.Call(env, addMethod, + jni::Make(env, item.first), + JsonElement::New(env, item.second)); + } + + return jsonObject; +} + +template // void (jni::String, jni::Object) +static void iterateEntrySet(jni::JNIEnv& env, const jni::Object& jsonObject, F callback) { + // Get Set> + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod ()>(env, "entrySet"); + + auto entryArray = java::util::Set::toArray(env, jsonObject.Call(env, method)); + + size_t size = entryArray.Length(env); + for (size_t i = 0; i < size; i++) { + auto entry = entryArray.Get(env, i); + if (entry) { + callback( + java::util::Map::Entry::getKey(env, entry), + java::util::Map::Entry::getValue(env, entry)); + } + } +} + +mbgl::PropertyMap JsonObject::convert(jni::JNIEnv& env, const jni::Object& jsonObject) { + mbgl::PropertyMap map; + + if (jsonObject) { + iterateEntrySet(env, jsonObject, [&map, &env](const jni::String& jId, const jni::Object& jsonElement) { + map[jni::Make(env, jId)] = JsonElement::convert(env, jsonElement); + }); + } + + return map; +} + +void JsonObject::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace gson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/gson/json_object.hpp b/src/gson/json_object.hpp new file mode 100644 index 000000000..b8916f8ae --- /dev/null +++ b/src/gson/json_object.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "json_element.hpp" + +#include + +namespace mbgl { +namespace android { +namespace gson { + +class JsonObject { +public: + using SuperTag = JsonElement; + static constexpr auto Name() { return "com/google/gson/JsonObject"; }; + + static jni::Local> New(jni::JNIEnv&, const mbgl::PropertyMap&); + static mbgl::PropertyMap convert(JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace gson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/gson/json_primitive.cpp b/src/gson/json_primitive.cpp new file mode 100644 index 000000000..79d6b0761 --- /dev/null +++ b/src/gson/json_primitive.cpp @@ -0,0 +1,13 @@ +#include "json_primitive.hpp" + +namespace mbgl { +namespace android { +namespace gson { + +void JsonPrimitive::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace gson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/gson/json_primitive.hpp b/src/gson/json_primitive.hpp new file mode 100644 index 000000000..36f138d90 --- /dev/null +++ b/src/gson/json_primitive.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "json_element.hpp" + +#include + +namespace mbgl { +namespace android { +namespace gson { + +class JsonPrimitive { +public: + using SuperTag = JsonElement; + static constexpr auto Name() { return "com/google/gson/JsonPrimitive"; }; + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace gson +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/http_file_source.cpp b/src/http_file_source.cpp new file mode 100644 index 000000000..7066918fe --- /dev/null +++ b/src/http_file_source.cpp @@ -0,0 +1,194 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include "attach_env.hpp" + +namespace mbgl { + +class HTTPFileSource::Impl { +public: + android::UniqueEnv env { android::AttachEnv() }; +}; + +class HTTPRequest : public AsyncRequest { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/http/NativeHttpRequest"; }; + + HTTPRequest(jni::JNIEnv&, const Resource&, FileSource::Callback); + ~HTTPRequest(); + + void onFailure(jni::JNIEnv&, int type, const jni::String& message); + void onResponse(jni::JNIEnv&, int code, + const jni::String& etag, const jni::String& modified, + const jni::String& cacheControl, const jni::String& expires, + const jni::String& retryAfter, const jni::String& xRateLimitReset, + const jni::Array& body); + + jni::Global> javaRequest; + +private: + Resource resource; + FileSource::Callback callback; + Response response; + + util::AsyncTask async { [this] { + // Calling `callback` may result in deleting `this`. Copy data to temporaries first. + auto callback_ = callback; + auto response_ = response; + callback_(response_); + } }; + + static const int connectionError = 0; + static const int temporaryError = 1; + static const int permanentError = 2; +}; + +namespace android { + +void RegisterNativeHTTPRequest(jni::JNIEnv& env) { + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + jni::RegisterNativePeer(env, javaClass, "nativePtr", + METHOD(&HTTPRequest::onFailure, "nativeOnFailure"), + METHOD(&HTTPRequest::onResponse, "nativeOnResponse")); +} + +} // namespace android + +HTTPRequest::HTTPRequest(jni::JNIEnv& env, const Resource& resource_, FileSource::Callback callback_) + : resource(resource_), + callback(callback_) { + std::string etagStr; + std::string modifiedStr; + + if (resource.priorEtag) { + etagStr = *resource.priorEtag; + } else if (resource.priorModified) { + modifiedStr = util::rfc1123(*resource.priorModified); + } + + jni::UniqueLocalFrame frame = jni::PushLocalFrame(env, 10); + + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = + javaClass.GetConstructor(env); + + javaRequest = jni::NewGlobal(env, + javaClass.New(env, constructor, + reinterpret_cast(this), + jni::Make(env, resource.url), + jni::Make(env, etagStr), + jni::Make(env, modifiedStr), + (jboolean) (resource_.usage == Resource::Usage::Offline) + ) + ); +} + +HTTPRequest::~HTTPRequest() { + android::UniqueEnv env = android::AttachEnv(); + + static auto& javaClass = jni::Class::Singleton(*env); + static auto cancel = javaClass.GetMethod(*env, "cancel"); + + javaRequest.Call(*env, cancel); +} + +void HTTPRequest::onResponse(jni::JNIEnv& env, int code, + const jni::String& etag, const jni::String& modified, + const jni::String& cacheControl, const jni::String& expires, + const jni::String& jRetryAfter, const jni::String& jXRateLimitReset, + const jni::Array& body) { + + using Error = Response::Error; + + if (etag) { + response.etag = jni::Make(env, etag); + } + + if (modified) { + response.modified = util::parseTimestamp(jni::Make(env, modified).c_str()); + } + + if (cacheControl) { + const auto cc = http::CacheControl::parse(jni::Make(env, cacheControl).c_str()); + response.expires = cc.toTimePoint(); + response.mustRevalidate = cc.mustRevalidate; + } + + if (expires) { + response.expires = util::parseTimestamp(jni::Make(env, expires).c_str()); + } + + if (code == 200) { + if (body) { + auto data = std::make_shared(body.Length(env), char()); + jni::GetArrayRegion(env, *body, 0, data->size(), reinterpret_cast(&(*data)[0])); + response.data = data; + } else { + response.data = std::make_shared(); + } + } else if (code == 204 || (code == 404 && resource.kind == Resource::Kind::Tile)) { + response.noContent = true; + } else if (code == 304) { + response.notModified = true; + } else if (code == 404) { + response.error = std::make_unique(Error::Reason::NotFound, "HTTP status code 404"); + } else if (code == 429) { + optional retryAfter; + optional xRateLimitReset; + if (jRetryAfter) { + retryAfter = jni::Make(env, jRetryAfter); + } + if (jXRateLimitReset) { + xRateLimitReset = jni::Make(env, jXRateLimitReset); + } + response.error = std::make_unique(Error::Reason::RateLimit, "HTTP status code 429", http::parseRetryHeaders(retryAfter, xRateLimitReset)); + } else if (code >= 500 && code < 600) { + response.error = std::make_unique(Error::Reason::Server, std::string{ "HTTP status code " } + util::toString(code)); + } else { + response.error = std::make_unique(Error::Reason::Other, std::string{ "HTTP status code " } + util::toString(code)); + } + + async.send(); +} + +void HTTPRequest::onFailure(jni::JNIEnv& env, int type, const jni::String& message) { + std::string messageStr = jni::Make(env, message); + + using Error = Response::Error; + + switch (type) { + case connectionError: + response.error = std::make_unique(Error::Reason::Connection, messageStr); + break; + case temporaryError: + response.error = std::make_unique(Error::Reason::Server, messageStr); + break; + default: + response.error = std::make_unique(Error::Reason::Other, messageStr); + } + + async.send(); +} + +HTTPFileSource::HTTPFileSource() + : impl(std::make_unique()) { +} + +HTTPFileSource::~HTTPFileSource() = default; + +std::unique_ptr HTTPFileSource::request(const Resource& resource, Callback callback) { + return std::make_unique(*impl->env, resource, callback); +} + +} // namespace mbgl diff --git a/src/image.cpp b/src/image.cpp new file mode 100644 index 000000000..31515ff16 --- /dev/null +++ b/src/image.cpp @@ -0,0 +1,21 @@ +#include +#include + +#include + +#include "attach_env.hpp" +#include "bitmap_factory.hpp" + +namespace mbgl { + +PremultipliedImage decodeImage(const std::string& string) { + auto env{ android::AttachEnv() }; + + auto array = jni::Array::New(*env, string.size()); + jni::SetArrayRegion(*env, *array, 0, string.size(), + reinterpret_cast(string.data())); + + return android::Bitmap::GetImage(*env, android::BitmapFactory::DecodeByteArray(*env, array, 0, string.size())); +} + +} // namespace mbgl diff --git a/src/java/util.cpp b/src/java/util.cpp new file mode 100644 index 000000000..b3aebb36c --- /dev/null +++ b/src/java/util.cpp @@ -0,0 +1,19 @@ +#include "util.hpp" + +namespace mbgl { +namespace android { +namespace java { +namespace util { + +void registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); + jni::Class::Singleton(env); + jni::Class::Singleton(env); + jni::Class::Singleton(env); + jni::Class::Singleton(env); +} + +} // namespace util +} // namespace java +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/java/util.hpp b/src/java/util.hpp new file mode 100644 index 000000000..9b0c01459 --- /dev/null +++ b/src/java/util.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include + +#include + +namespace mbgl { +namespace android { +namespace java { +namespace util { + +class List : private mbgl::util::noncopyable { +public: + static constexpr auto Name() { return "java/util/List"; }; + + template + static jni::Local>> toArray(jni::JNIEnv& env, const jni::Object& list) { + static auto& javaClass = jni::Class::Singleton(env); + static auto toArray = javaClass.GetMethod> ()>(env, "toArray"); + + return jni::Local>>(env, list.Call(env, toArray).release()); + }; +}; + +class Arrays : private mbgl::util::noncopyable { +public: + static constexpr auto Name() { return "java/util/Arrays"; }; + + template + static jni::Local> asList(jni::JNIEnv& env, const jni::Array>& array) { + static auto& javaClass = jni::Class::Singleton(env); + static auto asList = javaClass.GetStaticMethod(jni::Array>)>(env, "asList"); + + auto typeErasedArray = jni::Local>>(env, jni::NewLocal(env, array).release()); + return javaClass.Call(env, asList, typeErasedArray); + } +}; + +class Set : private mbgl::util::noncopyable { +public: + static constexpr auto Name() { return "java/util/Set"; }; + + template + static jni::Local>> toArray(jni::JNIEnv& env, const jni::Object& list) { + static auto& javaClass = jni::Class::Singleton(env); + static auto toArray = javaClass.GetMethod> ()>(env, "toArray"); + + return jni::Local>>(env, list.Call(env, toArray).release()); + }; +}; + +class Map : private mbgl::util::noncopyable { +public: + static constexpr auto Name() { return "java/util/Map"; }; + + class Entry : private mbgl::util::noncopyable { + public: + static constexpr auto Name() { return "java/util/Map$Entry"; }; + + template + static jni::Local> getKey(jni::JNIEnv& env, const jni::Object& entry) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod ()>(env, "getKey"); + return jni::Cast(env, jni::Class::Singleton(env), entry.Call(env, method)); + } + + template + static jni::Local> getValue(jni::JNIEnv& env, const jni::Object& entry) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod ()>(env, "getValue"); + return jni::Cast(env, jni::Class::Singleton(env), entry.Call(env, method)); + } + }; +}; + +void registerNative(jni::JNIEnv&); + +} // namespace util +} // namespace java +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/java_types.cpp b/src/java_types.cpp new file mode 100644 index 000000000..7a1ba93a5 --- /dev/null +++ b/src/java_types.cpp @@ -0,0 +1,49 @@ +#include "java_types.hpp" + +namespace mbgl { +namespace android { +namespace java { + + jni::jclass* ObjectArray::jclass; + + jni::jclass* String::jclass; + + jni::jclass* Boolean::jclass; + jni::jmethodID* Boolean::booleanValueMethodId; + + jni::jclass* Number::jclass; + jni::jmethodID* Number::floatValueMethodId; + jni::jmethodID* Number::doubleValueMethodId; + jni::jmethodID* Number::longValueMethodId; + + jni::jclass* Map::jclass; + jni::jmethodID* Map::getMethodId; + jni::jmethodID* Map::keySetMethodId; + + jni::jclass* Set::jclass; + jni::jmethodID* Set::toArrayMethodId; + + void registerNatives(JNIEnv& env) { + ObjectArray::jclass = jni::NewGlobalRef(env, &jni::FindClass(env, "[Ljava/lang/Object;")).release(); + + String::jclass = jni::NewGlobalRef(env, &jni::FindClass(env, "java/lang/String")).release(); + + Boolean::jclass = jni::NewGlobalRef(env, &jni::FindClass(env, "java/lang/Boolean")).release(); + Boolean::booleanValueMethodId = &jni::GetMethodID(env, *Boolean::jclass, "booleanValue", "()Z"); + + Number::jclass = jni::NewGlobalRef(env, &jni::FindClass(env, "java/lang/Number")).release(); + Number::floatValueMethodId = &jni::GetMethodID(env, *Number::jclass, "floatValue", "()F"); + Number::doubleValueMethodId = &jni::GetMethodID(env, *Number::jclass, "doubleValue", "()D"); + Number::longValueMethodId = &jni::GetMethodID(env, *Number::jclass, "longValue", "()J"); + + Map::jclass = jni::NewGlobalRef(env, &jni::FindClass(env, "java/util/Map")).release(); + Map::getMethodId = &jni::GetMethodID(env, *Map::jclass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); + Map::keySetMethodId = &jni::GetMethodID(env, *Map::jclass, "keySet", "()Ljava/util/Set;"); + + Set::jclass = jni::NewGlobalRef(env, &jni::FindClass(env, "java/util/Set")).release(); + Set::toArrayMethodId = &jni::GetMethodID(env, *Set::jclass, "toArray", "()[Ljava/lang/Object;"); + } + +} +} +} diff --git a/src/java_types.hpp b/src/java_types.hpp new file mode 100644 index 000000000..c7c93ce71 --- /dev/null +++ b/src/java_types.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +namespace mbgl { +namespace android { +namespace java { + + struct ObjectArray { + static jni::jclass* jclass; + }; + + struct String { + static jni::jclass* jclass; + }; + + struct Boolean { + static jni::jclass* jclass; + static jni::jmethodID* booleanValueMethodId; + }; + + struct Number { + static jni::jclass* jclass; + static jni::jmethodID* floatValueMethodId; + static jni::jmethodID* doubleValueMethodId; + static jni::jmethodID* longValueMethodId; + }; + + struct Map { + static jni::jclass* jclass; + static jni::jmethodID* getMethodId; + static jni::jmethodID* keySetMethodId; + }; + + struct Set { + static jni::jclass* jclass; + static jni::jmethodID* toArrayMethodId; + }; + + void registerNatives(JNIEnv&); +} +} +} diff --git a/src/jni.cpp b/src/jni.cpp new file mode 100755 index 000000000..189ec29dc --- /dev/null +++ b/src/jni.cpp @@ -0,0 +1,54 @@ +#include "jni.hpp" + +#include + +namespace mbgl { +namespace android { + +JavaVM* theJVM; + +//TODO: remove +bool attach_jni_thread(JavaVM* vm, JNIEnv** env, std::string threadName) { + assert(vm != nullptr); + assert(env != nullptr); + + JavaVMAttachArgs args = {JNI_VERSION_1_2, threadName.c_str(), NULL}; + + jint ret; + *env = nullptr; + bool detach = false; + ret = vm->GetEnv(reinterpret_cast(env), JNI_VERSION_1_6); + if (ret != JNI_OK) { + if (ret != JNI_EDETACHED) { + mbgl::Log::Error(mbgl::Event::JNI, "GetEnv() failed with %i", ret); + throw std::runtime_error("GetEnv() failed"); + } else { + ret = vm->AttachCurrentThread(env, &args); + if (ret != JNI_OK) { + mbgl::Log::Error(mbgl::Event::JNI, "AttachCurrentThread() failed with %i", ret); + throw std::runtime_error("AttachCurrentThread() failed"); + } + detach = true; + } + } + + return detach; +} + +//TODO: remove +void detach_jni_thread(JavaVM* vm, JNIEnv** env, bool detach) { + if (detach) { + assert(vm != nullptr); + assert(env != nullptr); + + jint ret; + if ((ret = vm->DetachCurrentThread()) != JNI_OK) { + mbgl::Log::Error(mbgl::Event::JNI, "DetachCurrentThread() failed with %i", ret); + throw std::runtime_error("DetachCurrentThread() failed"); + } + } + *env = nullptr; +} + +} // namespace android +} // namespace mbgl diff --git a/src/jni.hpp b/src/jni.hpp new file mode 100644 index 000000000..9bfe2ce5b --- /dev/null +++ b/src/jni.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include +#include + +typedef struct _jmethodID* jmethodID; +typedef struct _JavaVM JavaVM; +typedef struct _JNIEnv JNIEnv; + +namespace mbgl { +namespace android { + +extern JavaVM* theJVM; + +extern std::string cachePath; +extern std::string dataPath; + +bool attach_jni_thread(JavaVM* vm, JNIEnv** env, std::string threadName); +void detach_jni_thread(JavaVM* vm, JNIEnv** env, bool detach); + +} // namespace android +} // namespace mbgl diff --git a/src/jni_native.cpp b/src/jni_native.cpp new file mode 100755 index 000000000..df96ba975 --- /dev/null +++ b/src/jni_native.cpp @@ -0,0 +1,164 @@ +#include "jni_native.hpp" + +#include "annotation/marker.hpp" +#include "annotation/polygon.hpp" +#include "annotation/polyline.hpp" +#include "bitmap.hpp" +#include "bitmap_factory.hpp" +#include "connectivity_listener.hpp" +#include "conversion/conversion.hpp" +#include "conversion/collection.hpp" +#include "file_source.hpp" +#include "geojson/feature.hpp" +#include "geojson/feature_collection.hpp" +#include "geojson/geometry.hpp" +#include "geojson/geometry_collection.hpp" +#include "geojson/line_string.hpp" +#include "geojson/multi_line_string.hpp" +#include "geojson/multi_point.hpp" +#include "geojson/multi_polygon.hpp" +#include "geojson/point.hpp" +#include "geojson/polygon.hpp" +#include "geometry/lat_lng.hpp" +#include "geometry/lat_lng_bounds.hpp" +#include "geometry/lat_lng_quad.hpp" +#include "geometry/projected_meters.hpp" +#include "graphics/pointf.hpp" +#include "graphics/rectf.hpp" +#include "gson/json_array.hpp" +#include "gson/json_element.hpp" +#include "gson/json_object.hpp" +#include "gson/json_primitive.hpp" +#include "java_types.hpp" +#include "map_renderer.hpp" +#include "map_renderer_runnable.hpp" +#include "native_map_view.hpp" +#ifndef MBGL_MODULE_OFFLINE_DISABLE +#include "offline/offline_manager.hpp" +#include "offline/offline_region.hpp" +#include "offline/offline_region_definition.hpp" +#include "offline/offline_region_error.hpp" +#include "offline/offline_region_status.hpp" +#endif +#include "style/transition_options.hpp" +#include "style/layers/layer_manager.hpp" +#include "style/sources/source.hpp" +#include "style/light.hpp" +#include "style/formatted.hpp" +#include "style/formatted_section.hpp" +#ifndef MBGL_MODULE_SNAPSHOT_DISABLE +#include "snapshotter/map_snapshotter.hpp" +#include "snapshotter/map_snapshot.hpp" +#endif +#include "text/collator_jni.hpp" +#include "text/local_glyph_rasterizer_jni.hpp" +#include "text/format_number_jni.hpp" +#include "logger.hpp" + +namespace mbgl { +namespace android { + +void RegisterNativeHTTPRequest(JNIEnv&); + +void registerNatives(JavaVM *vm) { + theJVM = vm; + + jni::JNIEnv& env = jni::GetEnv(*vm, jni::jni_version_1_6); + + // For the DefaultFileSource + static mbgl::util::RunLoop mainRunLoop; + FileSource::registerNative(env); + + // Basic types + java::registerNatives(env); + java::util::registerNative(env); + PointF::registerNative(env); + RectF::registerNative(env); + + // GeoJSON + geojson::Feature::registerNative(env); + geojson::FeatureCollection::registerNative(env); + geojson::Geometry::registerNative(env); + geojson::GeometryCollection::registerNative(env); + geojson::LineString::registerNative(env); + geojson::MultiLineString::registerNative(env); + geojson::MultiPoint::registerNative(env); + geojson::MultiPolygon::registerNative(env); + geojson::Point::registerNative(env); + geojson::Polygon::registerNative(env); + + // Geometry + LatLng::registerNative(env); + LatLngBounds::registerNative(env); + LatLngQuad::registerNative(env); + ProjectedMeters::registerNative(env); + + // GSon + gson::JsonArray::registerNative(env); + gson::JsonElement::registerNative(env); + gson::JsonObject::registerNative(env); + gson::JsonPrimitive::registerNative(env); + + //Annotation + Marker::registerNative(env); + Polygon::registerNative(env); + Polyline::registerNative(env); + + // Map + MapRenderer::registerNative(env); + MapRendererRunnable::registerNative(env); + NativeMapView::registerNative(env); + + // Http + RegisterNativeHTTPRequest(env); + + // Bitmap + Bitmap::registerNative(env); + BitmapFactory::registerNative(env); + + // Style + TransitionOptions::registerNative(env); + LayerManagerAndroid::get()->registerNative(env); + Source::registerNative(env); + Light::registerNative(env); + Position::registerNative(env); + Formatted::registerNative(env); + FormattedSection::registerNative(env); + + // Map + CameraPosition::registerNative(env); + Image::registerNative(env); + + // Connectivity + ConnectivityListener::registerNative(env); + + // Offline +#ifndef MBGL_MODULE_OFFLINE_DISABLE + OfflineManager::registerNative(env); + OfflineRegion::registerNative(env); + OfflineRegionDefinition::registerNative(env); + OfflineTilePyramidRegionDefinition::registerNative(env); + OfflineGeometryRegionDefinition::registerNative(env); + OfflineRegionError::registerNative(env); + OfflineRegionStatus::registerNative(env); +#endif + + // Snapshotter +#ifndef MBGL_MODULE_SNAPSHOT_DISABLE + MapSnapshotter::registerNative(env); + MapSnapshot::registerNative(env); +#endif + + // text + LocalGlyphRasterizer::registerNative(env); + Locale::registerNative(env); + Collator::registerNative(env); + StringUtils::registerNative(env); + NumberFormat::registerNative(env); + + // Logger + Logger::registerNative(env); +} + +} // namespace android +} // namespace mbgl diff --git a/src/jni_native.hpp b/src/jni_native.hpp new file mode 100644 index 000000000..c8e284a52 --- /dev/null +++ b/src/jni_native.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +#include "jni.hpp" + +namespace mbgl { +namespace android { + +MBGL_PUBLIC void registerNatives(JavaVM* vm); + +} // namespace android +} // namespace mbgl diff --git a/src/logger.cpp b/src/logger.cpp new file mode 100644 index 000000000..f5d78cc83 --- /dev/null +++ b/src/logger.cpp @@ -0,0 +1,34 @@ +#include "logger.hpp" +#include "java_types.hpp" + +namespace mbgl { +namespace android { + +void Logger::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +void Logger::log(jni::JNIEnv& env, EventSeverity severity, const std::string &msg) { + static auto& _class = jni::Class::Singleton(env); + + auto tag = jni::Make(env, "Mbgl"); + auto message = jni::Make(env, msg); + using Signature = void(jni::String, jni::String); + + if (severity == EventSeverity::Debug) { + auto static debug = _class.GetStaticMethod(env, "d"); + _class.Call(env, debug, tag, message); + } else if (severity == EventSeverity::Info) { + auto static info = _class.GetStaticMethod(env, "i"); + _class.Call(env, info, tag, message); + } else if(severity == EventSeverity::Warning) { + auto static warning = _class.GetStaticMethod(env, "w"); + _class.Call(env, warning, tag, message); + } else { + auto static error = _class.GetStaticMethod(env, "e"); + _class.Call(env, error, tag, message); + } +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/logger.hpp b/src/logger.hpp new file mode 100644 index 000000000..7481ecb26 --- /dev/null +++ b/src/logger.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include "bitmap.hpp" + +namespace mbgl { +namespace android { + +class Logger { +public: + static constexpr auto Name() { + return "com/mapbox/mapboxsdk/log/Logger"; + }; + static void registerNative(jni::JNIEnv&); + + static void log(jni::JNIEnv&, EventSeverity severity, const std::string &msg); +}; + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/logging_android.cpp b/src/logging_android.cpp new file mode 100644 index 000000000..130136728 --- /dev/null +++ b/src/logging_android.cpp @@ -0,0 +1,13 @@ +#include + +#include "logger.hpp" +#include "attach_env.hpp" + +namespace mbgl { + +void Log::platformRecord(EventSeverity severity, const std::string &msg) { + auto env{ android::AttachEnv() }; + android::Logger::log(*env, severity, msg); +} + +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 000000000..9b5c6c197 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,9 @@ +#include "jni.hpp" +#include "jni_native.hpp" +#include + +extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) { + assert(vm != nullptr); + mbgl::android::registerNatives(vm); + return JNI_VERSION_1_6; +} diff --git a/src/map/camera_position.cpp b/src/map/camera_position.cpp new file mode 100644 index 000000000..03aca79bd --- /dev/null +++ b/src/map/camera_position.cpp @@ -0,0 +1,68 @@ +#include "camera_position.hpp" +#include "../geometry/lat_lng.hpp" + +namespace mbgl { +namespace android { + +jni::Local> CameraPosition::New(jni::JNIEnv &env, mbgl::CameraOptions options, float pixelRatio) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor, double, double, double, jni::Array>(env); + + // wrap LatLng values coming from core + auto center = options.center.value(); + center.wrap(); + + // convert bearing, measured in radians counterclockwise from true north. + // Wrapped to [−π rad, π rad). Android binding from 0 to 360 degrees + double bearing_degrees = options.bearing.value_or(0); + while (bearing_degrees > 360) { + bearing_degrees -= 360; + } + while (bearing_degrees < 0) { + bearing_degrees += 360; + } + + // convert tilt, core ranges from [0 rad, 1,0472 rad], android ranges from 0 to 60 + double tilt_degrees = options.pitch.value_or(0); + + std::vector paddingVect; + auto insets = options.padding.value_or(EdgeInsets {0, 0, 0, 0}); + auto padding = jni::Array::New(env, 4); + paddingVect.push_back(insets.left() * pixelRatio); + paddingVect.push_back(insets.top() * pixelRatio); + paddingVect.push_back(insets.right() * pixelRatio); + paddingVect.push_back(insets.bottom() * pixelRatio); + padding.SetRegion>(env, 0, paddingVect); + + return javaClass.New(env, constructor, LatLng::New(env, center), options.zoom.value_or(0), tilt_degrees, bearing_degrees, padding); +} + +mbgl::CameraOptions CameraPosition::getCameraOptions(jni::JNIEnv& env, const jni::Object& position, float pixelRatio) { + static auto& javaClass = jni::Class::Singleton(env); + static auto bearing = javaClass.GetField(env, "bearing"); + static auto target = javaClass.GetField>(env, "target"); + static auto tilt = javaClass.GetField(env, "tilt"); + static auto zoom = javaClass.GetField(env, "zoom"); + static auto paddingField = javaClass.GetField>(env, "padding"); + static auto padding = position.Get(env, paddingField); + + auto center = LatLng::getLatLng(env, position.Get(env, target)); + + return mbgl::CameraOptions { + center, + EdgeInsets {padding.Get(env, 1) * pixelRatio, padding.Get(env, 0) * pixelRatio, + padding.Get(env, 3) * pixelRatio, padding.Get(env, 2) * pixelRatio}, + {}, + position.Get(env, zoom), + position.Get(env, bearing), + position.Get(env, tilt) + }; +} + +void CameraPosition::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mb + diff --git a/src/map/camera_position.hpp b/src/map/camera_position.hpp new file mode 100644 index 000000000..b677f04ea --- /dev/null +++ b/src/map/camera_position.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +#include + +namespace mbgl { +namespace android { + +class CameraPosition : private mbgl::util::noncopyable { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/camera/CameraPosition"; }; + + static jni::Local> New(jni::JNIEnv&, mbgl::CameraOptions, float pixelRatio); + + static mbgl::CameraOptions getCameraOptions(jni::JNIEnv&, const jni::Object&, float pixelRatio); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/map/image.cpp b/src/map/image.cpp new file mode 100644 index 000000000..a91cc938e --- /dev/null +++ b/src/map/image.cpp @@ -0,0 +1,41 @@ +#include +#include +#include "image.hpp" + +namespace mbgl { +namespace android { + +mbgl::style::Image Image::getImage(jni::JNIEnv& env, const jni::Object& image) { + static auto& javaClass = jni::Class::Singleton(env); + static auto widthField = javaClass.GetField(env, "width"); + static auto heightField = javaClass.GetField(env, "height"); + static auto pixelRatioField = javaClass.GetField(env, "pixelRatio"); + static auto bufferField = javaClass.GetField>(env, "buffer"); + static auto nameField = javaClass.GetField(env, "name"); + static auto sdfField = javaClass.GetField(env, "sdf"); + + auto height = image.Get(env, heightField); + auto width = image.Get(env, widthField); + auto pixelRatio = image.Get(env, pixelRatioField); + auto pixels = image.Get(env, bufferField); + auto name = jni::Make(env, image.Get(env, nameField)); + auto sdf = (bool) image.Get(env, sdfField); + + jni::NullCheck(env, pixels.get()); + std::size_t size = pixels.Length(env); + + mbgl::PremultipliedImage premultipliedImage({ static_cast(width), static_cast(height) }); + if (premultipliedImage.bytes() != uint32_t(size)) { + throw mbgl::util::SpriteImageException("Sprite image pixel count mismatch"); + } + + jni::GetArrayRegion(env, *pixels, 0, size, reinterpret_cast(premultipliedImage.data.get())); + return mbgl::style::Image {name, std::move(premultipliedImage), pixelRatio, sdf}; +} + +void Image::registerNative(jni::JNIEnv &env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mb diff --git a/src/map/image.hpp b/src/map/image.hpp new file mode 100644 index 000000000..7549bc646 --- /dev/null +++ b/src/map/image.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include +#include + +namespace mbgl { +namespace android { + +class Image : private mbgl::util::noncopyable { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/maps/Image"; }; + + static mbgl::style::Image getImage(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/map_renderer.cpp b/src/map_renderer.cpp new file mode 100644 index 000000000..6be708b99 --- /dev/null +++ b/src/map_renderer.cpp @@ -0,0 +1,236 @@ +#include "map_renderer.hpp" + +#include +#include +#include + +#include + +#include "attach_env.hpp" +#include "android_renderer_backend.hpp" +#include "map_renderer_runnable.hpp" + +namespace mbgl { +namespace android { + +MapRenderer::MapRenderer(jni::JNIEnv& _env, + const jni::Object& obj, + jni::jfloat pixelRatio_, + const jni::String& localIdeographFontFamily_) + : javaPeer(_env, obj) + , pixelRatio(pixelRatio_) + , localIdeographFontFamily(localIdeographFontFamily_ ? jni::Make(_env, localIdeographFontFamily_) : optional{}) + , mailbox(std::make_shared(*this)) { +} + +MapRenderer::~MapRenderer() = default; + +void MapRenderer::reset() { + destroyed = true; + + if (renderer) { + // Make sure to destroy the renderer on the GL Thread + auto self = ActorRef(*this, mailbox); + self.ask(&MapRenderer::resetRenderer).wait(); + } + + // Lock to make sure there is no concurrent initialisation on the gl thread + std::lock_guard lock(initialisationMutex); + rendererObserver.reset(); +} + +ActorRef MapRenderer::actor() const { + return *rendererRef; +} + +void MapRenderer::schedule(std::weak_ptr scheduled) { + // Create a runnable + android::UniqueEnv _env = android::AttachEnv(); + auto runnable = std::make_unique(*_env, std::move(scheduled)); + + // Obtain ownership of the peer (gets transferred to the MapRenderer on the JVM for later GC) + auto peer = runnable->peer(); + + // Queue the event on the Java Peer + static auto& javaClass = jni::Class::Singleton(*_env); + static auto queueEvent = javaClass.GetMethod)>(*_env, "queueEvent"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, queueEvent, peer); + } + + // Release the c++ peer as it will be destroyed on GC of the Java Peer + runnable.release(); +} + +void MapRenderer::requestRender() { + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onInvalidate = javaClass.GetMethod(*_env, "requestRender"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onInvalidate); + } +} + +void MapRenderer::update(std::shared_ptr params) { + // Lock on the parameters + std::lock_guard lock(updateMutex); + updateParameters = std::move(params); +} + +void MapRenderer::setObserver(std::shared_ptr _rendererObserver) { + // Lock as the initialization can come from the main thread or the GL thread first + std::lock_guard lock(initialisationMutex); + + rendererObserver = std::move(_rendererObserver); + + // Set the new observer on the Renderer implementation + if (renderer) { + renderer->setObserver(rendererObserver.get()); + } +} + +void MapRenderer::requestSnapshot(SnapshotCallback callback) { + auto self = ActorRef(*this, mailbox); + self.invoke( + &MapRenderer::scheduleSnapshot, + std::make_unique([&, callback=std::move(callback), runloop=util::RunLoop::Get()](PremultipliedImage image) { + runloop->invoke([callback=std::move(callback), image=std::move(image), renderer=std::move(this)]() mutable { + if (renderer && !renderer->destroyed) { + callback(std::move(image)); + } + }); + snapshotCallback.reset(); + }) + ); +} + +// Called on OpenGL thread // + +void MapRenderer::resetRenderer() { + renderer.reset(); +} + +void MapRenderer::scheduleSnapshot(std::unique_ptr callback) { + snapshotCallback = std::move(callback); + requestRender(); +} + +void MapRenderer::render(JNIEnv&) { + assert (renderer); + + std::shared_ptr params; + { + // Lock on the parameters + std::unique_lock lock(updateMutex); + if (!updateParameters) return; + + // Hold on to the update parameters during render + params = updateParameters; + } + + // Activate the backend + gfx::BackendScope backendGuard { *backend }; + + // Ensure that the "current" scheduler on the render thread is + // this scheduler. + Scheduler::SetCurrent(this); + + if (framebufferSizeChanged) { + backend->updateViewPort(); + framebufferSizeChanged = false; + } + + renderer->render(*params); + + // Deliver the snapshot if requested + if (snapshotCallback) { + snapshotCallback->operator()(backend->readFramebuffer()); + snapshotCallback.reset(); + } +} + +void MapRenderer::onSurfaceCreated(JNIEnv&) { + // Lock as the initialization can come from the main thread or the GL thread first + std::lock_guard lock(initialisationMutex); + + // The GL context is already active if get a new surface. + gfx::BackendScope backendGuard { *backend, gfx::BackendScope::ScopeType::Implicit }; + + // The android system will have already destroyed the underlying + // GL resources if this is not the first initialization and an + // attempt to clean them up will fail + if (backend) backend->markContextLost(); + if (renderer) renderer->markContextLost(); + + // Reset in opposite order + renderer.reset(); + backend.reset(); + + // Create the new backend and renderer + backend = std::make_unique(); + renderer = std::make_unique(*backend, pixelRatio, localIdeographFontFamily); + rendererRef = std::make_unique>(*renderer, mailbox); + + // Set the observer on the new Renderer implementation + if (rendererObserver) { + renderer->setObserver(rendererObserver.get()); + } +} + +void MapRenderer::onSurfaceChanged(JNIEnv& env, jint width, jint height) { + if (!renderer) { + // In case the surface has been destroyed (due to app back-grounding) + onSurfaceCreated(env); + } + + backend->resizeFramebuffer(width, height); + framebufferSizeChanged = true; + requestRender(); +} + +void MapRenderer::onRendererReset(JNIEnv&) { + // Make sure to destroy the renderer on the GL Thread + auto self = ActorRef(*this, mailbox); + self.ask(&MapRenderer::resetRenderer).wait(); +} + +// needs to be called on GL thread +void MapRenderer::onSurfaceDestroyed(JNIEnv&) { + resetRenderer(); +} + +// Static methods // + +void MapRenderer::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + +#define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer(env, javaClass, "nativePtr", + jni::MakePeer&, jni::jfloat, const jni::String&>, + "nativeInitialize", "finalize", + METHOD(&MapRenderer::render, "nativeRender"), + METHOD(&MapRenderer::onRendererReset, "nativeReset"), + METHOD(&MapRenderer::onSurfaceCreated, + "nativeOnSurfaceCreated"), + METHOD(&MapRenderer::onSurfaceChanged, + "nativeOnSurfaceChanged"), + METHOD(&MapRenderer::onSurfaceDestroyed, + "nativeOnSurfaceDestroyed")); +} + +MapRenderer& MapRenderer::getNativePeer(JNIEnv& env, const jni::Object& jObject) { + static auto& javaClass = jni::Class::Singleton(env); + static auto field = javaClass.GetField(env, "nativePtr"); + MapRenderer* mapRenderer = reinterpret_cast(jObject.Get(env, field)); + assert(mapRenderer != nullptr); + return *mapRenderer; +} + +} // namespace android +} // namespace mbgl diff --git a/src/map_renderer.hpp b/src/map_renderer.hpp new file mode 100644 index 000000000..5a8ddeeb9 --- /dev/null +++ b/src/map_renderer.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace mbgl { + +template +class ActorRef; +class Mailbox; +class Renderer; +class RendererBackend; +class RendererObserver; +class ThreadPool; +class UpdateParameters; + +namespace android { + +class AndroidRendererBackend; + +/** + * The MapRenderer is a peer class that encapsulates the actions + * performed on the GL Thread. + * + * The public methods are safe to call from the main thread, others are not. + */ +class MapRenderer : public Scheduler { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/maps/renderer/MapRenderer"; }; + + static void registerNative(jni::JNIEnv&); + + static MapRenderer& getNativePeer(JNIEnv&, const jni::Object&); + + MapRenderer(jni::JNIEnv& _env, + const jni::Object&, + jni::jfloat pixelRatio, + const jni::String& localIdeographFontFamily); + + ~MapRenderer() override; + + // Resets the renderer to clean up on the calling thread + void reset(); + + // Takes the RendererObserver by shared_ptr so we + // don't have to make the header public. Use + // this instead of Renderer#setObserver directly + void setObserver(std::shared_ptr); + + // Sets the new update parameters to use on subsequent + // renders. Be sure to trigger a render with + // requestRender(). + void update(std::shared_ptr); + + // Gives a handle to the Renderer to enable actions on + // any thread. + ActorRef actor() const; + + // From Scheduler. Schedules by using callbacks to the + // JVM to process the mailbox on the right thread. + void schedule(std::weak_ptr scheduled) override; + + void requestRender(); + + // Snapshot - requires a RunLoop on the calling thread + using SnapshotCallback = std::function; + void requestSnapshot(SnapshotCallback); + +protected: + // Called from the GL Thread // + + void scheduleSnapshot(std::unique_ptr); + +private: + // Called from the GL Thread // + + // Resets the renderer + void resetRenderer(); + + // Renders a frame. + void render(JNIEnv&); + + void onSurfaceCreated(JNIEnv&); + + void onSurfaceChanged(JNIEnv&, jint width, jint height); + + void onSurfaceDestroyed(JNIEnv&); + +private: + // Called on either Main or GL thread // + void onRendererReset(JNIEnv&); + +private: + jni::WeakReference, jni::EnvAttachingDeleter> javaPeer; + + float pixelRatio; + optional localIdeographFontFamily; + + std::shared_ptr threadPool; + std::shared_ptr mailbox; + + std::mutex initialisationMutex; + std::shared_ptr rendererObserver; + + std::unique_ptr backend; + std::unique_ptr renderer; + std::unique_ptr> rendererRef; + + std::shared_ptr updateParameters; + std::mutex updateMutex; + + bool framebufferSizeChanged = false; + std::atomic destroyed {false}; + + std::unique_ptr snapshotCallback; +}; + +} // namespace android +} // namespace mbgl diff --git a/src/map_renderer_runnable.cpp b/src/map_renderer_runnable.cpp new file mode 100644 index 000000000..77c3aa301 --- /dev/null +++ b/src/map_renderer_runnable.cpp @@ -0,0 +1,50 @@ +#include "map_renderer_runnable.hpp" + +#include + +namespace mbgl { +namespace android { + +MapRendererRunnable::MapRendererRunnable(jni::JNIEnv& env, std::weak_ptr mailbox_) + : mailbox(std::move(mailbox_)) { + + // Create the Java peer and hold on to a global reference + // Not using a weak reference here as this might oerflow + // the weak reference table on some devices + jni::UniqueLocalFrame frame = jni::PushLocalFrame(env, 5); + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + auto instance = javaClass.New(env, constructor, reinterpret_cast(this)); + javaPeer = jni::NewGlobal(env, instance); +} + +MapRendererRunnable::~MapRendererRunnable() = default; + +void MapRendererRunnable::run(jni::JNIEnv&) { + Mailbox::maybeReceive(mailbox); +} + +jni::Global> MapRendererRunnable::peer() { + return std::move(javaPeer); +} + +// Static methods // + +void MapRendererRunnable::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + +#define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + jni::RegisterNativePeer( + env, + javaClass, + "nativePtr", + std::make_unique, + "nativeInitialize", + "finalize", + METHOD(&MapRendererRunnable::run, "run")); +} + +} // namespace android +} // namespace mbgl diff --git a/src/map_renderer_runnable.hpp b/src/map_renderer_runnable.hpp new file mode 100644 index 000000000..21c4369b6 --- /dev/null +++ b/src/map_renderer_runnable.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +#include +#include + +#include + +namespace mbgl { +namespace android { + +/** + * The MapRendererRunnable is a peer class that encapsulates + * a scheduled mailbox in a Java Runnable so it can be + * scheduled on the map renderer thread. + * + */ +class MapRendererRunnable { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/maps/renderer/MapRendererRunnable"; }; + + static void registerNative(jni::JNIEnv&); + + MapRendererRunnable(jni::JNIEnv&, std::weak_ptr); + + // Only for jni registration, unused + MapRendererRunnable(jni::JNIEnv&) { + assert(false); + } + + ~MapRendererRunnable(); + + void run(jni::JNIEnv&); + + // Transfers ownership of the Peer object to the caller + jni::Global> peer(); + +private: + jni::Global> javaPeer; + std::weak_ptr mailbox; +}; + +} // namespace android +} // namespace mbgl diff --git a/src/native_map_view.cpp b/src/native_map_view.cpp new file mode 100755 index 000000000..8ef757de1 --- /dev/null +++ b/src/native_map_view.cpp @@ -0,0 +1,1153 @@ +#include "native_map_view.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Java -> C++ conversion +#include "style/android_conversion.hpp" +#include +#include + +// C++ -> Java conversion +#include "conversion/conversion.hpp" +#include "conversion/collection.hpp" +#include "style/conversion/filter.hpp" +#include "geojson/feature.hpp" + +#include "jni.hpp" +#include "attach_env.hpp" +#include "map_renderer.hpp" +#include "android_renderer_frontend.hpp" +#include "file_source.hpp" +#include "bitmap.hpp" +#include "run_loop_impl.hpp" +#include "java/util.hpp" +#include "geometry/lat_lng_bounds.hpp" +#include "map/camera_position.hpp" +#include "map/image.hpp" +#include "style/light.hpp" +#include "bitmap_factory.hpp" + +namespace mbgl { +namespace android { + +NativeMapView::NativeMapView(jni::JNIEnv& _env, + const jni::Object& _obj, + const jni::Object& jFileSource, + const jni::Object& jMapRenderer, + jni::jfloat _pixelRatio, + jni::jboolean _crossSourceCollisions) + : javaPeer(_env, _obj) + , mapRenderer(MapRenderer::getNativePeer(_env, jMapRenderer)) + , pixelRatio(_pixelRatio) { + + // Get a reference to the JavaVM for callbacks + if (_env.GetJavaVM(&vm) < 0) { + _env.ExceptionDescribe(); + return; + } + + // Create a renderer frontend + rendererFrontend = std::make_unique(mapRenderer); + + // Create Map options + MapOptions options; + options.withMapMode(MapMode::Continuous) + .withSize(mbgl::Size{ static_cast(width), static_cast(height) }) + .withPixelRatio(pixelRatio) + .withConstrainMode(ConstrainMode::HeightOnly) + .withViewportMode(ViewportMode::Default) + .withCrossSourceCollisions(_crossSourceCollisions); + + // Create the core map + map = std::make_unique( + *rendererFrontend, *this, options, + mbgl::android::FileSource::getSharedResourceOptions(_env, jFileSource)); +} + +/** + * Called through NativeMapView#destroy() + */ +NativeMapView::~NativeMapView() { + map.reset(); + vm = nullptr; +} + +void NativeMapView::onCameraWillChange(MapObserver::CameraChangeMode mode) { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onCameraWillChange = javaClass.GetMethod(*_env, "onCameraWillChange"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onCameraWillChange, (jboolean) (mode != MapObserver::CameraChangeMode::Immediate)); + } +} + + +void NativeMapView::onCameraIsChanging() { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onCameraIsChanging = javaClass.GetMethod(*_env, "onCameraIsChanging"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onCameraIsChanging); + } +} + +void NativeMapView::onCameraDidChange(MapObserver::CameraChangeMode mode) { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onCameraDidChange = javaClass.GetMethod(*_env, "onCameraDidChange"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onCameraDidChange, + (jboolean) (mode != MapObserver::CameraChangeMode::Immediate)); + } +} + +void NativeMapView::onWillStartLoadingMap() { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onWillStartLoadingMap = javaClass.GetMethod(*_env, "onWillStartLoadingMap"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onWillStartLoadingMap); + } +} + +void NativeMapView::onDidFinishLoadingMap() { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onDidFinishLoadingMap = javaClass.GetMethod(*_env, "onDidFinishLoadingMap"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onDidFinishLoadingMap); + } +} + +void NativeMapView::onDidFailLoadingMap(MapLoadError, const std::string& error) { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onDidFailLoadingMap = javaClass.GetMethod(*_env, "onDidFailLoadingMap"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onDidFailLoadingMap, jni::Make(*_env, error)); + } +} + +void NativeMapView::onWillStartRenderingFrame() { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onWillStartRenderingFrame = javaClass.GetMethod(*_env, "onWillStartRenderingFrame"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onWillStartRenderingFrame); + } +} + +void NativeMapView::onDidFinishRenderingFrame(MapObserver::RenderMode mode, bool) { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onDidFinishRenderingFrame = javaClass.GetMethod(*_env, "onDidFinishRenderingFrame"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onDidFinishRenderingFrame, (jboolean) (mode != MapObserver::RenderMode::Partial)); + } +} + + +void NativeMapView::onWillStartRenderingMap() { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onWillStartRenderingMap = javaClass.GetMethod(*_env, "onWillStartRenderingMap"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onWillStartRenderingMap); + } +} + +void NativeMapView::onDidFinishRenderingMap(MapObserver::RenderMode mode) { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onDidFinishRenderingMap = javaClass.GetMethod(*_env, "onDidFinishRenderingMap"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onDidFinishRenderingMap, (jboolean) (mode != MapObserver::RenderMode::Partial)); + } +} + +void NativeMapView::onDidBecomeIdle() { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onDidBecomeIdle = javaClass.GetMethod(*_env, "onDidBecomeIdle"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onDidBecomeIdle); + } +} + +void NativeMapView::onDidFinishLoadingStyle() { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onDidFinishLoadingStyle = javaClass.GetMethod(*_env, "onDidFinishLoadingStyle"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onDidFinishLoadingStyle); + } +} + +void NativeMapView::onSourceChanged(mbgl::style::Source& source) { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onSourceChanged = javaClass.GetMethod(*_env, "onSourceChanged"); + auto sourceId = jni::Make(*_env, source.getID()); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onSourceChanged, sourceId); + } +} + +void NativeMapView::onStyleImageMissing(const std::string& imageId) { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onStyleImageMissing = javaClass.GetMethod(*_env, "onStyleImageMissing"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onStyleImageMissing, jni::Make(*_env, imageId)); + } +} + +bool NativeMapView::onCanRemoveUnusedStyleImage(const std::string& imageId) { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onCanRemoveUnusedStyleImage = javaClass.GetMethod(*_env, "onCanRemoveUnusedStyleImage"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + return weakReference.Call(*_env, onCanRemoveUnusedStyleImage, jni::Make(*_env, imageId)); + } + + return true; +} + +// JNI Methods // + +void NativeMapView::resizeView(jni::JNIEnv&, int w, int h) { + width = util::max(64, w); + height = util::max(64, h); + map->setSize({ static_cast(width), static_cast(height) }); +} + +jni::Local NativeMapView::getStyleUrl(jni::JNIEnv& env) { + return jni::Make(env, map->getStyle().getURL()); +} + +void NativeMapView::setStyleUrl(jni::JNIEnv& env, const jni::String& url) { + map->getStyle().loadURL(jni::Make(env, url)); +} + +jni::Local NativeMapView::getStyleJson(jni::JNIEnv& env) { + return jni::Make(env, map->getStyle().getJSON()); +} + +void NativeMapView::setStyleJson(jni::JNIEnv& env, const jni::String& json) { + map->getStyle().loadJSON(jni::Make(env, json)); +} + +void NativeMapView::setLatLngBounds(jni::JNIEnv& env, const jni::Object& jBounds) { + mbgl::BoundOptions bounds; + if (jBounds) { + bounds.withLatLngBounds(mbgl::android::LatLngBounds::getLatLngBounds(env, jBounds)); + } else { + bounds.withLatLngBounds(mbgl::LatLngBounds::unbounded()); + } + map->setBounds(bounds); +} + +void NativeMapView::cancelTransitions(jni::JNIEnv&) { + map->cancelTransitions(); +} + +void NativeMapView::setGestureInProgress(jni::JNIEnv&, jni::jboolean inProgress) { + map->setGestureInProgress(inProgress); +} + +void NativeMapView::moveBy(jni::JNIEnv&, jni::jdouble dx, jni::jdouble dy, jni::jlong duration) { + mbgl::AnimationOptions animationOptions; + if (duration > 0) { + animationOptions.duration.emplace(mbgl::Milliseconds(duration)); + animationOptions.easing.emplace(mbgl::util::UnitBezier {0.25, 0.46, 0.45, 0.94}); + } + map->moveBy({dx, dy}, animationOptions); +} + +void NativeMapView::jumpTo(jni::JNIEnv& env, jni::jdouble bearing, jni::jdouble latitude, jni::jdouble longitude, jni::jdouble pitch, jni::jdouble zoom, const jni::Array& padding) { + mbgl::CameraOptions options; + if (bearing != -1) { + options.bearing = bearing; + } + options.center = mbgl::LatLng(latitude, longitude); + if (padding) { + assert(padding.Length(env) == 4); + options.padding = mbgl::EdgeInsets{padding.Get(env, 0), padding.Get(env, 1), + padding.Get(env, 2), padding.Get(env, 3)}; + } + if (pitch != -1) { + options.pitch = pitch; + } + if (zoom != -1) { + options.zoom = zoom; + } + + map->jumpTo(options); +} + +void NativeMapView::easeTo(jni::JNIEnv& env, jni::jdouble bearing, jni::jdouble latitude, jni::jdouble longitude, jni::jlong duration, jni::jdouble pitch, jni::jdouble zoom, const jni::Array& padding, jni::jboolean easing) { + mbgl::CameraOptions cameraOptions; + if (bearing != -1) { + cameraOptions.bearing = bearing; + } + cameraOptions.center = mbgl::LatLng(latitude, longitude); + if (padding) { + assert(padding.Length(env) == 4); + cameraOptions.padding = mbgl::EdgeInsets{padding.Get(env, 0), padding.Get(env, 1), + padding.Get(env, 2), padding.Get(env, 3)}; + } + if (pitch != -1) { + cameraOptions.pitch = pitch; + } + if (zoom != -1) { + cameraOptions.zoom = zoom; + } + + mbgl::AnimationOptions animationOptions; + animationOptions.duration.emplace(mbgl::Milliseconds(duration)); + if (!easing) { + // add a linear interpolator instead of easing + animationOptions.easing.emplace(mbgl::util::UnitBezier { 0, 0, 1, 1 }); + } + + map->easeTo(cameraOptions, animationOptions); +} + +void NativeMapView::flyTo(jni::JNIEnv& env, jni::jdouble bearing, jni::jdouble latitude, jni::jdouble longitude, jni::jlong duration, jni::jdouble pitch, jni::jdouble zoom, const jni::Array& padding) { + mbgl::CameraOptions cameraOptions; + if (bearing != -1) { + cameraOptions.bearing = bearing; + } + cameraOptions.center = mbgl::LatLng(latitude, longitude); + if (padding) { + assert(padding.Length(env) == 4); + cameraOptions.padding = mbgl::EdgeInsets{padding.Get(env, 0), padding.Get(env, 1), + padding.Get(env, 2), padding.Get(env, 3)}; + } + if (pitch != -1) { + cameraOptions.pitch = pitch; + } + if (zoom != -1) { + cameraOptions.zoom = zoom; + } + + mbgl::AnimationOptions animationOptions; + animationOptions.duration.emplace(mbgl::Milliseconds(duration)); + map->flyTo(cameraOptions, animationOptions); +} + +jni::Local> NativeMapView::getLatLng(JNIEnv& env) { + return LatLng::New(env, *map->getCameraOptions(mbgl::nullopt).center); +} + +void NativeMapView::setLatLng(jni::JNIEnv& env, jni::jdouble latitude, jni::jdouble longitude, const jni::Array& padding, jni::jlong duration) { + mbgl::CameraOptions cameraOptions; + cameraOptions.center = mbgl::LatLng(latitude, longitude); + if (padding) { + assert(padding.Length(env) == 4); + cameraOptions.padding = mbgl::EdgeInsets{padding.Get(env, 0), padding.Get(env, 1), + padding.Get(env, 2), padding.Get(env, 3)}; + } + map->easeTo(cameraOptions, mbgl::AnimationOptions{mbgl::Milliseconds(duration)}); +} + +jni::Local> NativeMapView::getCameraForLatLngBounds(jni::JNIEnv& env, const jni::Object& jBounds, double top, double left, double bottom, double right, double bearing, double tilt) { + mbgl::EdgeInsets padding = {top, left, bottom, right}; + return CameraPosition::New(env, map->cameraForLatLngBounds(mbgl::android::LatLngBounds::getLatLngBounds(env, jBounds), padding, bearing, tilt), pixelRatio); +} + +jni::Local> NativeMapView::getCameraForGeometry(jni::JNIEnv& env, const jni::Object& jGeometry, double top, double left, double bottom, double right, double bearing, double tilt) { + auto geometry = geojson::Geometry::convert(env, jGeometry); + mbgl::EdgeInsets padding = {top, left, bottom, right}; + return CameraPosition::New(env, map->cameraForGeometry(geometry, padding, bearing, tilt), pixelRatio); +} + +void NativeMapView::setReachability(jni::JNIEnv&, jni::jboolean reachable) { + if (reachable) { + mbgl::NetworkStatus::Reachable(); + } +} + +void NativeMapView::resetPosition(jni::JNIEnv&) { + map->jumpTo(mbgl::CameraOptions().withCenter(mbgl::LatLng {}).withZoom(0.0).withBearing(0.0).withPitch(0.0)); +} + +jni::jdouble NativeMapView::getPitch(jni::JNIEnv&) { + return *map->getCameraOptions().pitch; +} + +void NativeMapView::setPitch(jni::JNIEnv&, jni::jdouble pitch, jni::jlong duration) { + map->easeTo(mbgl::CameraOptions().withPitch(pitch), + mbgl::AnimationOptions{ mbgl::Milliseconds(duration) }); +} + +void NativeMapView::setZoom(jni::JNIEnv&, jni::jdouble zoom, jni::jdouble x, jni::jdouble y, jni::jlong duration) { + map->easeTo(mbgl::CameraOptions().withZoom(zoom).withAnchor(mbgl::ScreenCoordinate{ x, y }), + mbgl::AnimationOptions{ mbgl::Milliseconds(duration) }); +} + +jni::jdouble NativeMapView::getZoom(jni::JNIEnv&) { + return *map->getCameraOptions().zoom; +} + +void NativeMapView::resetZoom(jni::JNIEnv&) { + map->jumpTo(mbgl::CameraOptions().withZoom(0.0)); +} + +void NativeMapView::setMinZoom(jni::JNIEnv&, jni::jdouble zoom) { + map->setBounds(BoundOptions().withMinZoom(zoom)); +} + +jni::jdouble NativeMapView::getMinZoom(jni::JNIEnv&) { + return *map->getBounds().minZoom; +} + +void NativeMapView::setMaxZoom(jni::JNIEnv&, jni::jdouble zoom) { + map->setBounds(BoundOptions().withMaxZoom(zoom)); +} + +jni::jdouble NativeMapView::getMaxZoom(jni::JNIEnv&) { + return *map->getBounds().maxZoom; +} + +void NativeMapView::rotateBy(jni::JNIEnv&, jni::jdouble sx, jni::jdouble sy, jni::jdouble ex, jni::jdouble ey, jni::jlong duration) { + mbgl::ScreenCoordinate first(sx, sy); + mbgl::ScreenCoordinate second(ex, ey); + map->rotateBy(first, second, mbgl::AnimationOptions{mbgl::Milliseconds(duration)}); +} + +void NativeMapView::setBearing(jni::JNIEnv&, jni::jdouble degrees, jni::jlong duration) { + map->easeTo(mbgl::CameraOptions().withBearing(degrees), mbgl::AnimationOptions{mbgl::Milliseconds(duration)}); +} + +void NativeMapView::setBearingXY(jni::JNIEnv&, jni::jdouble degrees, jni::jdouble cx, jni::jdouble cy, jni::jlong duration) { + mbgl::ScreenCoordinate anchor(cx, cy); + map->easeTo(mbgl::CameraOptions().withBearing(degrees).withAnchor(anchor), mbgl::AnimationOptions{mbgl::Milliseconds(duration)}); +} + +jni::jdouble NativeMapView::getBearing(jni::JNIEnv&) { + return *map->getCameraOptions().bearing; +} + +void NativeMapView::resetNorth(jni::JNIEnv&) { + map->easeTo(mbgl::CameraOptions().withBearing(0.0), mbgl::AnimationOptions {{mbgl::Milliseconds(500)}}); +} + +void NativeMapView::setVisibleCoordinateBounds(JNIEnv& env, const jni::Array>& coordinates, const jni::Object& padding, jdouble direction, jni::jlong duration) { + NullCheck(env, &coordinates); + std::size_t count = coordinates.Length(env); + + std::vector latLngs; + latLngs.reserve(count); + + for (std::size_t i = 0; i < count; i++) { + latLngs.push_back(LatLng::getLatLng(env, coordinates.Get(env, i))); + } + + mbgl::EdgeInsets mbglInsets = { RectF::getTop(env, padding), RectF::getLeft(env, padding), RectF::getBottom(env, padding), RectF::getRight(env, padding) }; + mbgl::CameraOptions cameraOptions = map->cameraForLatLngs(latLngs, mbglInsets); + if (direction >= 0) { + cameraOptions.bearing = direction; + } + + mbgl::AnimationOptions animationOptions; + if (duration > 0) { + animationOptions.duration.emplace(mbgl::Milliseconds(duration)); + // equivalent to kCAMediaTimingFunctionDefault in iOS + animationOptions.easing.emplace(mbgl::util::UnitBezier { 0.25, 0.1, 0.25, 0.1 }); + } + + map->easeTo(cameraOptions, animationOptions); +} + +void NativeMapView::scheduleSnapshot(jni::JNIEnv&) { + mapRenderer.requestSnapshot([&](PremultipliedImage image) { + auto _env = android::AttachEnv(); + // Convert image to bitmap + auto bitmap = Bitmap::CreateBitmap(*_env, std::move(image)); + + // invoke Mapview#OnSnapshotReady + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onSnapshotReady = javaClass.GetMethod)>(*_env, "onSnapshotReady"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onSnapshotReady, bitmap); + } + }); +} + +jni::Local> NativeMapView::getCameraPosition(jni::JNIEnv& env) { + return CameraPosition::New(env, map->getCameraOptions(mbgl::nullopt), pixelRatio); +} + +void NativeMapView::updateMarker(jni::JNIEnv& env, jni::jlong markerId, jni::jdouble lat, jni::jdouble lon, const jni::String& jid) { + if (markerId == -1) { + return; + } + + std::string iconId = jni::Make(env, jid); + // Because Java only has int, not unsigned int, we need to bump the annotation id up to a long. + map->updateAnnotation(markerId, mbgl::SymbolAnnotation { mbgl::Point(lon, lat), iconId }); +} + +jni::Local> NativeMapView::addMarkers(jni::JNIEnv& env, const jni::Array>& jmarkers) { + jni::NullCheck(env, &jmarkers); + std::size_t len = jmarkers.Length(env); + + std::vector ids; + ids.reserve(len); + + for (std::size_t i = 0; i < len; i++) { + auto marker = jmarkers.Get(env, i); + ids.push_back(map->addAnnotation(mbgl::SymbolAnnotation { + Marker::getPosition(env, marker), + Marker::getIconId(env, marker) + })); + } + + auto result = jni::Array::New(env, len); + result.SetRegion>(env, 0, ids); + + return result; +} + +void NativeMapView::onLowMemory(JNIEnv&) { + rendererFrontend->reduceMemoryUse(); +} + +using DebugOptions = mbgl::MapDebugOptions; + +void NativeMapView::setDebug(JNIEnv&, jni::jboolean debug) { + DebugOptions debugOptions = debug ? DebugOptions::TileBorders | DebugOptions::ParseStatus | DebugOptions::Collision + : DebugOptions::NoDebug; + map->setDebug(debugOptions); +} + +void NativeMapView::cycleDebugOptions(JNIEnv&) { + map->cycleDebugOptions(); +} + +jni::jboolean NativeMapView::getDebug(JNIEnv&) { + return map->getDebug() != DebugOptions::NoDebug; +} + +jni::jboolean NativeMapView::isFullyLoaded(JNIEnv&) { + return map->isFullyLoaded(); +} + +jni::jdouble NativeMapView::getMetersPerPixelAtLatitude(JNIEnv&, jni::jdouble lat, jni::jdouble zoom) { + return mbgl::Projection::getMetersPerPixelAtLatitude(lat, zoom); +} + +jni::Local> NativeMapView::projectedMetersForLatLng(JNIEnv& env, jni::jdouble latitude, jni::jdouble longitude) { + mbgl::ProjectedMeters projectedMeters = mbgl::Projection::projectedMetersForLatLng(mbgl::LatLng(latitude, longitude)); + return ProjectedMeters::New(env, projectedMeters.northing(), projectedMeters.easting()); +} + +jni::Local> NativeMapView::latLngForProjectedMeters(JNIEnv& env, jdouble northing, jdouble easting) { + return LatLng::New(env, mbgl::Projection::latLngForProjectedMeters(mbgl::ProjectedMeters(northing, easting))); +} + +jni::Local> NativeMapView::pixelForLatLng(JNIEnv& env, jdouble latitude, jdouble longitude) { + mbgl::ScreenCoordinate pixel = map->pixelForLatLng(mbgl::LatLng(latitude, longitude)); + return PointF::New(env, static_cast(pixel.x), static_cast(pixel.y)); +} + +jni::Local> NativeMapView::latLngForPixel(JNIEnv& env, jfloat x, jfloat y) { + return LatLng::New(env, map->latLngForPixel(mbgl::ScreenCoordinate(x, y))); +} + +jni::Local> NativeMapView::addPolylines(JNIEnv& env, const jni::Array>& polylines) { + NullCheck(env, &polylines); + std::size_t len = polylines.Length(env); + + std::vector ids; + ids.reserve(len); + + for (std::size_t i = 0; i < len; i++) { + mbgl::LineAnnotation annotation = Polyline::toAnnotation(env, polylines.Get(env, i)); + ids.push_back(map->addAnnotation(annotation)); + } + + auto result = jni::Array::New(env, len); + result.SetRegion>(env, 0, ids); + + return result; +} + + +jni::Local> NativeMapView::addPolygons(JNIEnv& env, const jni::Array>& polygons) { + NullCheck(env, &polygons); + std::size_t len = polygons.Length(env); + + std::vector ids; + ids.reserve(len); + + for (std::size_t i = 0; i < len; i++) { + mbgl::FillAnnotation annotation = Polygon::toAnnotation(env, polygons.Get(env, i)); + ids.push_back(map->addAnnotation(annotation)); + } + + auto result = jni::Array::New(env, len); + result.SetRegion>(env, 0, ids); + + return result; +} + +void NativeMapView::updatePolyline(JNIEnv& env, jlong polylineId, const jni::Object& polyline) { + mbgl::LineAnnotation annotation = Polyline::toAnnotation(env, polyline); + map->updateAnnotation(polylineId, annotation); +} + +void NativeMapView::updatePolygon(JNIEnv& env, jlong polygonId, const jni::Object& polygon) { + mbgl::FillAnnotation annotation = Polygon::toAnnotation(env, polygon); + map->updateAnnotation(polygonId, annotation); +} + +void NativeMapView::removeAnnotations(JNIEnv& env, const jni::Array& ids) { + NullCheck(env, &ids); + std::size_t len = ids.Length(env); + auto elements = jni::GetArrayElements(env, *ids); + jlong* jids = std::get<0>(elements).get(); + + for (std::size_t i = 0; i < len; i++) { + if(jids[i] == -1L) { + continue; + } + map->removeAnnotation(jids[i]); + } +} + +void NativeMapView::addAnnotationIcon(JNIEnv& env, const jni::String& symbol, jint w, jint h, jfloat scale, const jni::Array& jpixels) { + const std::string symbolName = jni::Make(env, symbol); + + NullCheck(env, &jpixels); + std::size_t size = jpixels.Length(env); + + mbgl::PremultipliedImage premultipliedImage({ static_cast(w), static_cast(h) }); + if (premultipliedImage.bytes() != uint32_t(size)) { + throw mbgl::util::SpriteImageException("Sprite image pixel count mismatch"); + } + + jni::GetArrayRegion(env, *jpixels, 0, size, reinterpret_cast(premultipliedImage.data.get())); + map->addAnnotationImage(std::make_unique( + symbolName, std::move(premultipliedImage), float(scale))); +} + +void NativeMapView::removeAnnotationIcon(JNIEnv& env, const jni::String& symbol) { + const std::string symbolName = jni::Make(env, symbol); + map->removeAnnotationImage(symbolName); +} + +jdouble NativeMapView::getTopOffsetPixelsForAnnotationSymbol(JNIEnv& env, const jni::String& symbolName) { + return map->getTopOffsetPixelsForAnnotationImage(jni::Make(env, symbolName)); +} + +jni::Local> NativeMapView::getTransitionOptions(JNIEnv& env) { + const auto transitionOptions = map->getStyle().getTransitionOptions(); + const auto duration = std::chrono::duration_cast(transitionOptions.duration.value_or(mbgl::Duration::zero())).count(); + const auto delay = std::chrono::duration_cast(transitionOptions.delay.value_or(mbgl::Duration::zero())).count(); + const auto enablePlacementTransitions = (jboolean) transitionOptions.enablePlacementTransitions; + return TransitionOptions::fromTransitionOptions(env, duration, delay, enablePlacementTransitions); +} + +void NativeMapView::setTransitionOptions(JNIEnv& env, const jni::Object& options) { + const mbgl::style::TransitionOptions transitionOptions( + Duration(mbgl::Milliseconds(TransitionOptions::getDuration(env, options))), + Duration(mbgl::Milliseconds(TransitionOptions::getDelay(env, options))), + TransitionOptions::isEnablePlacementTransitions(env, options) + ); + map->getStyle().setTransitionOptions(transitionOptions); +} + +jni::Local> NativeMapView::queryPointAnnotations(JNIEnv& env, const jni::Object& rect) { + using namespace mbgl::style; + using namespace mbgl::style::conversion; + + // Convert input + mbgl::ScreenBox box = { + { RectF::getLeft(env, rect), RectF::getTop(env, rect) }, + { RectF::getRight(env, rect), RectF::getBottom(env, rect) }, + }; + + // Assume only points for now + mbgl::AnnotationIDs ids = rendererFrontend->queryPointAnnotations(box); + + // Convert result + std::vector longIds(ids.begin(), ids.end()); + auto result = jni::Array::New(env, ids.size()); + result.SetRegion>(env, 0, longIds); + + return result; +} + +jni::Local> NativeMapView::queryShapeAnnotations(JNIEnv& env, const jni::Object& rect) { + using namespace mbgl::style; + using namespace mbgl::style::conversion; + + // Convert input + mbgl::ScreenBox box = { + {RectF::getLeft(env, rect), RectF::getTop(env, rect)}, + {RectF::getRight(env, rect), RectF::getBottom(env, rect)}, + }; + + mbgl::AnnotationIDs ids = rendererFrontend->queryShapeAnnotations(box); + + // Convert result + std::vector longIds(ids.begin(), ids.end()); + auto result = jni::Array::New(env, ids.size()); + result.SetRegion>(env, 0, longIds); + + return result; +} + +jni::Local>> NativeMapView::queryRenderedFeaturesForPoint(JNIEnv& env, jni::jfloat x, jni::jfloat y, + const jni::Array& layerIds, + const jni::Array>& jfilter) { + using namespace mbgl::android::conversion; + using namespace mbgl::android::geojson; + + mbgl::optional> layers; + if (layerIds && layerIds.Length(env) > 0) { + layers = android::conversion::toVector(env, layerIds); + } + mapbox::geometry::point point = {x, y}; + + return Feature::convert( + env, + rendererFrontend->queryRenderedFeatures(point, { layers, toFilter(env, jfilter) })); +} + +jni::Local>> NativeMapView::queryRenderedFeaturesForBox(JNIEnv& env, jni::jfloat left, jni::jfloat top, + jni::jfloat right, jni::jfloat bottom, const jni::Array& layerIds, + const jni::Array>& jfilter) { + using namespace mbgl::android::conversion; + using namespace mbgl::android::geojson; + + mbgl::optional> layers; + if (layerIds && layerIds.Length(env) > 0) { + layers = toVector(env, layerIds); + } + mapbox::geometry::box box = { + mapbox::geometry::point{ left, top}, + mapbox::geometry::point{ right, bottom } + }; + + return Feature::convert( + env, + rendererFrontend->queryRenderedFeatures(box, { layers, toFilter(env, jfilter) })); +} + +jni::Local> NativeMapView::getLight(JNIEnv& env) { + mbgl::style::Light* light = map->getStyle().getLight(); + if (light) { + return Light::createJavaLightPeer(env, *map, *light); + } else { + return jni::Local>(); + } +} + +jni::Local>> NativeMapView::getLayers(JNIEnv& env) { + + // Get the core layers + std::vector layers = map->getStyle().getLayers(); + + // Convert + auto jLayers = jni::Array>::New(env, layers.size()); + int index = 0; + for (auto layer : layers) { + jLayers.Set(env, index, LayerManagerAndroid::get()->createJavaLayerPeer(env, *map, *layer)); + index++; + } + + return jLayers; +} + +jni::Local> NativeMapView::getLayer(JNIEnv& env, const jni::String& layerId) { + + // Find the layer + mbgl::style::Layer* coreLayer = map->getStyle().getLayer(jni::Make(env, layerId)); + if (!coreLayer) { + mbgl::Log::Debug(mbgl::Event::JNI, "No layer found"); + return jni::Local>(); + } + + // Create and return the layer's native peer + return LayerManagerAndroid::get()->createJavaLayerPeer(env, *map, *coreLayer); +} + +void NativeMapView::addLayer(JNIEnv& env, jlong nativeLayerPtr, const jni::String& before) { + assert(nativeLayerPtr != 0); + + Layer *layer = reinterpret_cast(nativeLayerPtr); + try { + layer->addToMap(*map, before ? mbgl::optional(jni::Make(env, before)) : mbgl::optional()); + } catch (const std::runtime_error& error) { + jni::ThrowNew(env, jni::FindClass(env, "com/mapbox/mapboxsdk/style/layers/CannotAddLayerException"), error.what()); + } +} + +void NativeMapView::addLayerAbove(JNIEnv& env, jlong nativeLayerPtr, const jni::String& above) { + assert(nativeLayerPtr != 0); + + Layer *layer = reinterpret_cast(nativeLayerPtr); + + // Find the sibling + auto layers = map->getStyle().getLayers(); + auto siblingId = jni::Make(env, above); + + size_t index = 0; + for (auto l : layers) { + if (l->getID() == siblingId) { + break; + } + index++; + } + + // Check if we found a sibling to place before + mbgl::optional before; + if (index + 1 > layers.size()) { + // Not found + jni::ThrowNew(env, jni::FindClass(env, "com/mapbox/mapboxsdk/style/layers/CannotAddLayerException"), + std::string("Could not find layer: ").append(siblingId).c_str()); + return; + } else if (index + 1 < layers.size()) { + // Place before the sibling + before = { layers.at(index + 1)->getID() }; + } + + // Add the layer + try { + layer->addToMap(*map, before); + } catch (const std::runtime_error& error) { + jni::ThrowNew(env, jni::FindClass(env, "com/mapbox/mapboxsdk/style/layers/CannotAddLayerException"), error.what()); + } +} + +void NativeMapView::addLayerAt(JNIEnv& env, jlong nativeLayerPtr, jni::jint index) { + assert(nativeLayerPtr != 0); + + Layer *layer = reinterpret_cast(nativeLayerPtr); + auto layers = map->getStyle().getLayers(); + + // Check index + int numLayers = layers.size() - 1; + if (index > numLayers || index < 0) { + Log::Error(Event::JNI, "Index out of range: %i", index); + jni::ThrowNew(env, jni::FindClass(env, "com/mapbox/mapboxsdk/style/layers/CannotAddLayerException"), + std::string("Invalid index").c_str()); + return; + } + + // Insert it below the current at that index + try { + layer->addToMap(*map, layers.at(index)->getID()); + } catch (const std::runtime_error& error) { + jni::ThrowNew(env, jni::FindClass(env, "com/mapbox/mapboxsdk/style/layers/CannotAddLayerException"), error.what()); + } +} + + +/** + * Remove layer at index. + */ +jni::jboolean NativeMapView::removeLayerAt(JNIEnv& env, jni::jint index) { + auto layers = map->getStyle().getLayers(); + + // Check index + int numLayers = layers.size() - 1; + if (index > numLayers || index < 0) { + Log::Warning(Event::JNI, "Index out of range: %i", index); + return jni::jni_false; + } + + std::unique_ptr coreLayer = map->getStyle().removeLayer(layers.at(index)->getID()); + if (coreLayer) { + jni::Local> layerObj = + LayerManagerAndroid::get()->createJavaLayerPeer(env, *map, std::move(coreLayer)); + return jni::jni_true; + } + return jni::jni_false; +} + +/** + * Remove with wrapper object id. Ownership is transferred back to the wrapper + */ +jni::jboolean NativeMapView::removeLayer(JNIEnv&, jlong layerPtr) { + assert(layerPtr != 0); + + mbgl::android::Layer *layer = reinterpret_cast(layerPtr); + std::unique_ptr coreLayer = map->getStyle().removeLayer(layer->get().getID()); + if (coreLayer) { + layer->setLayer(std::move(coreLayer)); + return jni::jni_true; + } + return jni::jni_false; +} + +jni::Local>> NativeMapView::getSources(JNIEnv& env) { + // Get the core sources + std::vector sources = map->getStyle().getSources(); + + // Convert + auto jSources = jni::Array>::New(env, sources.size()); + int index = 0; + for (auto source : sources) { + jSources.Set(env, index, Source::peerForCoreSource(env, *source, *rendererFrontend)); + index++; + } + + return jSources; +} + +jni::Local> NativeMapView::getSource(JNIEnv& env, const jni::String& sourceId) { + // Find the source + mbgl::style::Source* coreSource = map->getStyle().getSource(jni::Make(env, sourceId)); + if (!coreSource) { + mbgl::Log::Debug(mbgl::Event::JNI, "No source found"); + return jni::Local>(); + } + + // Create and return the source's native peer + return jni::NewLocal(env, Source::peerForCoreSource(env, *coreSource, *rendererFrontend)); +} + +void NativeMapView::addSource(JNIEnv& env, const jni::Object& obj, jlong sourcePtr) { + assert(sourcePtr != 0); + + Source *source = reinterpret_cast(sourcePtr); + try { + source->addToMap(env, obj, *map, *rendererFrontend); + } catch (const std::runtime_error& error) { + jni::ThrowNew(env, jni::FindClass(env, "com/mapbox/mapboxsdk/style/sources/CannotAddSourceException"), error.what()); + } +} + +jni::jboolean NativeMapView::removeSource(JNIEnv& env, const jni::Object& obj, jlong sourcePtr) { + assert(sourcePtr != 0); + + mbgl::android::Source *source = reinterpret_cast(sourcePtr); + if (source->removeFromMap(env, obj, *map)) { + source->releaseJavaPeer(); + return jni::jni_true; + } + + return jni::jni_false; +} + +void NativeMapView::addImage(JNIEnv& env, const jni::String& name, const jni::Object& bitmap, jni::jfloat scale, jni::jboolean sdf) { + jni::NullCheck(env, &bitmap); + mbgl::PremultipliedImage premultipliedImage = Bitmap::GetImage(env, bitmap); + + map->getStyle().addImage(std::make_unique( + jni::Make(env, name), + std::move(premultipliedImage), + float(scale), + sdf) + ); +} + +void NativeMapView::addImages(JNIEnv& env, const jni::Array>& jimages) { + jni::NullCheck(env, &jimages); + std::size_t len = jimages.Length(env); + + for (std::size_t i = 0; i < len; i++) { + auto image = mbgl::android::Image::getImage(env, jimages.Get(env, i)); + map->getStyle().addImage(std::make_unique(image)); + } +} + +void NativeMapView::removeImage(JNIEnv& env, const jni::String& name) { + map->getStyle().removeImage(jni::Make(env, name)); +} + +jni::Local> NativeMapView::getImage(JNIEnv& env, const jni::String& name) { + const mbgl::style::Image *image = map->getStyle().getImage(jni::Make(env, name)); + if (image) { + return Bitmap::CreateBitmap(env, image->getImage()); + } else { + return jni::Local>(); + } +} + +void NativeMapView::setPrefetchTiles(JNIEnv&, jni::jboolean enable) { + map->setPrefetchZoomDelta(enable ? util::DEFAULT_PREFETCH_ZOOM_DELTA : uint8_t(0)); +} + +jni::jboolean NativeMapView::getPrefetchTiles(JNIEnv&) { + return jni::jboolean(map->getPrefetchZoomDelta() > 0); +} + +mbgl::Map& NativeMapView::getMap() { + return *map; +} + +// Static methods // + +void NativeMapView::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer(env, javaClass, "nativePtr", + jni::MakePeer&, const jni::Object&, const jni::Object&, jni::jfloat, jni::jboolean>, + "nativeInitialize", + "nativeDestroy", + METHOD(&NativeMapView::resizeView, "nativeResizeView"), + METHOD(&NativeMapView::getStyleUrl, "nativeGetStyleUrl"), + METHOD(&NativeMapView::setStyleUrl, "nativeSetStyleUrl"), + METHOD(&NativeMapView::getStyleJson, "nativeGetStyleJson"), + METHOD(&NativeMapView::setStyleJson, "nativeSetStyleJson"), + METHOD(&NativeMapView::cancelTransitions, "nativeCancelTransitions"), + METHOD(&NativeMapView::setGestureInProgress, "nativeSetGestureInProgress"), + METHOD(&NativeMapView::moveBy, "nativeMoveBy"), + METHOD(&NativeMapView::jumpTo, "nativeJumpTo"), + METHOD(&NativeMapView::easeTo, "nativeEaseTo"), + METHOD(&NativeMapView::flyTo, "nativeFlyTo"), + METHOD(&NativeMapView::getLatLng, "nativeGetLatLng"), + METHOD(&NativeMapView::setLatLng, "nativeSetLatLng"), + METHOD(&NativeMapView::getCameraForLatLngBounds, "nativeGetCameraForLatLngBounds"), + METHOD(&NativeMapView::getCameraForGeometry, "nativeGetCameraForGeometry"), + METHOD(&NativeMapView::setReachability, "nativeSetReachability"), + METHOD(&NativeMapView::resetPosition, "nativeResetPosition"), + METHOD(&NativeMapView::getPitch, "nativeGetPitch"), + METHOD(&NativeMapView::setPitch, "nativeSetPitch"), + METHOD(&NativeMapView::getZoom, "nativeGetZoom"), + METHOD(&NativeMapView::setZoom, "nativeSetZoom"), + METHOD(&NativeMapView::resetZoom, "nativeResetZoom"), + METHOD(&NativeMapView::setMinZoom, "nativeSetMinZoom"), + METHOD(&NativeMapView::getMinZoom, "nativeGetMinZoom"), + METHOD(&NativeMapView::setMaxZoom, "nativeSetMaxZoom"), + METHOD(&NativeMapView::getMaxZoom, "nativeGetMaxZoom"), + METHOD(&NativeMapView::rotateBy, "nativeRotateBy"), + METHOD(&NativeMapView::setBearing, "nativeSetBearing"), + METHOD(&NativeMapView::setBearingXY, "nativeSetBearingXY"), + METHOD(&NativeMapView::getBearing, "nativeGetBearing"), + METHOD(&NativeMapView::resetNorth, "nativeResetNorth"), + METHOD(&NativeMapView::setVisibleCoordinateBounds, "nativeSetVisibleCoordinateBounds"), + METHOD(&NativeMapView::scheduleSnapshot, "nativeTakeSnapshot"), + METHOD(&NativeMapView::getCameraPosition, "nativeGetCameraPosition"), + METHOD(&NativeMapView::updateMarker, "nativeUpdateMarker"), + METHOD(&NativeMapView::addMarkers, "nativeAddMarkers"), + METHOD(&NativeMapView::setDebug, "nativeSetDebug"), + METHOD(&NativeMapView::cycleDebugOptions, "nativeCycleDebugOptions"), + METHOD(&NativeMapView::getDebug, "nativeGetDebug"), + METHOD(&NativeMapView::isFullyLoaded, "nativeIsFullyLoaded"), + METHOD(&NativeMapView::onLowMemory, "nativeOnLowMemory"), + METHOD(&NativeMapView::getMetersPerPixelAtLatitude, "nativeGetMetersPerPixelAtLatitude"), + METHOD(&NativeMapView::projectedMetersForLatLng, "nativeProjectedMetersForLatLng"), + METHOD(&NativeMapView::pixelForLatLng, "nativePixelForLatLng"), + METHOD(&NativeMapView::latLngForProjectedMeters, "nativeLatLngForProjectedMeters"), + METHOD(&NativeMapView::latLngForPixel, "nativeLatLngForPixel"), + METHOD(&NativeMapView::addPolylines, "nativeAddPolylines"), + METHOD(&NativeMapView::addPolygons, "nativeAddPolygons"), + METHOD(&NativeMapView::updatePolyline, "nativeUpdatePolyline"), + METHOD(&NativeMapView::updatePolygon, "nativeUpdatePolygon"), + METHOD(&NativeMapView::removeAnnotations, "nativeRemoveAnnotations"), + METHOD(&NativeMapView::addAnnotationIcon, "nativeAddAnnotationIcon"), + METHOD(&NativeMapView::removeAnnotationIcon, "nativeRemoveAnnotationIcon"), + METHOD(&NativeMapView::getTopOffsetPixelsForAnnotationSymbol, "nativeGetTopOffsetPixelsForAnnotationSymbol"), + METHOD(&NativeMapView::getTransitionOptions, "nativeGetTransitionOptions"), + METHOD(&NativeMapView::setTransitionOptions, "nativeSetTransitionOptions"), + METHOD(&NativeMapView::queryPointAnnotations, "nativeQueryPointAnnotations"), + METHOD(&NativeMapView::queryShapeAnnotations, "nativeQueryShapeAnnotations"), + METHOD(&NativeMapView::queryRenderedFeaturesForPoint, "nativeQueryRenderedFeaturesForPoint"), + METHOD(&NativeMapView::queryRenderedFeaturesForBox, "nativeQueryRenderedFeaturesForBox"), + METHOD(&NativeMapView::getLight, "nativeGetLight"), + METHOD(&NativeMapView::getLayers, "nativeGetLayers"), + METHOD(&NativeMapView::getLayer, "nativeGetLayer"), + METHOD(&NativeMapView::addLayer, "nativeAddLayer"), + METHOD(&NativeMapView::addLayerAbove, "nativeAddLayerAbove"), + METHOD(&NativeMapView::addLayerAt, "nativeAddLayerAt"), + METHOD(&NativeMapView::removeLayerAt, "nativeRemoveLayerAt"), + METHOD(&NativeMapView::removeLayer, "nativeRemoveLayer"), + METHOD(&NativeMapView::getSources, "nativeGetSources"), + METHOD(&NativeMapView::getSource, "nativeGetSource"), + METHOD(&NativeMapView::addSource, "nativeAddSource"), + METHOD(&NativeMapView::removeSource, "nativeRemoveSource"), + METHOD(&NativeMapView::addImage, "nativeAddImage"), + METHOD(&NativeMapView::addImages, "nativeAddImages"), + METHOD(&NativeMapView::removeImage, "nativeRemoveImage"), + METHOD(&NativeMapView::getImage, "nativeGetImage"), + METHOD(&NativeMapView::setLatLngBounds, "nativeSetLatLngBounds"), + METHOD(&NativeMapView::setPrefetchTiles, "nativeSetPrefetchTiles"), + METHOD(&NativeMapView::getPrefetchTiles, "nativeGetPrefetchTiles") + ); +} + +} // namespace android +} // namespace mbgl diff --git a/src/native_map_view.hpp b/src/native_map_view.hpp new file mode 100755 index 000000000..26567a003 --- /dev/null +++ b/src/native_map_view.hpp @@ -0,0 +1,261 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "annotation/marker.hpp" +#include "annotation/polygon.hpp" +#include "annotation/polyline.hpp" +#include "graphics/pointf.hpp" +#include "graphics/rectf.hpp" +#include "geojson/feature.hpp" +#include "geojson/geometry.hpp" +#include "geometry/lat_lng.hpp" +#include "geometry/projected_meters.hpp" +#include "style/layers/layer_manager.hpp" +#include "style/sources/source.hpp" +#include "geometry/lat_lng_bounds.hpp" +#include "map/camera_position.hpp" +#include "map/image.hpp" +#include "style/light.hpp" +#include "bitmap.hpp" + +#include +#include +#include +#include +#include +#include + +namespace mbgl { +namespace android { + +class AndroidRendererFrontend; +class FileSource; +class MapRenderer; + +class NativeMapView : public MapObserver { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/maps/NativeMapView"; }; + + static void registerNative(jni::JNIEnv&); + + NativeMapView(jni::JNIEnv&, + const jni::Object&, + const jni::Object&, + const jni::Object&, + jni::jfloat, + jni::jboolean); + + virtual ~NativeMapView(); + + // mbgl::RendererBackend (mbgl::MapObserver) // + void onCameraWillChange(MapObserver::CameraChangeMode) override; + void onCameraIsChanging() override; + void onCameraDidChange(MapObserver::CameraChangeMode) override; + void onWillStartLoadingMap() override; + void onDidFinishLoadingMap() override; + void onDidFailLoadingMap(MapLoadError, const std::string&) override; + void onWillStartRenderingFrame() override; + void onDidFinishRenderingFrame(MapObserver::RenderMode, bool) override; + void onWillStartRenderingMap() override; + void onDidFinishRenderingMap(MapObserver::RenderMode) override; + void onDidBecomeIdle() override; + void onDidFinishLoadingStyle() override; + void onSourceChanged(mbgl::style::Source&) override; + void onStyleImageMissing(const std::string&) override; + bool onCanRemoveUnusedStyleImage(const std::string&) override; + + // JNI // + + void resizeView(jni::JNIEnv&, int, int); + + jni::Local getStyleUrl(jni::JNIEnv&); + + void setStyleUrl(jni::JNIEnv&, const jni::String&); + + jni::Local getStyleJson(jni::JNIEnv&); + + void setStyleJson(jni::JNIEnv&, const jni::String&); + + void setLatLngBounds(jni::JNIEnv&, const jni::Object&); + + void cancelTransitions(jni::JNIEnv&); + + void setGestureInProgress(jni::JNIEnv&, jni::jboolean); + + void moveBy(jni::JNIEnv&, jni::jdouble, jni::jdouble, jni::jlong); + + void jumpTo(jni::JNIEnv&, jni::jdouble, jni::jdouble, jni::jdouble, jni::jdouble, jni::jdouble, const jni::Array&); + + void easeTo(jni::JNIEnv&, jni::jdouble, jni::jdouble, jni::jdouble, jni::jlong, jni::jdouble, jni::jdouble, const jni::Array&, jni::jboolean); + + void flyTo(jni::JNIEnv&, jni::jdouble, jni::jdouble, jni::jdouble, jni::jlong, jni::jdouble, jni::jdouble, const jni::Array&); + + jni::Local> getLatLng(JNIEnv&); + + void setLatLng(jni::JNIEnv&, jni::jdouble, jni::jdouble, const jni::Array&, jni::jlong); + + jni::Local> getCameraForLatLngBounds(jni::JNIEnv&, const jni::Object&, double top, double left, double bottom, double right, double bearing, double tilt); + + jni::Local> getCameraForGeometry(jni::JNIEnv&, const jni::Object&, double top, double left, double bottom, double right, double bearing, double tilt); + + void setReachability(jni::JNIEnv&, jni::jboolean); + + void resetPosition(jni::JNIEnv&); + + jni::jdouble getPitch(jni::JNIEnv&); + + void setPitch(jni::JNIEnv&, jni::jdouble, jni::jlong); + + void setZoom(jni::JNIEnv&, jni::jdouble, jni::jdouble, jni::jdouble, jni::jlong); + + jni::jdouble getZoom(jni::JNIEnv&); + + void resetZoom(jni::JNIEnv&); + + void setMinZoom(jni::JNIEnv&, jni::jdouble); + + jni::jdouble getMinZoom(jni::JNIEnv&); + + void setMaxZoom(jni::JNIEnv&, jni::jdouble); + + jni::jdouble getMaxZoom(jni::JNIEnv&); + + void rotateBy(jni::JNIEnv&, jni::jdouble, jni::jdouble, jni::jdouble, jni::jdouble, jni::jlong); + + void setBearing(jni::JNIEnv&, jni::jdouble, jni::jlong); + + void setBearingXY(jni::JNIEnv&, jni::jdouble, jni::jdouble, jni::jdouble, jni::jlong); + + jni::jdouble getBearing(jni::JNIEnv&); + + void resetNorth(jni::JNIEnv&); + + void setVisibleCoordinateBounds(JNIEnv&, const jni::Array>&, const jni::Object&, jni::jdouble, jni::jlong); + + void scheduleSnapshot(jni::JNIEnv&); + + jni::Local> getCameraPosition(jni::JNIEnv&); + + void updateMarker(jni::JNIEnv&, jni::jlong, jni::jdouble, jni::jdouble, const jni::String&); + + jni::Local> addMarkers(jni::JNIEnv&, const jni::Array>&); + + void onLowMemory(JNIEnv& env); + + void setDebug(JNIEnv&, jni::jboolean); + + void cycleDebugOptions(JNIEnv&); + + jni::jboolean getDebug(JNIEnv&); + + jni::jboolean isFullyLoaded(JNIEnv&); + + jni::jdouble getMetersPerPixelAtLatitude(JNIEnv&, jni::jdouble, jni::jdouble); + + jni::Local> projectedMetersForLatLng(JNIEnv&, jni::jdouble, jni::jdouble); + + jni::Local> pixelForLatLng(JNIEnv&, jdouble, jdouble); + + jni::Local> latLngForProjectedMeters(JNIEnv&, jdouble, jdouble); + + jni::Local> latLngForPixel(JNIEnv&, jfloat, jfloat); + + jni::Local> addPolylines(JNIEnv&, const jni::Array>&); + + jni::Local> addPolygons(JNIEnv&, const jni::Array>&); + + void updatePolyline(JNIEnv&, jlong, const jni::Object&); + + void updatePolygon(JNIEnv&, jlong, const jni::Object&); + + void removeAnnotations(JNIEnv&, const jni::Array&); + + void addAnnotationIcon(JNIEnv&, const jni::String&, jint, jint, jfloat, const jni::Array&); + + void removeAnnotationIcon(JNIEnv&, const jni::String&); + + jni::jdouble getTopOffsetPixelsForAnnotationSymbol(JNIEnv&, const jni::String&); + + jni::Local> getTransitionOptions(JNIEnv&); + + void setTransitionOptions(JNIEnv&, const jni::Object&); + + jni::Local> queryPointAnnotations(JNIEnv&, const jni::Object&); + + jni::Local> queryShapeAnnotations(JNIEnv&, const jni::Object&); + + jni::Local>> queryRenderedFeaturesForPoint(JNIEnv&, jni::jfloat, jni::jfloat, + const jni::Array&, + const jni::Array>& jfilter); + + jni::Local>> queryRenderedFeaturesForBox(JNIEnv&, jni::jfloat, jni::jfloat, jni::jfloat, + jni::jfloat, const jni::Array&, + const jni::Array>& jfilter); + + jni::Local> getLight(JNIEnv&); + + jni::Local>> getLayers(JNIEnv&); + + jni::Local> getLayer(JNIEnv&, const jni::String&); + + void addLayer(JNIEnv&, jlong, const jni::String&); + + void addLayerAbove(JNIEnv&, jlong, const jni::String&); + + void addLayerAt(JNIEnv&, jni::jlong, jni::jint); + + jni::jboolean removeLayerAt(JNIEnv&, jni::jint); + + jni::jboolean removeLayer(JNIEnv&, jlong); + + jni::Local>> getSources(JNIEnv&); + + jni::Local> getSource(JNIEnv&, const jni::String&); + + void addSource(JNIEnv&, const jni::Object&, jlong nativePtr); + + jni::jboolean removeSource(JNIEnv&, const jni::Object&, jlong nativePtr); + + void addImage(JNIEnv&, const jni::String&, const jni::Object& bitmap, jni::jfloat, jni::jboolean); + + void addImages(JNIEnv&, const jni::Array>&); + + void removeImage(JNIEnv&, const jni::String&); + + jni::Local> getImage(JNIEnv&, const jni::String&); + + void setPrefetchTiles(JNIEnv&, jni::jboolean); + + jni::jboolean getPrefetchTiles(JNIEnv&); + + mbgl::Map& getMap(); + +private: + std::unique_ptr rendererFrontend; + + JavaVM *vm = nullptr; + jni::WeakReference> javaPeer; + + MapRenderer& mapRenderer; + + std::string styleUrl; + + float pixelRatio; + + // Minimum texture size according to OpenGL ES 2.0 specification. + int width = 64; + int height = 64; + + // Ensure these are initialised last + std::unique_ptr map; +}; + +} // namespace android +} // namespace mbgl diff --git a/src/offline/offline_manager.cpp b/src/offline/offline_manager.cpp new file mode 100644 index 000000000..029252f78 --- /dev/null +++ b/src/offline/offline_manager.cpp @@ -0,0 +1,330 @@ +#include "offline_manager.hpp" + +#include + +#include "../attach_env.hpp" + +namespace mbgl { +namespace android { + +// OfflineManager // + +OfflineManager::OfflineManager(jni::JNIEnv& env, const jni::Object& jFileSource) + : fileSource(std::static_pointer_cast(mbgl::FileSource::getSharedFileSource(FileSource::getSharedResourceOptions(env, jFileSource)))) {} + +OfflineManager::~OfflineManager() {} + +void OfflineManager::setOfflineMapboxTileCountLimit(jni::JNIEnv&, jni::jlong limit) { + fileSource->setOfflineMapboxTileCountLimit(limit); +} + +void OfflineManager::listOfflineRegions(jni::JNIEnv& env_, const jni::Object& jFileSource_, const jni::Object& callback_) { + auto globalCallback = jni::NewGlobal(env_, callback_); + auto globalFilesource = jni::NewGlobal(env_, jFileSource_); + + fileSource->listOfflineRegions([ + //Keep a shared ptr to a global reference of the callback and file source so they are not GC'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)), + jFileSource = std::make_shared(std::move(globalFilesource)) + ](mbgl::expected regions) mutable { + + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (regions) { + OfflineManager::ListOfflineRegionsCallback::onList( + *env, *jFileSource, *callback, *regions); + } else { + OfflineManager::ListOfflineRegionsCallback::onError( + *env, *callback, regions.error()); + } + }); +} + +void OfflineManager::createOfflineRegion(jni::JNIEnv& env_, + const jni::Object& jFileSource_, + const jni::Object& definition_, + const jni::Array& metadata_, + const jni::Object& callback_) { + // Convert + auto definition = OfflineRegionDefinition::getDefinition(env_, definition_); + + mbgl::OfflineRegionMetadata metadata; + if (metadata_) { + metadata = OfflineRegion::metadata(env_, metadata_); + } + + auto globalCallback = jni::NewGlobal(env_, callback_); + auto globalFilesource = jni::NewGlobal(env_, jFileSource_); + + // Create region + fileSource->createOfflineRegion(definition, metadata, [ + //Keep a shared ptr to a global reference of the callback and file source so they are not GC'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)), + jFileSource = std::make_shared(std::move(globalFilesource)) + ](mbgl::expected region) mutable { + + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (region) { + OfflineManager::CreateOfflineRegionCallback::onCreate( + *env, *jFileSource, *callback, *region + ); + } else { + OfflineManager::CreateOfflineRegionCallback::onError( + *env, *callback, region.error()); + } + }); +} + +void OfflineManager::mergeOfflineRegions(jni::JNIEnv& env_, const jni::Object& jFileSource_, + const jni::String& jString_, + const jni::Object& callback_) { + auto globalCallback = jni::NewGlobal(env_, callback_); + auto globalFilesource = jni::NewGlobal(env_, jFileSource_); + + auto path = jni::Make(env_, jString_); + fileSource->mergeOfflineRegions(path, [ + //Keep a shared ptr to a global reference of the callback and file source so they are not GC'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)), + jFileSource = std::make_shared(std::move(globalFilesource)) + ](mbgl::expected regions) mutable { + + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (regions) { + OfflineManager::MergeOfflineRegionsCallback::onMerge( + *env, *jFileSource, *callback, *regions); + } else { + OfflineManager::MergeOfflineRegionsCallback::onError( + *env, *callback, regions.error()); + } + }); +} + +void OfflineManager::resetDatabase(jni::JNIEnv& env_, const jni::Object& callback_) { + auto globalCallback = jni::NewGlobal(env_, callback_); + + fileSource->resetDatabase([ + //Keep a shared ptr to a global reference of the callback so they are not GC'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)) + ](std::exception_ptr exception) mutable { + + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (exception) { + OfflineManager::FileSourceCallback::onError(*env, *callback, jni::Make(*env, mbgl::util::toString(exception))); + } else { + OfflineManager::FileSourceCallback::onSuccess(*env, *callback); + } + }); +} + +void OfflineManager::invalidateAmbientCache(jni::JNIEnv& env_, const jni::Object& callback_) { + auto globalCallback = jni::NewGlobal(env_, callback_); + + fileSource->invalidateAmbientCache([ + //Keep a shared ptr to a global reference of the callback so they are not GC'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)) + ](std::exception_ptr exception) mutable { + + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (exception) { + OfflineManager::FileSourceCallback::onError(*env, *callback, jni::Make(*env, mbgl::util::toString(exception))); + } else { + OfflineManager::FileSourceCallback::onSuccess(*env, *callback); + } + }); +} + +void OfflineManager::clearAmbientCache(jni::JNIEnv& env_, const jni::Object& callback_) { + auto globalCallback = jni::NewGlobal(env_, callback_); + + fileSource->clearAmbientCache([ + //Keep a shared ptr to a global reference of the callback so they are not GC'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)) + ](std::exception_ptr exception) mutable { + + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (exception) { + OfflineManager::FileSourceCallback::onError(*env, *callback, jni::Make(*env, mbgl::util::toString(exception))); + } else { + OfflineManager::FileSourceCallback::onSuccess(*env, *callback); + } + }); +} + +void OfflineManager::setMaximumAmbientCacheSize(jni::JNIEnv& env_, const jni::jlong size_, const jni::Object& callback_) { + auto globalCallback = jni::NewGlobal(env_, callback_); + + fileSource->setMaximumAmbientCacheSize(size_, [ + //Keep a shared ptr to a global reference of the callback so they are not GC'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)) + ](std::exception_ptr exception) mutable { + + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (exception) { + OfflineManager::FileSourceCallback::onError(*env, *callback, jni::Make(*env, mbgl::util::toString(exception))); + } else { + OfflineManager::FileSourceCallback::onSuccess(*env, *callback); + } + }); +} + +// FileSource::FileSourceCallback // + +void OfflineManager::FileSourceCallback::onSuccess(jni::JNIEnv& env, + const jni::Object& callback) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onSuccess"); + callback.Call(env, method); +} + +void OfflineManager::FileSourceCallback::onError(jni::JNIEnv& env, + const jni::Object& callback, + const jni::String& message) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onError"); + callback.Call(env, method, message); +} + +void OfflineManager::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); + jni::Class::Singleton(env); + jni::Class::Singleton(env); + jni::Class::Singleton(env); + + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + jni::RegisterNativePeer( env, javaClass, "nativePtr", + jni::MakePeer&>, + "initialize", + "finalize", + METHOD(&OfflineManager::setOfflineMapboxTileCountLimit, "setOfflineMapboxTileCountLimit"), + METHOD(&OfflineManager::listOfflineRegions, "listOfflineRegions"), + METHOD(&OfflineManager::createOfflineRegion, "createOfflineRegion"), + METHOD(&OfflineManager::mergeOfflineRegions, "mergeOfflineRegions"), + METHOD(&OfflineManager::resetDatabase, "nativeResetDatabase"), + METHOD(&OfflineManager::invalidateAmbientCache, "nativeInvalidateAmbientCache"), + METHOD(&OfflineManager::clearAmbientCache, "nativeClearAmbientCache"), + METHOD(&OfflineManager::setMaximumAmbientCacheSize, "nativeSetMaximumAmbientCacheSize"), + METHOD(&OfflineManager::putResourceWithUrl, "putResourceWithUrl")); +} + +// OfflineManager::ListOfflineRegionsCallback // + +void OfflineManager::ListOfflineRegionsCallback::onError(jni::JNIEnv& env, + const jni::Object& callback, + std::exception_ptr error) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onError"); + + callback.Call(env, method, jni::Make(env, mbgl::util::toString(error))); +} + +void OfflineManager::ListOfflineRegionsCallback::onList(jni::JNIEnv& env, + const jni::Object& jFileSource, + const jni::Object& callback, + mbgl::OfflineRegions& regions) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod>)>(env, "onList"); + + std::size_t index = 0; + auto jregions = jni::Array>::New(env, regions.size()); + for (auto& region : regions) { + jregions.Set(env, index, OfflineRegion::New(env, jFileSource, std::move(region))); + index++; + } + + callback.Call(env, method, jregions); +} + +// OfflineManager::CreateOfflineRegionCallback // + +void OfflineManager::CreateOfflineRegionCallback::onError(jni::JNIEnv& env, + const jni::Object& callback, + std::exception_ptr error) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onError"); + + callback.Call(env, method, jni::Make(env, mbgl::util::toString(error))); +} + +void OfflineManager::CreateOfflineRegionCallback::onCreate(jni::JNIEnv& env, + const jni::Object& jFileSource, + const jni::Object& callback, + mbgl::OfflineRegion& region) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod)>(env, "onCreate"); + + callback.Call(env, method, OfflineRegion::New(env, jFileSource, std::move(region))); +} + +// OfflineManager::MergeOfflineRegionsCallback // + +void OfflineManager::MergeOfflineRegionsCallback::onError(jni::JNIEnv& env, + const jni::Object& callback, + std::exception_ptr error) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onError"); + + callback.Call(env, method, jni::Make(env, mbgl::util::toString(error))); +} + +void OfflineManager::MergeOfflineRegionsCallback::onMerge(jni::JNIEnv& env, + const jni::Object& jFileSource, + const jni::Object& callback, + mbgl::OfflineRegions& regions) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod>)>(env, "onMerge"); + + std::size_t index = 0; + auto jregions = jni::Array>::New(env, regions.size()); + for (auto& region : regions) { + jregions.Set(env, index, OfflineRegion::New(env, jFileSource, std::move(region))); + index++; + } + + callback.Call(env, method, jregions); +} + +void OfflineManager::putResourceWithUrl(jni::JNIEnv& env, + const jni::String& url_, + const jni::Array& arr, + jlong modified, + jlong expires, + const jni::String& eTag_, + jboolean mustRevalidate) { + auto url = jni::Make(env, url_); + auto data = std::make_shared(arr.Length(env), char()); + jni::GetArrayRegion(env, *arr, 0, data->size(), reinterpret_cast(&(*data)[0])); + mbgl::Resource resource(mbgl::Resource::Kind::Unknown, url); + mbgl::Response response; + response.data = data; + response.mustRevalidate = mustRevalidate; + if (eTag_) { + response.etag = jni::Make(env, eTag_); + } + if (modified > 0) { + response.modified = Timestamp(mbgl::Seconds(modified)); + } + if (expires > 0) { + response.expires = Timestamp(mbgl::Seconds(expires)); + } + + fileSource->put(resource, response); +} + +} // namespace android +} // namespace mbgl diff --git a/src/offline/offline_manager.hpp b/src/offline/offline_manager.hpp new file mode 100644 index 000000000..058cfb5b4 --- /dev/null +++ b/src/offline/offline_manager.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include +#include + +#include "../file_source.hpp" +#include "offline_region.hpp" +#include "offline_region_definition.hpp" +#include "../java_types.hpp" + +#include + +namespace mbgl { + +class DefaultFileSource; + +namespace android { + +class OfflineManager { +public: + + class ListOfflineRegionsCallback { + public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineManager$ListOfflineRegionsCallback";} + + static void onError(jni::JNIEnv&, const jni::Object&, std::exception_ptr); + + static void onList(jni::JNIEnv&, + const jni::Object&, + const jni::Object&, + mbgl::OfflineRegions&); + }; + + class CreateOfflineRegionCallback { + public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineManager$CreateOfflineRegionCallback"; } + + static void onError(jni::JNIEnv&, const jni::Object&, std::exception_ptr); + + static void onCreate(jni::JNIEnv&, + const jni::Object&, + const jni::Object&, + mbgl::OfflineRegion&); + }; + + class MergeOfflineRegionsCallback { + public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineManager$MergeOfflineRegionsCallback";} + + static void onError(jni::JNIEnv&, const jni::Object&, std::exception_ptr); + + static void onMerge(jni::JNIEnv&, + const jni::Object&, + const jni::Object&, + mbgl::OfflineRegions&); + }; + + struct FileSourceCallback { + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineManager$FileSourceCallback";} + + static void onSuccess(jni::JNIEnv&, const jni::Object&); + + static void onError(jni::JNIEnv&, const jni::Object&, const jni::String&); + }; + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineManager"; }; + + static void registerNative(jni::JNIEnv&); + + OfflineManager(jni::JNIEnv&, const jni::Object&); + ~OfflineManager(); + + void setOfflineMapboxTileCountLimit(jni::JNIEnv&, jni::jlong limit); + + void listOfflineRegions(jni::JNIEnv&, const jni::Object&, const jni::Object& callback); + + void createOfflineRegion(jni::JNIEnv&, + const jni::Object& jFileSource_, + const jni::Object& definition, + const jni::Array& metadata, + const jni::Object& callback); + + void mergeOfflineRegions(jni::JNIEnv&, + const jni::Object&, + const jni::String&, + const jni::Object&); + + void putResourceWithUrl(jni::JNIEnv&, + const jni::String& url, + const jni::Array& data, + jlong modified, + jlong expires, + const jni::String& eTag, + jboolean mustRevalidate); + + void resetDatabase(jni::JNIEnv&, const jni::Object& callback_); + + void invalidateAmbientCache(jni::JNIEnv&, const jni::Object& callback_); + + void clearAmbientCache(jni::JNIEnv&, const jni::Object& callback_); + + void setMaximumAmbientCacheSize(jni::JNIEnv&, const jni::jlong size, const jni::Object& callback_); + +private: + std::shared_ptr fileSource; +}; + +} // namespace android +} // namespace mbgl diff --git a/src/offline/offline_region.cpp b/src/offline/offline_region.cpp new file mode 100644 index 000000000..ac9f491ab --- /dev/null +++ b/src/offline/offline_region.cpp @@ -0,0 +1,300 @@ +#include "offline_region.hpp" + +#include +#include + +#include "offline_region_definition.hpp" +#include "offline_region_error.hpp" +#include "offline_region_status.hpp" +#include "../attach_env.hpp" + +namespace mbgl { +namespace android { + +// OfflineRegion // + +OfflineRegion::OfflineRegion(jni::JNIEnv& env, jni::jlong offlineRegionPtr, const jni::Object& jFileSource) + : region(reinterpret_cast(offlineRegionPtr)) + , fileSource(std::static_pointer_cast(mbgl::FileSource::getSharedFileSource(FileSource::getSharedResourceOptions(env, jFileSource)))) {} + +OfflineRegion::~OfflineRegion() {} + +void OfflineRegion::setOfflineRegionObserver(jni::JNIEnv& env_, const jni::Object& callback) { + + // Define the observer + class Observer : public mbgl::OfflineRegionObserver { + public: + Observer(jni::Global, jni::EnvAttachingDeleter> callback_) + : callback(std::move(callback_)) { + } + + void statusChanged(mbgl::OfflineRegionStatus status) override { + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + static auto& javaClass = jni::Class::Singleton(*env); + static auto method = javaClass.GetMethod)>(*env, "onStatusChanged"); + + callback.Call(*env, method, OfflineRegionStatus::New(*env, status)); + } + + void responseError(mbgl::Response::Error error) override { + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + static auto& javaClass = jni::Class::Singleton(*env); + static auto method = javaClass.GetMethod)>(*env, "onError"); + + callback.Call(*env, method, OfflineRegionError::New(*env, error)); + } + + void mapboxTileCountLimitExceeded(uint64_t limit) override { + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + static auto& javaClass = jni::Class::Singleton(*env); + static auto method = javaClass.GetMethod(*env, "mapboxTileCountLimitExceeded"); + + callback.Call(*env, method, jlong(limit)); + } + + jni::Global, jni::EnvAttachingDeleter> callback; + }; + + // Set the observer + fileSource->setOfflineRegionObserver(*region, std::make_unique(jni::NewGlobal(env_, callback))); +} + +void OfflineRegion::setOfflineRegionDownloadState(jni::JNIEnv&, jni::jint jState) { + // State + mbgl::OfflineRegionDownloadState state; + switch (jState) { + case 0: + state = mbgl::OfflineRegionDownloadState::Inactive; + break; + case 1: + state = mbgl::OfflineRegionDownloadState::Active; + break; + default: + mbgl::Log::Error(mbgl::Event::JNI, "State can only be 0 (inactive) or 1 (active)."); + return; + } + + fileSource->setOfflineRegionDownloadState(*region, state); +} + +void OfflineRegion::getOfflineRegionStatus(jni::JNIEnv& env_, const jni::Object& callback_) { + auto globalCallback = jni::NewGlobal(env_, callback_); + + fileSource->getOfflineRegionStatus(*region, [ + //Ensure the object is not gc'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)) + ](mbgl::expected status) mutable { + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (status) { + OfflineRegionStatusCallback::onStatus(*env, *callback, std::move(*status)); + } else { + OfflineRegionStatusCallback::onError(*env, *callback, status.error()); + } + }); +} + +void OfflineRegion::deleteOfflineRegion(jni::JNIEnv& env_, const jni::Object& callback_) { + auto globalCallback = jni::NewGlobal(env_, callback_); + + fileSource->deleteOfflineRegion(std::move(*region), [ + //Ensure the object is not gc'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)) + ](std::exception_ptr error) mutable { + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (error) { + OfflineRegionDeleteCallback::onError(*env, *callback, error); + } else { + OfflineRegionDeleteCallback::onDelete(*env, *callback); + } + }); +} + +void OfflineRegion::invalidateOfflineRegion(jni::JNIEnv& env_, const jni::Object& callback_) { + auto globalCallback = jni::NewGlobal(env_, callback_); + + fileSource->invalidateOfflineRegion(*region, [ + //Ensure the object is not gc'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)) + ](std::exception_ptr error) mutable { + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (error) { + OfflineRegionInvalidateCallback::onError(*env, *callback, error); + } else { + OfflineRegionInvalidateCallback::onInvalidate(*env, *callback); + } + }); +} + +void OfflineRegion::updateOfflineRegionMetadata(jni::JNIEnv& env_, const jni::Array& jMetadata, const jni::Object& callback_) { + auto metadata = OfflineRegion::metadata(env_, jMetadata); + auto globalCallback = jni::NewGlobal(env_, callback_); + + fileSource->updateOfflineMetadata(region->getID(), metadata, [ + //Ensure the object is not gc'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)) + ](mbgl::expected data) mutable { + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (data) { + OfflineRegionUpdateMetadataCallback::onUpdate(*env, *callback, std::move(*data)); + } else { + OfflineRegionUpdateMetadataCallback::onError(*env, *callback, data.error()); + } + }); +} + +jni::Local> OfflineRegion::New(jni::JNIEnv& env, const jni::Object& jFileSource, mbgl::OfflineRegion region) { + + // Definition + auto definition = region.getDefinition().match( + [&](const mbgl::OfflineTilePyramidRegionDefinition def) { + return OfflineTilePyramidRegionDefinition::New(env, def); + }, [&](const mbgl::OfflineGeometryRegionDefinition def) { + return OfflineGeometryRegionDefinition::New(env, def); + }); + + // Create region java object + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor, jni::jlong, jni::Object, jni::Array>(env); + + return javaClass.New(env, constructor, + reinterpret_cast(new mbgl::OfflineRegion(std::move(region))), //Copy a region to the heap + jFileSource, + jni::jlong(region.getID()), + definition, + OfflineRegion::metadata(env, region.getMetadata())); +} + +jni::Local> OfflineRegion::metadata(jni::JNIEnv& env, mbgl::OfflineRegionMetadata metadata_) { + std::vector convertedMetadata(metadata_.begin(), metadata_.end()); + std::size_t length = static_cast(convertedMetadata.size()); + auto metadata = jni::Array::New(env, length); + metadata.SetRegion>(env, 0, convertedMetadata); + return metadata; +} + +mbgl::OfflineRegionMetadata OfflineRegion::metadata(jni::JNIEnv& env, const jni::Array& metadata_) { + std::size_t length = metadata_.Length(env); + auto metadata_tmp = std::vector(); + metadata_tmp.resize(length); + metadata_.GetRegion>(env, 0, metadata_tmp); + auto metadata = std::vector(metadata_tmp.begin(), metadata_tmp.end()); + return metadata; +} + +void OfflineRegion::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); + jni::Class::Singleton(env); + jni::Class::Singleton(env); + jni::Class::Singleton(env); + jni::Class::Singleton(env); + + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + jni::RegisterNativePeer( env, javaClass, "nativePtr", + jni::MakePeer&>, + "initialize", + "finalize", + METHOD(&OfflineRegion::setOfflineRegionObserver, "setOfflineRegionObserver"), + METHOD(&OfflineRegion::setOfflineRegionDownloadState, "setOfflineRegionDownloadState"), + METHOD(&OfflineRegion::getOfflineRegionStatus, "getOfflineRegionStatus"), + METHOD(&OfflineRegion::deleteOfflineRegion, "deleteOfflineRegion"), + METHOD(&OfflineRegion::invalidateOfflineRegion, "invalidateOfflineRegion"), + METHOD(&OfflineRegion::updateOfflineRegionMetadata, "updateOfflineRegionMetadata") + ); +} + +// OfflineRegionObserver // + +// OfflineRegionStatusCallback // + +void OfflineRegion::OfflineRegionStatusCallback::onError(jni::JNIEnv& env, + const jni::Object& callback, + std::exception_ptr error) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onError"); + + callback.Call(env, method, jni::Make(env, mbgl::util::toString(error))); +} + +void OfflineRegion::OfflineRegionStatusCallback::onStatus(jni::JNIEnv& env, + const jni::Object& callback, + mbgl::optional status) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod)>(env, "onStatus"); + + callback.Call(env, method, OfflineRegionStatus::New(env, std::move(*status))); +} + +// OfflineRegionDeleteCallback // + +void OfflineRegion::OfflineRegionDeleteCallback::onError(jni::JNIEnv& env, + const jni::Object& callback, + std::exception_ptr error) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onError"); + + callback.Call(env, method, jni::Make(env, mbgl::util::toString(error))); +} + +void OfflineRegion::OfflineRegionDeleteCallback::onDelete(jni::JNIEnv& env, const jni::Object& callback) { + // Trigger callback + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onDelete"); + + callback.Call(env, method); +} + +// OfflineRegionUpdateMetadataCallback // + +void OfflineRegion::OfflineRegionUpdateMetadataCallback::onError(jni::JNIEnv& env, + const jni::Object& callback, + std::exception_ptr error) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onError"); + + callback.Call(env, method, jni::Make(env, mbgl::util::toString(error))); +} + +void OfflineRegion::OfflineRegionUpdateMetadataCallback::onUpdate(jni::JNIEnv& env, + const jni::Object& callback, + mbgl::optional metadata) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod)>(env, "onUpdate"); + + callback.Call(env, method, OfflineRegion::metadata(env, std::move(*metadata))); +} + +// OfflineRegionInvalidateCallback // + +void OfflineRegion::OfflineRegionInvalidateCallback::onError(jni::JNIEnv& env, + const jni::Object& callback, + std::exception_ptr error) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onError"); + callback.Call(env, method, jni::Make(env, mbgl::util::toString(error))); +} + +void OfflineRegion::OfflineRegionInvalidateCallback::onInvalidate(jni::JNIEnv& env, const jni::Object& callback) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onInvalidate"); + callback.Call(env, method); +} + +} // namespace android +} // namespace mbgl diff --git a/src/offline/offline_region.hpp b/src/offline/offline_region.hpp new file mode 100644 index 000000000..dda253469 --- /dev/null +++ b/src/offline/offline_region.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include +#include + +#include "../file_source.hpp" + +#include + +namespace mbgl { +namespace android { + +class OfflineRegion { +public: + class OfflineRegionObserver { + public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionObserver"; }; + }; + + class OfflineRegionStatusCallback { + public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionStatusCallback"; }; + + static void onError(jni::JNIEnv&, const jni::Object&, std::exception_ptr); + + static void onStatus(jni::JNIEnv&, + const jni::Object&, + mbgl::optional); + }; + + class OfflineRegionDeleteCallback { + public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionDeleteCallback"; }; + + static void onError(jni::JNIEnv&, const jni::Object&, std::exception_ptr); + + static void onDelete(jni::JNIEnv&, const jni::Object&); + }; + + class OfflineRegionInvalidateCallback { + public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionInvalidateCallback"; }; + + static void onError(jni::JNIEnv&, const jni::Object&, std::exception_ptr); + + static void onInvalidate(jni::JNIEnv&, const jni::Object&); + }; + + class OfflineRegionUpdateMetadataCallback { + public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionUpdateMetadataCallback"; }; + + static void onError(jni::JNIEnv&, const jni::Object&, std::exception_ptr); + + static void onUpdate(jni::JNIEnv&, + const jni::Object&, + mbgl::optional); + }; + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineRegion"; }; + + OfflineRegion(jni::JNIEnv&, jni::jlong, const jni::Object&); + + ~OfflineRegion(); + + void setOfflineRegionObserver(jni::JNIEnv&, const jni::Object&); + + void setOfflineRegionDownloadState(jni::JNIEnv&, jni::jint); + + void getOfflineRegionStatus(jni::JNIEnv&, const jni::Object&); + + void deleteOfflineRegion(jni::JNIEnv&, const jni::Object&); + + void invalidateOfflineRegion(jni::JNIEnv&, const jni::Object&); + + void updateOfflineRegionMetadata(jni::JNIEnv&, const jni::Array&, const jni::Object&); + + static jni::Local> New(jni::JNIEnv&, const jni::Object&, mbgl::OfflineRegion); + + static jni::Local> metadata(jni::JNIEnv&, mbgl::OfflineRegionMetadata); + + static mbgl::OfflineRegionMetadata metadata(jni::JNIEnv&, const jni::Array&); + + static void registerNative(jni::JNIEnv&); + +private: + std::unique_ptr region; + std::shared_ptr fileSource; +}; + +} // namespace android +} // namespace mbgl diff --git a/src/offline/offline_region_definition.cpp b/src/offline/offline_region_definition.cpp new file mode 100644 index 000000000..bb9dfc8dd --- /dev/null +++ b/src/offline/offline_region_definition.cpp @@ -0,0 +1,107 @@ +#include "offline_region_definition.hpp" + +#include "../geometry/lat_lng_bounds.hpp" +#include "../geojson/geometry.hpp" + +#include + +namespace mbgl { +namespace android { + +// OfflineRegionDefinition // + +void OfflineRegionDefinition::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +mbgl::OfflineRegionDefinition OfflineRegionDefinition::getDefinition(JNIEnv& env, + const jni::Object& jDefinition) { + if (jDefinition.IsInstanceOf(env, jni::Class::Singleton(env))) { + return OfflineTilePyramidRegionDefinition::getDefinition(env, jni::Cast(env, jni::Class::Singleton(env), jDefinition)); + } else if (jDefinition.IsInstanceOf(env, jni::Class::Singleton(env))) { + return OfflineGeometryRegionDefinition::getDefinition(env, jni::Cast(env, jni::Class::Singleton(env), jDefinition)); + } + + throw std::runtime_error("Unknown offline region definition java class"); +} + +// OfflineTilePyramidRegionDefinition // + +jni::Local> OfflineTilePyramidRegionDefinition::New(jni::JNIEnv& env, const mbgl::OfflineTilePyramidRegionDefinition& definition) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor, jni::jdouble, jni::jdouble, jni::jfloat, jni::jboolean>(env); + + return javaClass.New(env, constructor, + jni::Make(env, definition.styleURL), + LatLngBounds::New(env, definition.bounds), + definition.minZoom, + definition.maxZoom, + definition.pixelRatio, + jni::jboolean(definition.includeIdeographs)); +} + +mbgl::OfflineTilePyramidRegionDefinition OfflineTilePyramidRegionDefinition::getDefinition(jni::JNIEnv& env, const jni::Object& jDefinition) { + // Field references + static auto& javaClass = jni::Class::Singleton(env); + static auto styleURLF = javaClass.GetField(env, "styleURL"); + static auto boundsF = javaClass.GetField>(env, "bounds"); + static auto minZoomF = javaClass.GetField(env, "minZoom"); + static auto maxZoomF = javaClass.GetField(env, "maxZoom"); + static auto pixelRatioF = javaClass.GetField(env, "pixelRatio"); + static auto includeIdeographsF = javaClass.GetField(env, "includeIdeographs"); + + return mbgl::OfflineTilePyramidRegionDefinition( + jni::Make(env, jDefinition.Get(env, styleURLF)), + LatLngBounds::getLatLngBounds(env, jDefinition.Get(env, boundsF)), + jDefinition.Get(env, minZoomF), + jDefinition.Get(env, maxZoomF), + jDefinition.Get(env, pixelRatioF), + jDefinition.Get(env, includeIdeographsF) + ); +} + +void OfflineTilePyramidRegionDefinition::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +// OfflineGeometryRegionDefinition // + +jni::Local> OfflineGeometryRegionDefinition::New(jni::JNIEnv& env, const mbgl::OfflineGeometryRegionDefinition& definition) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor, jni::jdouble, jni::jdouble, jni::jfloat, jni::jboolean>(env); + + return javaClass.New(env, constructor, + jni::Make(env, definition.styleURL), + geojson::Geometry::New(env, definition.geometry), + definition.minZoom, + definition.maxZoom, + definition.pixelRatio, + jni::jboolean(definition.includeIdeographs)); +} + +mbgl::OfflineGeometryRegionDefinition OfflineGeometryRegionDefinition::getDefinition(jni::JNIEnv& env, const jni::Object& jDefinition) { + // Field references + static auto& javaClass = jni::Class::Singleton(env); + static auto styleURLF = javaClass.GetField(env, "styleURL"); + static auto geometryF = javaClass.GetField>(env, "geometry"); + static auto minZoomF = javaClass.GetField(env, "minZoom"); + static auto maxZoomF = javaClass.GetField(env, "maxZoom"); + static auto pixelRatioF = javaClass.GetField(env, "pixelRatio"); + static auto includeIdeographsF = javaClass.GetField(env, "includeIdeographs"); + + return mbgl::OfflineGeometryRegionDefinition( + jni::Make(env, jDefinition.Get(env, styleURLF)), + geojson::Geometry::convert(env, jDefinition.Get(env, geometryF)), + jDefinition.Get(env, minZoomF), + jDefinition.Get(env, maxZoomF), + jDefinition.Get(env, pixelRatioF), + jDefinition.Get(env, includeIdeographsF) + ); +} + +void OfflineGeometryRegionDefinition::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl diff --git a/src/offline/offline_region_definition.hpp b/src/offline/offline_region_definition.hpp new file mode 100644 index 000000000..827fac0a8 --- /dev/null +++ b/src/offline/offline_region_definition.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace android { + +class OfflineRegionDefinition { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineRegionDefinition"; }; + + static void registerNative(jni::JNIEnv&); + + static mbgl::OfflineRegionDefinition getDefinition(JNIEnv& env, const jni::Object& jDefinition); +}; + +class OfflineTilePyramidRegionDefinition { +public: + using SuperTag = OfflineRegionDefinition; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineTilePyramidRegionDefinition"; }; + + static jni::Local> New(jni::JNIEnv&, const mbgl::OfflineTilePyramidRegionDefinition&); + + static mbgl::OfflineTilePyramidRegionDefinition getDefinition(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +class OfflineGeometryRegionDefinition { +public: + using SuperTag = OfflineRegionDefinition; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineGeometryRegionDefinition"; }; + + static jni::Local> New(jni::JNIEnv&, const mbgl::OfflineGeometryRegionDefinition&); + + static mbgl::OfflineGeometryRegionDefinition getDefinition(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace android +} // namespace mbgl diff --git a/src/offline/offline_region_error.cpp b/src/offline/offline_region_error.cpp new file mode 100644 index 000000000..199bbafa6 --- /dev/null +++ b/src/offline/offline_region_error.cpp @@ -0,0 +1,44 @@ +#include "offline_region_error.hpp" + +namespace mbgl { +namespace android { + +jni::Local> OfflineRegionError::New(jni::JNIEnv& env, mbgl::Response::Error error) { + + // Handle the value of reason independently of the underlying int value + std::string reason; + switch(error.reason) { + case mbgl::Response::Error::Reason::Success: + reason = "REASON_SUCCESS"; + break; + case mbgl::Response::Error::Reason::NotFound: + reason = "REASON_NOT_FOUND"; + break; + case mbgl::Response::Error::Reason::Server: + reason = "REASON_SERVER"; + break; + case mbgl::Response::Error::Reason::Connection: + reason = "REASON_CONNECTION"; + break; + case mbgl::Response::Error::Reason::RateLimit: + reason = "REASON_RATE_LIMIT"; + break; + case mbgl::Response::Error::Reason::Other: + reason = "REASON_OTHER"; + break; + } + + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + + return javaClass.New(env, constructor, + jni::Make(env, reason), + jni::Make(env, error.message)); +} + +void OfflineRegionError::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl diff --git a/src/offline/offline_region_error.hpp b/src/offline/offline_region_error.hpp new file mode 100644 index 000000000..151e05ab1 --- /dev/null +++ b/src/offline/offline_region_error.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace android { + +class OfflineRegionError { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineRegionError"; }; + + static jni::Local> New(jni::JNIEnv&, mbgl::Response::Error); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace android +} // namespace mbgl diff --git a/src/offline/offline_region_status.cpp b/src/offline/offline_region_status.cpp new file mode 100644 index 000000000..5dd9f20a7 --- /dev/null +++ b/src/offline/offline_region_status.cpp @@ -0,0 +1,38 @@ +#include "offline_region_status.hpp" + +namespace mbgl { +namespace android { + +jni::Local> OfflineRegionStatus::New(jni::JNIEnv& env, mbgl::OfflineRegionStatus status) { + + // Convert to jint + jint downloadState; + switch(status.downloadState) { + case mbgl::OfflineRegionDownloadState::Inactive: + downloadState = 0; + break; + case mbgl::OfflineRegionDownloadState::Active: + downloadState = 1; + break; + } + + // Create java object + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, + downloadState, + jlong(status.completedResourceCount), + jlong(status.completedResourceSize), + jlong(status.completedTileCount), + jlong(status.completedTileSize), + jlong(status.requiredResourceCount), + jboolean(status.requiredResourceCountIsPrecise) + ); +} + +void OfflineRegionStatus::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl diff --git a/src/offline/offline_region_status.hpp b/src/offline/offline_region_status.hpp new file mode 100644 index 000000000..9ef48ec35 --- /dev/null +++ b/src/offline/offline_region_status.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace android { + +class OfflineRegionStatus { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/offline/OfflineRegionStatus"; }; + + static jni::Local> New(jni::JNIEnv&, mbgl::OfflineRegionStatus status); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace android +} // namespace mbgl diff --git a/src/run_loop.cpp b/src/run_loop.cpp new file mode 100644 index 000000000..a9cbfb57a --- /dev/null +++ b/src/run_loop.cpp @@ -0,0 +1,256 @@ +#include "run_loop_impl.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define PIPE_OUT 0 +#define PIPE_IN 1 + +namespace { + +using namespace mbgl::util; + +int looperCallbackNew(int fd, int, void* data) { + int buffer[1]; + while (read(fd, buffer, sizeof(buffer)) > 0) {} + + auto runLoopImpl = reinterpret_cast(data); + + runLoopImpl->coalesce.clear(); + ALooper_wake(runLoopImpl->loop); + + return 1; +} + +int looperCallbackDefault(int fd, int, void* data) { + int buffer[1]; + while (read(fd, buffer, sizeof(buffer)) > 0) {} + + auto runLoopImpl = reinterpret_cast(data); + + runLoopImpl->coalesce.clear(); + runLoopImpl->runLoop->runOnce(); + + if (!runLoopImpl->running) { + ALooper_wake(runLoopImpl->loop); + } + + return 1; +} + +} // namespace + +namespace mbgl { +namespace util { + +// This is needed only for the RunLoop living on the main thread because of +// how we implement timers. We sleep on `ALooper_pollAll` until the next +// timeout, but on the main thread `ALooper_pollAll` is called by the activity +// automatically, thus we cannot set the timeout. Instead we wake the loop +// with an external file descriptor event coming from this thread. +// +// Usually an actor should not carry pointers to other threads, but in +// this case the RunLoop itself owns the Alarm and calling wake() is the most +// efficient way of waking up the RunLoop and it is also thread-safe. +class Alarm { +public: + Alarm(ActorRef, RunLoop::Impl* loop_) : loop(loop_) {} + + void set(const Milliseconds& timeout) { + alarm.start(timeout, mbgl::Duration::zero(), [this]() { loop->wake(); }); + } + +private: + Timer alarm; + RunLoop::Impl* loop; +}; + +RunLoop::Impl::Impl(RunLoop* runLoop_, RunLoop::Type type) : runLoop(runLoop_) { + loop = ALooper_prepare(0); + assert(loop); + + ALooper_acquire(loop); + + if (pipe(fds)) { + throw std::runtime_error("Failed to create pipe."); + } + + if (fcntl(fds[PIPE_OUT], F_SETFL, O_NONBLOCK)) { + throw std::runtime_error("Failed to set pipe read end non-blocking."); + } + + int ret = 0; + + switch (type) { + case Type::New: + ret = ALooper_addFd(loop, fds[PIPE_OUT], ALOOPER_POLL_CALLBACK, + ALOOPER_EVENT_INPUT, looperCallbackNew, this); + break; + case Type::Default: + ret = ALooper_addFd(loop, fds[PIPE_OUT], ALOOPER_POLL_CALLBACK, + ALOOPER_EVENT_INPUT, looperCallbackDefault, this); + alarm = std::make_unique>("Alarm", this); + running = true; + break; + } + + if (ret != 1) { + throw std::runtime_error("Failed to add file descriptor to Looper."); + } +} + +RunLoop::Impl::~Impl() { + alarm.reset(); + + if (ALooper_removeFd(loop, fds[PIPE_OUT]) != 1) { + Log::Error(mbgl::Event::General, "Failed to remove file descriptor from Looper"); + } + + if (close(fds[PIPE_IN]) || close(fds[PIPE_OUT])) { + Log::Error(mbgl::Event::General, "Failed to close file descriptor."); + } + + ALooper_release(loop); +} + +void RunLoop::Impl::wake() { + if (coalesce.test_and_set(std::memory_order_acquire)) { + return; + } + + if (write(fds[PIPE_IN], "\n", 1) == -1) { + throw std::runtime_error("Failed to write to file descriptor."); + } +} + +void RunLoop::Impl::addRunnable(Runnable* runnable) { + { + std::lock_guard lock(mutex); + runnables.push_back(runnable); + } + + wake(); +} + +void RunLoop::Impl::removeRunnable(Runnable* runnable) { + std::lock_guard lock(mutex); + runnables.remove(runnable); +} + +Milliseconds RunLoop::Impl::processRunnables() { + auto now = Clock::now(); + auto nextDue = TimePoint::max(); + std::list tmp; + { + std::lock_guard lock(mutex); + + // O(N) but in the render thread where we get tons + // of messages, the size of the list is usually 1~2. + auto it = runnables.begin(); + while (it != runnables.end()) { + Runnable* runnable = *it; + + auto const dueTime = runnable->dueTime(); + if (dueTime <= now) { + tmp.push_back(runnable); + runnables.erase(it++); + } else { + nextDue = std::min(nextDue, dueTime); + ++it; + } + } + } + + for (auto runnable : tmp) { + runnable->runTask(); + } + + if (runnables.empty() || nextDue == TimePoint::max()) { + return Milliseconds(-1); + } + + auto timeout = std::chrono::duration_cast(nextDue - now); + if (alarm) { + alarm->actor().invoke(&Alarm::set, timeout); + } + + return timeout; +} + +RunLoop* RunLoop::Get() { + assert(static_cast(Scheduler::GetCurrent())); + return static_cast(Scheduler::GetCurrent()); +} + +RunLoop::RunLoop(Type type) : impl(std::make_unique(this, type)) { + Scheduler::SetCurrent(this); +} + +RunLoop::~RunLoop() { + Scheduler::SetCurrent(nullptr); +} + +LOOP_HANDLE RunLoop::getLoopHandle() { + return Get()->impl.get(); +} + +void RunLoop::wake() { + impl->wake(); +} + +void RunLoop::run() { + MBGL_VERIFY_THREAD(tid); + + impl->running = true; + + int outFd, outEvents; + char *outData = nullptr; + + while (impl->running) { + process(); + auto timeout = impl->processRunnables().count(); + ALooper_pollAll(timeout, &outFd, &outEvents, reinterpret_cast(&outData)); + } +} + +void RunLoop::runOnce() { + MBGL_VERIFY_THREAD(tid); + + process(); + impl->processRunnables(); +} + +void RunLoop::stop() { + invoke([&] { + impl->running = false; + impl->wake(); + }); +} + +void RunLoop::addWatch(int, Event, std::function&&) { + throw std::runtime_error("Not implemented."); +} + +void RunLoop::removeWatch(int) { + throw std::runtime_error("Not implemented."); +} + +} // namespace util +} // namespace mbgl diff --git a/src/run_loop_impl.hpp b/src/run_loop_impl.hpp new file mode 100644 index 000000000..c6a1b23a7 --- /dev/null +++ b/src/run_loop_impl.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "jni.hpp" + +#include +#include + +#include +#include +#include +#include + +struct ALooper; + +namespace mbgl { +namespace util { + +template class Thread; +class Alarm; + +class RunLoop::Impl { +public: + class Runnable { + public: + virtual ~Runnable() = default; + + virtual void runTask() = 0; + virtual TimePoint dueTime() const = 0; + }; + + Impl(RunLoop*, RunLoop::Type); + ~Impl(); + + void wake(); + + void addRunnable(Runnable*); + void removeRunnable(Runnable*); + + Milliseconds processRunnables(); + + ALooper* loop = nullptr; + RunLoop* runLoop = nullptr; + std::atomic running; + std::atomic_flag coalesce = ATOMIC_FLAG_INIT; + +private: + friend RunLoop; + + int fds[2]; + + std::unique_ptr> alarm; + + std::mutex mutex; + std::list runnables; +}; + +} // namespace util +} // namespace mbgl diff --git a/src/snapshotter/map_snapshot.cpp b/src/snapshotter/map_snapshot.cpp new file mode 100644 index 000000000..1650f72dc --- /dev/null +++ b/src/snapshotter/map_snapshot.cpp @@ -0,0 +1,65 @@ +#include "map_snapshot.hpp" + +#include "../bitmap.hpp" +#include "../conversion/collection.hpp" + +#include + +namespace mbgl { +namespace android { + +MapSnapshot::MapSnapshot(float pixelRatio_, MapSnapshot::PointForFn pointForFn_, MapSnapshot::LatLngForFn latLngForFn_) + : pixelRatio(pixelRatio_) + , pointForFn(std::move(pointForFn_)) + , latLngForFn(std::move(latLngForFn_)) { +} + +MapSnapshot::~MapSnapshot() = default; + +jni::Local> MapSnapshot::pixelForLatLng(jni::JNIEnv& env, jni::Object& jLatLng) { + ScreenCoordinate point = pointForFn(LatLng::getLatLng(env, jLatLng)); + return PointF::New(env, point.x * pixelRatio, point.y * pixelRatio); +} + +jni::Local> MapSnapshot::latLngForPixel(jni::JNIEnv& env, jni::Object& jPoint) { + return LatLng::New(env, latLngForFn(PointF::getScreenCoordinate(env, jPoint))); +} + +// Static methods // + +jni::Local> MapSnapshot::New(JNIEnv& env, + PremultipliedImage&& image, + float pixelRatio, + std::vector attributions, + bool showLogo, + mbgl::MapSnapshotter::PointForFn pointForFn, + mbgl::MapSnapshotter::LatLngForFn latLngForFn) { + // Create the bitmap + auto bitmap = Bitmap::CreateBitmap(env, std::move(image)); + + // Create the Mapsnapshot peers + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor, jni::Array, jni::jboolean>(env); + auto nativePeer = std::make_unique(pixelRatio, pointForFn, latLngForFn); + return javaClass.New(env, constructor, reinterpret_cast(nativePeer.release()), bitmap, conversion::toArray(env, attributions), (jni::jboolean) showLogo); +} + +void MapSnapshot::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + +#define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer(env, javaClass, + "nativePtr", + std::make_unique, + "initialize", + "finalize", + METHOD(&MapSnapshot::latLngForPixel, "latLngForPixel"), + METHOD(&MapSnapshot::pixelForLatLng, "pixelForLatLng") + ); +} + +} // namespace android +} // namespace mbgl diff --git a/src/snapshotter/map_snapshot.hpp b/src/snapshotter/map_snapshot.hpp new file mode 100644 index 000000000..6b82d0283 --- /dev/null +++ b/src/snapshotter/map_snapshot.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include + +#include "../geometry/lat_lng.hpp" +#include "../graphics/pointf.hpp" + +#include +#include + +namespace mbgl { +namespace android { + +class MapSnapshot { +public: + + using PointForFn = mbgl::MapSnapshotter::PointForFn; + using LatLngForFn = mbgl::MapSnapshotter::LatLngForFn; + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/snapshotter/MapSnapshot"; }; + + static void registerNative(jni::JNIEnv&); + + static jni::Local> New(JNIEnv& env, + PremultipliedImage&& image, + float pixelRatio, + std::vector attributions, + bool showLogo, + PointForFn pointForFn, + LatLngForFn latLngForFn); + + MapSnapshot(jni::JNIEnv&) {}; + MapSnapshot(float pixelRatio, PointForFn, LatLngForFn); + ~MapSnapshot(); + + jni::Local> pixelForLatLng(jni::JNIEnv&, jni::Object&); + jni::Local> latLngForPixel(jni::JNIEnv&, jni::Object&); + +private: + float pixelRatio; + mbgl::MapSnapshotter::PointForFn pointForFn; + mbgl::MapSnapshotter::LatLngForFn latLngForFn; +}; + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/snapshotter/map_snapshotter.cpp b/src/snapshotter/map_snapshotter.cpp new file mode 100644 index 000000000..0b38269ad --- /dev/null +++ b/src/snapshotter/map_snapshotter.cpp @@ -0,0 +1,176 @@ +#include "map_snapshotter.hpp" + +#include +#include +#include +#include +#include + +#include "../attach_env.hpp" +#include "map_snapshot.hpp" + +namespace mbgl { +namespace android { + +MapSnapshotter::MapSnapshotter(jni::JNIEnv& _env, + const jni::Object& _obj, + const jni::Object& _jFileSource, + jni::jfloat _pixelRatio, + jni::jint width, + jni::jint height, + const jni::String& styleURL, + const jni::String& styleJSON, + const jni::Object& region, + const jni::Object& position, + jni::jboolean _showLogo, + const jni::String& _localIdeographFontFamily) + : javaPeer(_env, _obj) + , pixelRatio(_pixelRatio) { + + // Get a reference to the JavaVM for callbacks + if (_env.GetJavaVM(&vm) < 0) { + _env.ExceptionDescribe(); + return; + } + + jFileSource = FileSource::getNativePeer(_env, _jFileSource); + auto size = mbgl::Size { static_cast(width), static_cast(height) }; + + optional cameraOptions; + if (position) { + cameraOptions = CameraPosition::getCameraOptions(_env, position, pixelRatio); + } + + optional bounds; + if (region) { + bounds = LatLngBounds::getLatLngBounds(_env, region); + } + + std::pair style; + if (styleJSON) { + style = std::make_pair(true, jni::Make(_env, styleJSON)); + } else { + style = std::make_pair(false, jni::Make(_env, styleURL)); + } + + showLogo = _showLogo; + // Create the core snapshotter + snapshotter = std::make_unique(style, + size, + pixelRatio, + cameraOptions, + bounds, + _localIdeographFontFamily ? + jni::Make(_env, _localIdeographFontFamily) : + optional{}, + mbgl::android::FileSource::getSharedResourceOptions(_env, _jFileSource)); +} + +MapSnapshotter::~MapSnapshotter() = default; + +void MapSnapshotter::start(JNIEnv& env) { + MBGL_VERIFY_THREAD(tid); + activateFilesource(env); + + snapshotCallback = std::make_unique>( + *Scheduler::GetCurrent(), + [this](std::exception_ptr err, PremultipliedImage image, std::vector attributions, mbgl::MapSnapshotter::PointForFn pointForFn, mbgl::MapSnapshotter::LatLngForFn latLngForFn) { + MBGL_VERIFY_THREAD(tid); + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + + if (err) { + // error handler callback + static auto onSnapshotFailed = javaClass.GetMethod(*_env, "onSnapshotFailed"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onSnapshotFailed, jni::Make(*_env, util::toString(err))); + } + } else { + // Create the wrapper + auto mapSnapshot = android::MapSnapshot::New(*_env, std::move(image), pixelRatio, attributions, showLogo, pointForFn, latLngForFn); + + // invoke callback + static auto onSnapshotReady = javaClass.GetMethod)>(*_env, "onSnapshotReady"); + auto weakReference = javaPeer.get(*_env); + if (weakReference) { + weakReference.Call(*_env, onSnapshotReady, mapSnapshot); + } + } + + deactivateFilesource(*_env); + }); + + snapshotter->snapshot(snapshotCallback->self()); +} + +void MapSnapshotter::cancel(JNIEnv& env) { + MBGL_VERIFY_THREAD(tid); + snapshotCallback.reset(); + deactivateFilesource(env); +} + +void MapSnapshotter::setStyleUrl(JNIEnv& env, const jni::String& styleURL) { + snapshotter->setStyleURL(jni::Make(env, styleURL)); +} + +void MapSnapshotter::setStyleJson(JNIEnv& env, const jni::String& styleJSON) { + snapshotter->setStyleJSON(jni::Make(env, styleJSON)); +} + +void MapSnapshotter::setSize(JNIEnv&, jni::jint width, jni::jint height) { + auto size = mbgl::Size { static_cast(width), static_cast(height) }; + snapshotter->setSize(size); +} + +void MapSnapshotter::setCameraPosition(JNIEnv& env, const jni::Object& position) { + auto options = CameraPosition::getCameraOptions(env, position, pixelRatio); + snapshotter->setCameraOptions(options); +} + +void MapSnapshotter::setRegion(JNIEnv& env, const jni::Object& region) { + snapshotter->setRegion(LatLngBounds::getLatLngBounds(env, region)); +} + + +// Private methods // + +void MapSnapshotter::activateFilesource(JNIEnv& env) { + if (!activatedFilesource) { + activatedFilesource = true; + jFileSource->resume(env); + } +} + +void MapSnapshotter::deactivateFilesource(JNIEnv& env) { + if (activatedFilesource) { + activatedFilesource = false; + jFileSource->pause(env); + } +} + +// Static methods // + +void MapSnapshotter::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + +#define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer(env, javaClass, "nativePtr", + jni::MakePeer&, const jni::Object&, jni::jfloat, jni::jint, jni::jint, const jni::String&, const jni::String&, const jni::Object&, const jni::Object&, jni::jboolean, const jni::String&>, + "nativeInitialize", + "finalize", + METHOD(&MapSnapshotter::setStyleUrl, "setStyleUrl"), + METHOD(&MapSnapshotter::setStyleJson, "setStyleJson"), + METHOD(&MapSnapshotter::setSize, "setSize"), + METHOD(&MapSnapshotter::setCameraPosition, "setCameraPosition"), + METHOD(&MapSnapshotter::setRegion, "setRegion"), + METHOD(&MapSnapshotter::start, "nativeStart"), + METHOD(&MapSnapshotter::cancel, "nativeCancel") + ); +} + +} // namespace android +} // namespace mbgl diff --git a/src/snapshotter/map_snapshotter.hpp b/src/snapshotter/map_snapshotter.hpp new file mode 100644 index 000000000..608a4c855 --- /dev/null +++ b/src/snapshotter/map_snapshotter.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +#include "../file_source.hpp" +#include "../geometry/lat_lng_bounds.hpp" +#include "../map/camera_position.hpp" + +#include + +#include + +namespace mbgl { +namespace android { + +class SnapshotterRendererFrontend; + +class MapSnapshotter { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/snapshotter/MapSnapshotter"; }; + + static void registerNative(jni::JNIEnv&); + + MapSnapshotter(jni::JNIEnv&, + const jni::Object&, + const jni::Object&, + jni::jfloat pixelRatio, + jni::jint width, + jni::jint height, + const jni::String& styleURL, + const jni::String& styleJSON, + const jni::Object& region, + const jni::Object& position, + jni::jboolean showLogo, + const jni::String& localIdeographFontFamily); + + ~MapSnapshotter(); + + void setStyleUrl(JNIEnv&, const jni::String& styleURL); + + void setStyleJson(JNIEnv&, const jni::String& styleJSON); + + void setSize(JNIEnv&, jni::jint width, jni::jint height); + + void setCameraPosition(JNIEnv&, const jni::Object& position); + + void setRegion(JNIEnv&, const jni::Object& region); + + void start(JNIEnv&); + + void cancel(JNIEnv&); + +private: + MBGL_STORE_THREAD(tid); + + JavaVM *vm = nullptr; + jni::WeakReference, jni::EnvAttachingDeleter> javaPeer; + + float pixelRatio; + bool showLogo; + + std::unique_ptr> snapshotCallback; + std::unique_ptr snapshotter; + + FileSource *jFileSource; + void activateFilesource(JNIEnv&); + void deactivateFilesource(JNIEnv&); + bool activatedFilesource = false; +}; + +} // namespace android +} // namespace mbgl diff --git a/src/string_util.cpp b/src/string_util.cpp new file mode 100644 index 000000000..f02155caf --- /dev/null +++ b/src/string_util.cpp @@ -0,0 +1,25 @@ +#include +#include "attach_env.hpp" +#include + +namespace mbgl { +namespace platform { + +std::string uppercase(const std::string& str) { + auto env{ android::AttachEnv() }; + jni::Local value = jni::Make(*env, str.c_str()); + static auto toUpperCase = jni::Class::Singleton(*env).GetMethod(*env, "toUpperCase"); + auto result = value.Call(*env, toUpperCase); + return jni::Make(*env, result); +} + +std::string lowercase(const std::string& str) { + auto env{ android::AttachEnv() }; + jni::Local value = jni::Make(*env, str.c_str()); + static auto toLowerCase = jni::Class::Singleton(*env).GetMethod(*env, "toLowerCase"); + auto result = value.Call(*env, toLowerCase); + return jni::Make(*env, result); +} + +} // namespace platform +} // namespace mbgl diff --git a/src/style/android_conversion.hpp b/src/style/android_conversion.hpp new file mode 100644 index 000000000..dc0a0acb3 --- /dev/null +++ b/src/style/android_conversion.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include "value.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace mbgl { +namespace style { +namespace conversion { + +template <> +class ConversionTraits { +public: + static bool isUndefined(const mbgl::android::Value& value) { + return value.isNull(); + } + + static bool isArray(const mbgl::android::Value& value) { + return value.isArray(); + } + + static bool isObject(const mbgl::android::Value& value) { + return value.isObject(); + } + + static std::size_t arrayLength(const mbgl::android::Value& value) { + return value.getLength();; + } + + static mbgl::android::Value arrayMember(const mbgl::android::Value& value, std::size_t i) { + return value.get(i); + } + + static optional objectMember(const mbgl::android::Value& value, const char* key) { + mbgl::android::Value member = value.get(key); + if (!member.isNull()) { + return member; + } else { + return {}; + } + } + + template + static optional eachMember(const mbgl::android::Value& value, Fn&& fn) { + assert(value.isObject()); + mbgl::android::Value keys = value.keyArray(); + std::size_t length = arrayLength(keys); + for(std::size_t i = 0; i < length; ++i){ + const auto k = keys.get(i).toString(); + auto v = value.get(k.c_str()); + optional result = fn(k, std::move(v)); + if (result) { + return result; + } + } + return {}; + } + + static optional toBool(const mbgl::android::Value& value) { + if (value.isBool()) { + return value.toBool(); + } else { + return {}; + } + } + + static optional toNumber(const mbgl::android::Value& value) { + if (value.isNumber()) { + auto num = value.toFloat(); + return num; + } else { + return {}; + } + } + + static optional toDouble(const mbgl::android::Value& value) { + if (value.isNumber()) { + return value.toDouble(); + } else { + return {}; + } + } + + static optional toString(const mbgl::android::Value& value) { + if (value.isString()) { + return value.toString(); + } else { + return {}; + } + } + + static optional toValue(const mbgl::android::Value& value) { + if (value.isNull()) { + return {}; + } else if (value.isBool()) { + return { value.toBool() }; + } else if (value.isString()) { + return { value.toString() }; + } else if (value.isNumber()) { + return { value.toDouble() }; + } else { + return {}; + } + } + + static optional toGeoJSON(const mbgl::android::Value &value, Error &error) { + if (value.isNull() || !value.isString()) { + error = { "no json data found" }; + return {}; + } + return parseGeoJSON(value.toString(), error); + } +}; + +template +optional convert(mbgl::android::Value&& value, Error& error, Args&&...args) { + return convert(Convertible(std::move(value)), error, std::forward(args)...); +} + +} // namespace conversion +} // namespace style +} // namespace mbgl diff --git a/src/style/conversion/filter.cpp b/src/style/conversion/filter.cpp new file mode 100644 index 000000000..d15e2747a --- /dev/null +++ b/src/style/conversion/filter.cpp @@ -0,0 +1,26 @@ +#include "filter.hpp" +#include "../android_conversion.hpp" + +#include +#include + +namespace mbgl { +namespace android { +namespace conversion { + +optional toFilter(jni::JNIEnv& env, const jni::Array>& jfilter) { + mbgl::optional filter; + if (jfilter) { + mbgl::style::conversion::Error error; + auto converted = mbgl::style::conversion::convert(Value(env, jfilter), error); + if (!converted) { + mbgl::Log::Error(mbgl::Event::JNI, "Error converting filter: " + error.message); + } + filter = std::move(*converted); + } + return filter; +} + +} // namespace conversion +} // namespace android +} // namespace mbgl diff --git a/src/style/conversion/filter.hpp b/src/style/conversion/filter.hpp new file mode 100644 index 000000000..920d336a1 --- /dev/null +++ b/src/style/conversion/filter.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#include + +namespace mbgl { +namespace android { +namespace conversion { + +optional toFilter(jni::JNIEnv&, const jni::Array>&); + +} // namespace conversion +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/style/conversion/position.cpp b/src/style/conversion/position.cpp new file mode 100644 index 000000000..e3c334afd --- /dev/null +++ b/src/style/conversion/position.cpp @@ -0,0 +1,24 @@ +#include "position.hpp" + +namespace mbgl { +namespace android { +namespace conversion { + +Result>> Converter>, mbgl::style::Position>::operator()(jni::JNIEnv &env, const mbgl::style::Position &value) const { + std::array cartPosition = value.getSpherical(); + return Position::fromPosition(env, cartPosition[0], cartPosition[1], cartPosition[2]); +} + +Result Converter>::operator()(jni::JNIEnv &env, const jni::Object &value) const { + float radialCoordinate = Position::getRadialCoordinate(env, value); + float azimuthalAngle = Position::getAzimuthalAngle(env, value); + float polarAngle = Position::getPolarAngle(env, value); + std::array cartPosition {{radialCoordinate, azimuthalAngle, polarAngle}}; + mbgl::style::Position position{}; + position.set(cartPosition); + return position; +} + +} +} +} diff --git a/src/style/conversion/position.hpp b/src/style/conversion/position.hpp new file mode 100644 index 000000000..90409d527 --- /dev/null +++ b/src/style/conversion/position.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "../../conversion/conversion.hpp" +#include "../position.hpp" + +#include +#include + +namespace mbgl { +namespace android { +namespace conversion { + +template <> +struct Converter>, mbgl::style::Position> { + Result>> operator()(jni::JNIEnv &env, const mbgl::style::Position &value) const; +}; + +template <> +struct Converter> { + Result operator()(jni::JNIEnv &env, const jni::Object &value) const; +}; + +} +} +} diff --git a/src/style/conversion/property_expression.hpp b/src/style/conversion/property_expression.hpp new file mode 100644 index 000000000..4e26b11f9 --- /dev/null +++ b/src/style/conversion/property_expression.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "../../conversion/conversion.hpp" +#include "../../gson/json_element.hpp" + +#include + +#include + +namespace mbgl { +namespace android { +namespace conversion { + +template +struct Converter>, mbgl::style::PropertyExpression> { + Result>> operator()(jni::JNIEnv& env, const mbgl::style::PropertyExpression& value) const { + return gson::JsonElement::New(env, value.getExpression().serialize()); + } +}; + +} // namespace conversion +} // namespace android +} // namespace mbgl diff --git a/src/style/conversion/property_value.hpp b/src/style/conversion/property_value.hpp new file mode 100644 index 000000000..317705a28 --- /dev/null +++ b/src/style/conversion/property_value.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +#include "../../conversion/conversion.hpp" +#include "../../conversion/constant.hpp" +#include "property_expression.hpp" + +namespace mbgl { +namespace android { +namespace conversion { + +/** + * Conversion from core property value types to Java property value types + */ +template +class PropertyValueEvaluator { +public: + PropertyValueEvaluator(jni::JNIEnv& _env) : env(_env) {} + + jni::Local> operator()(const mbgl::style::Undefined) const { + return jni::Local>(env, nullptr); + } + + jni::Local> operator()(const T& value) const { + return std::move(*convert>>(env, value)); + } + + jni::Local> operator()(const mbgl::style::PropertyExpression& value) const { + return std::move(*convert>>(env, value)); + } + +private: + jni::JNIEnv& env; +}; + +/** + * Convert core property values to java + */ +template +struct Converter>, mbgl::style::PropertyValue> { + Result>> operator()(jni::JNIEnv& env, const mbgl::style::PropertyValue& value) const { + PropertyValueEvaluator evaluator(env); + return value.evaluate(evaluator); + } +}; + +/** + * Convert core heat map color property value to java + */ +template <> +struct Converter>, mbgl::style::ColorRampPropertyValue> { + Result>> operator()(jni::JNIEnv& env, const mbgl::style::ColorRampPropertyValue& value) const { + PropertyValueEvaluator evaluator(env); + return std::move(*convert>>(env, value.evaluate(evaluator))); + } +}; + +} // namespace conversion +} // namespace android +} // namespace mbgl diff --git a/src/style/conversion/transition_options.cpp b/src/style/conversion/transition_options.cpp new file mode 100644 index 000000000..75d9b4cc0 --- /dev/null +++ b/src/style/conversion/transition_options.cpp @@ -0,0 +1,17 @@ +#include "transition_options.hpp" + +namespace mbgl { +namespace android { +namespace conversion { + +Result>> Converter>, mbgl::style::TransitionOptions>::operator()(jni::JNIEnv& env, const mbgl::style::TransitionOptions& value) const { + return TransitionOptions::fromTransitionOptions(env, + std::chrono::duration_cast(value.duration.value_or(mbgl::Duration::zero())).count(), + std::chrono::duration_cast(value.delay.value_or(mbgl::Duration::zero())).count(), + (jboolean) value.enablePlacementTransitions + ); +} + +} +} +} diff --git a/src/style/conversion/transition_options.hpp b/src/style/conversion/transition_options.hpp new file mode 100644 index 000000000..06c042922 --- /dev/null +++ b/src/style/conversion/transition_options.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "../../conversion/conversion.hpp" +#include "../transition_options.hpp" + +#include + +#include + +namespace mbgl { +namespace android { +namespace conversion { + +template<> +struct Converter>, mbgl::style::TransitionOptions> { + Result>> operator()(jni::JNIEnv&, const mbgl::style::TransitionOptions&) const; +}; + +} +} +} diff --git a/src/style/conversion/url_or_tileset.cpp b/src/style/conversion/url_or_tileset.cpp new file mode 100644 index 000000000..2ec585675 --- /dev/null +++ b/src/style/conversion/url_or_tileset.cpp @@ -0,0 +1,30 @@ +#include "url_or_tileset.hpp" +#include "../android_conversion.hpp" + +#include +#include + +namespace mbgl { +namespace android { + +// This conversion is expected not to fail because it's used only in contexts where +// the value was originally a String or TileSet object on the Java side. If it fails +// to convert, it's a bug in our serialization or Java-side static typing. +variant convertURLOrTileset(mbgl::android::Value&& value) { + using namespace mbgl::style::conversion; + + const Convertible convertible(std::move(value)); + if (isObject(convertible)) { + Error error; + optional tileset = convert(convertible, error); + if (!tileset) { + throw std::logic_error(error.message); + } + return { *tileset }; + } else { + return { *toString(convertible) }; + } +} + +} +} diff --git a/src/style/conversion/url_or_tileset.hpp b/src/style/conversion/url_or_tileset.hpp new file mode 100644 index 000000000..f42a9b9a2 --- /dev/null +++ b/src/style/conversion/url_or_tileset.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#include + +#include "../value.hpp" + +namespace mbgl { +namespace android { + +variant convertURLOrTileset(mbgl::android::Value&& value); + +} +} diff --git a/src/style/formatted.cpp b/src/style/formatted.cpp new file mode 100644 index 000000000..69c3454e2 --- /dev/null +++ b/src/style/formatted.cpp @@ -0,0 +1,54 @@ +#include "formatted.hpp" +#include "formatted_section.hpp" +#include "../conversion/conversion.hpp" +#include "../conversion/constant.hpp" + +namespace mbgl { +namespace android { + +void Formatted::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +jni::Local> Formatted::New(jni::JNIEnv& env, const style::expression::Formatted& value) { + static auto& formatted = jni::Class::Singleton(env); + static auto formattedConstructor = formatted.GetConstructor>>(env); + static auto& formattedSectionClass = jni::Class::Singleton(env); + + auto sections = jni::Array>::New(env, value.sections.size()); + for (std::size_t i = 0; i < value.sections.size(); i++) { + auto section = value.sections.at(i); + auto text = jni::Make(env, section.text); + static auto formattedSectionConstructor = formattedSectionClass.GetConstructor(env); + auto formattedSection = formattedSectionClass.New(env, formattedSectionConstructor, text); + + if (section.fontScale) { + double fontScale = section.fontScale.value(); + static auto method = formattedSectionClass.GetMethod(env, "setFontScale"); + formattedSection.Call(env, method, jni::Box(env, fontScale)); + } + + if (section.fontStack) { + auto fontStack = jni::Array::New(env, section.fontStack.value().size()); + for (std::size_t j = 0; j < section.fontStack.value().size(); j++) { + fontStack.Set(env, j, jni::Make(env, section.fontStack.value().at(j))); + } + static auto method = formattedSectionClass.GetMethod)>(env, "setFontStack"); + formattedSection.Call(env, method, fontStack); + } + + if (section.textColor) { + using namespace mbgl::android::conversion; + auto textColor = std::move(*convert>>(env, *section.textColor)); + static auto method = formattedSectionClass.GetMethod)>(env, "setTextColor"); + formattedSection.Call(env, method, textColor); + } + + sections.Set(env, i, formattedSection); + } + + return formatted.New(env, formattedConstructor, sections); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/style/formatted.hpp b/src/style/formatted.hpp new file mode 100644 index 000000000..ca91f6562 --- /dev/null +++ b/src/style/formatted.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include +#include + +namespace mbgl { + namespace android { + + using SuperTag = jni::ObjectTag; + class Formatted : private mbgl::util::noncopyable { + public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/types/Formatted"; }; + + static jni::Local> + New(jni::JNIEnv&, const style::expression::Formatted &value); + + static void registerNative(jni::JNIEnv &); + }; + + } // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/style/formatted_section.cpp b/src/style/formatted_section.cpp new file mode 100644 index 000000000..ddcf76f81 --- /dev/null +++ b/src/style/formatted_section.cpp @@ -0,0 +1,11 @@ +#include "formatted_section.hpp" + +namespace mbgl { +namespace android { + +void FormattedSection::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/style/formatted_section.hpp b/src/style/formatted_section.hpp new file mode 100644 index 000000000..f759a94e8 --- /dev/null +++ b/src/style/formatted_section.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include + +namespace mbgl { +namespace android { + +class FormattedSection : private mbgl::util::noncopyable { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/types/FormattedSection"; }; + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/style/layers/background_layer.cpp b/src/style/layers/background_layer.cpp new file mode 100644 index 000000000..e28fd0798 --- /dev/null +++ b/src/style/layers/background_layer.cpp @@ -0,0 +1,145 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#include "background_layer.hpp" + +#include + +#include "../conversion/property_value.hpp" +#include "../conversion/transition_options.hpp" + +#include + +namespace mbgl { +namespace android { + + inline mbgl::style::BackgroundLayer& toBackgroundLayer(mbgl::style::Layer& layer) { + return static_cast(layer); + } + + /** + * Creates an owning peer object (for layers not attached to the map) from the JVM side + */ + BackgroundLayer::BackgroundLayer(jni::JNIEnv& env, jni::String& layerId) + : Layer(std::make_unique(jni::Make(env, layerId))) { + } + + /** + * Creates a non-owning peer object (for layers currently attached to the map) + */ + BackgroundLayer::BackgroundLayer(mbgl::Map& map, mbgl::style::BackgroundLayer& coreLayer) + : Layer(map, coreLayer) { + } + + /** + * Creates an owning peer object (for layers not attached to the map) + */ + BackgroundLayer::BackgroundLayer(mbgl::Map& map, std::unique_ptr coreLayer) + : Layer(map, std::move(coreLayer)) { + } + + BackgroundLayer::~BackgroundLayer() = default; + + // Property getters + + jni::Local> BackgroundLayer::getBackgroundColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toBackgroundLayer(layer).getBackgroundColor())); + } + + jni::Local> BackgroundLayer::getBackgroundColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toBackgroundLayer(layer).getBackgroundColorTransition(); + return std::move(*convert>>(env, options)); + } + + void BackgroundLayer::setBackgroundColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toBackgroundLayer(layer).setBackgroundColorTransition(options); + } + + jni::Local> BackgroundLayer::getBackgroundPattern(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toBackgroundLayer(layer).getBackgroundPattern())); + } + + jni::Local> BackgroundLayer::getBackgroundPatternTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toBackgroundLayer(layer).getBackgroundPatternTransition(); + return std::move(*convert>>(env, options)); + } + + void BackgroundLayer::setBackgroundPatternTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toBackgroundLayer(layer).setBackgroundPatternTransition(options); + } + + jni::Local> BackgroundLayer::getBackgroundOpacity(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toBackgroundLayer(layer).getBackgroundOpacity())); + } + + jni::Local> BackgroundLayer::getBackgroundOpacityTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toBackgroundLayer(layer).getBackgroundOpacityTransition(); + return std::move(*convert>>(env, options)); + } + + void BackgroundLayer::setBackgroundOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toBackgroundLayer(layer).setBackgroundOpacityTransition(options); + } + + + // BackgroundJavaLayerPeerFactory + + BackgroundJavaLayerPeerFactory::~BackgroundJavaLayerPeerFactory() = default; + + namespace { + jni::Local> createJavaPeer(jni::JNIEnv& env, Layer* layer) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(layer)); + } + } // namespace + + jni::Local> BackgroundJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, mbgl::style::Layer& layer) { + assert(layer.baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new BackgroundLayer(map, toBackgroundLayer(layer))); + } + + jni::Local> BackgroundJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr layer) { + assert(layer->baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new BackgroundLayer(map, std::unique_ptr(static_cast(layer.release())))); + } + + void BackgroundJavaLayerPeerFactory::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer, + "initialize", + "finalize", + METHOD(&BackgroundLayer::getBackgroundColorTransition, "nativeGetBackgroundColorTransition"), + METHOD(&BackgroundLayer::setBackgroundColorTransition, "nativeSetBackgroundColorTransition"), + METHOD(&BackgroundLayer::getBackgroundColor, "nativeGetBackgroundColor"), + METHOD(&BackgroundLayer::getBackgroundPatternTransition, "nativeGetBackgroundPatternTransition"), + METHOD(&BackgroundLayer::setBackgroundPatternTransition, "nativeSetBackgroundPatternTransition"), + METHOD(&BackgroundLayer::getBackgroundPattern, "nativeGetBackgroundPattern"), + METHOD(&BackgroundLayer::getBackgroundOpacityTransition, "nativeGetBackgroundOpacityTransition"), + METHOD(&BackgroundLayer::setBackgroundOpacityTransition, "nativeSetBackgroundOpacityTransition"), + METHOD(&BackgroundLayer::getBackgroundOpacity, "nativeGetBackgroundOpacity")); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/background_layer.hpp b/src/style/layers/background_layer.hpp new file mode 100644 index 000000000..654031c6f --- /dev/null +++ b/src/style/layers/background_layer.hpp @@ -0,0 +1,58 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#pragma once + +#include "layer.hpp" +#include "../transition_options.hpp" +#include +#include +#include + +namespace mbgl { +namespace android { + +class BackgroundLayer : public Layer { +public: + using SuperTag = Layer; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/BackgroundLayer"; }; + + BackgroundLayer(jni::JNIEnv&, jni::String&); + + BackgroundLayer(mbgl::Map&, mbgl::style::BackgroundLayer&); + + BackgroundLayer(mbgl::Map&, std::unique_ptr); + + ~BackgroundLayer(); + + // Properties + + jni::Local> getBackgroundColor(jni::JNIEnv&); + void setBackgroundColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getBackgroundColorTransition(jni::JNIEnv&); + + jni::Local> getBackgroundPattern(jni::JNIEnv&); + void setBackgroundPatternTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getBackgroundPatternTransition(jni::JNIEnv&); + + jni::Local> getBackgroundOpacity(jni::JNIEnv&); + void setBackgroundOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getBackgroundOpacityTransition(jni::JNIEnv&); + +}; // class BackgroundLayer + +class BackgroundJavaLayerPeerFactory final : public JavaLayerPeerFactory, public mbgl::BackgroundLayerFactory { +public: + ~BackgroundJavaLayerPeerFactory() override; + + // JavaLayerPeerFactory overrides. + jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&) final; + jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr) final; + + void registerNative(jni::JNIEnv&) final; + + LayerFactory* getLayerFactory() final { return this; } + +}; // class BackgroundJavaLayerPeerFactory + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/circle_layer.cpp b/src/style/layers/circle_layer.cpp new file mode 100644 index 000000000..f460031dc --- /dev/null +++ b/src/style/layers/circle_layer.cpp @@ -0,0 +1,268 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#include "circle_layer.hpp" + +#include + +#include "../conversion/property_value.hpp" +#include "../conversion/transition_options.hpp" + +#include + +namespace mbgl { +namespace android { + + inline mbgl::style::CircleLayer& toCircleLayer(mbgl::style::Layer& layer) { + return static_cast(layer); + } + + /** + * Creates an owning peer object (for layers not attached to the map) from the JVM side + */ + CircleLayer::CircleLayer(jni::JNIEnv& env, jni::String& layerId, jni::String& sourceId) + : Layer(std::make_unique(jni::Make(env, layerId), jni::Make(env, sourceId))) { + } + + /** + * Creates a non-owning peer object (for layers currently attached to the map) + */ + CircleLayer::CircleLayer(mbgl::Map& map, mbgl::style::CircleLayer& coreLayer) + : Layer(map, coreLayer) { + } + + /** + * Creates an owning peer object (for layers not attached to the map) + */ + CircleLayer::CircleLayer(mbgl::Map& map, std::unique_ptr coreLayer) + : Layer(map, std::move(coreLayer)) { + } + + CircleLayer::~CircleLayer() = default; + + // Property getters + + jni::Local> CircleLayer::getCircleRadius(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toCircleLayer(layer).getCircleRadius())); + } + + jni::Local> CircleLayer::getCircleRadiusTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toCircleLayer(layer).getCircleRadiusTransition(); + return std::move(*convert>>(env, options)); + } + + void CircleLayer::setCircleRadiusTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toCircleLayer(layer).setCircleRadiusTransition(options); + } + + jni::Local> CircleLayer::getCircleColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toCircleLayer(layer).getCircleColor())); + } + + jni::Local> CircleLayer::getCircleColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toCircleLayer(layer).getCircleColorTransition(); + return std::move(*convert>>(env, options)); + } + + void CircleLayer::setCircleColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toCircleLayer(layer).setCircleColorTransition(options); + } + + jni::Local> CircleLayer::getCircleBlur(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toCircleLayer(layer).getCircleBlur())); + } + + jni::Local> CircleLayer::getCircleBlurTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toCircleLayer(layer).getCircleBlurTransition(); + return std::move(*convert>>(env, options)); + } + + void CircleLayer::setCircleBlurTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toCircleLayer(layer).setCircleBlurTransition(options); + } + + jni::Local> CircleLayer::getCircleOpacity(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toCircleLayer(layer).getCircleOpacity())); + } + + jni::Local> CircleLayer::getCircleOpacityTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toCircleLayer(layer).getCircleOpacityTransition(); + return std::move(*convert>>(env, options)); + } + + void CircleLayer::setCircleOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toCircleLayer(layer).setCircleOpacityTransition(options); + } + + jni::Local> CircleLayer::getCircleTranslate(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toCircleLayer(layer).getCircleTranslate())); + } + + jni::Local> CircleLayer::getCircleTranslateTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toCircleLayer(layer).getCircleTranslateTransition(); + return std::move(*convert>>(env, options)); + } + + void CircleLayer::setCircleTranslateTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toCircleLayer(layer).setCircleTranslateTransition(options); + } + + jni::Local> CircleLayer::getCircleTranslateAnchor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toCircleLayer(layer).getCircleTranslateAnchor())); + } + + jni::Local> CircleLayer::getCirclePitchScale(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toCircleLayer(layer).getCirclePitchScale())); + } + + jni::Local> CircleLayer::getCirclePitchAlignment(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toCircleLayer(layer).getCirclePitchAlignment())); + } + + jni::Local> CircleLayer::getCircleStrokeWidth(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toCircleLayer(layer).getCircleStrokeWidth())); + } + + jni::Local> CircleLayer::getCircleStrokeWidthTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toCircleLayer(layer).getCircleStrokeWidthTransition(); + return std::move(*convert>>(env, options)); + } + + void CircleLayer::setCircleStrokeWidthTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toCircleLayer(layer).setCircleStrokeWidthTransition(options); + } + + jni::Local> CircleLayer::getCircleStrokeColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toCircleLayer(layer).getCircleStrokeColor())); + } + + jni::Local> CircleLayer::getCircleStrokeColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toCircleLayer(layer).getCircleStrokeColorTransition(); + return std::move(*convert>>(env, options)); + } + + void CircleLayer::setCircleStrokeColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toCircleLayer(layer).setCircleStrokeColorTransition(options); + } + + jni::Local> CircleLayer::getCircleStrokeOpacity(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toCircleLayer(layer).getCircleStrokeOpacity())); + } + + jni::Local> CircleLayer::getCircleStrokeOpacityTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toCircleLayer(layer).getCircleStrokeOpacityTransition(); + return std::move(*convert>>(env, options)); + } + + void CircleLayer::setCircleStrokeOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toCircleLayer(layer).setCircleStrokeOpacityTransition(options); + } + + + // CircleJavaLayerPeerFactory + + CircleJavaLayerPeerFactory::~CircleJavaLayerPeerFactory() = default; + + namespace { + jni::Local> createJavaPeer(jni::JNIEnv& env, Layer* layer) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(layer)); + } + } // namespace + + jni::Local> CircleJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, mbgl::style::Layer& layer) { + assert(layer.baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new CircleLayer(map, toCircleLayer(layer))); + } + + jni::Local> CircleJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr layer) { + assert(layer->baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new CircleLayer(map, std::unique_ptr(static_cast(layer.release())))); + } + + void CircleJavaLayerPeerFactory::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer, + "initialize", + "finalize", + METHOD(&CircleLayer::getCircleRadiusTransition, "nativeGetCircleRadiusTransition"), + METHOD(&CircleLayer::setCircleRadiusTransition, "nativeSetCircleRadiusTransition"), + METHOD(&CircleLayer::getCircleRadius, "nativeGetCircleRadius"), + METHOD(&CircleLayer::getCircleColorTransition, "nativeGetCircleColorTransition"), + METHOD(&CircleLayer::setCircleColorTransition, "nativeSetCircleColorTransition"), + METHOD(&CircleLayer::getCircleColor, "nativeGetCircleColor"), + METHOD(&CircleLayer::getCircleBlurTransition, "nativeGetCircleBlurTransition"), + METHOD(&CircleLayer::setCircleBlurTransition, "nativeSetCircleBlurTransition"), + METHOD(&CircleLayer::getCircleBlur, "nativeGetCircleBlur"), + METHOD(&CircleLayer::getCircleOpacityTransition, "nativeGetCircleOpacityTransition"), + METHOD(&CircleLayer::setCircleOpacityTransition, "nativeSetCircleOpacityTransition"), + METHOD(&CircleLayer::getCircleOpacity, "nativeGetCircleOpacity"), + METHOD(&CircleLayer::getCircleTranslateTransition, "nativeGetCircleTranslateTransition"), + METHOD(&CircleLayer::setCircleTranslateTransition, "nativeSetCircleTranslateTransition"), + METHOD(&CircleLayer::getCircleTranslate, "nativeGetCircleTranslate"), + METHOD(&CircleLayer::getCircleTranslateAnchor, "nativeGetCircleTranslateAnchor"), + METHOD(&CircleLayer::getCirclePitchScale, "nativeGetCirclePitchScale"), + METHOD(&CircleLayer::getCirclePitchAlignment, "nativeGetCirclePitchAlignment"), + METHOD(&CircleLayer::getCircleStrokeWidthTransition, "nativeGetCircleStrokeWidthTransition"), + METHOD(&CircleLayer::setCircleStrokeWidthTransition, "nativeSetCircleStrokeWidthTransition"), + METHOD(&CircleLayer::getCircleStrokeWidth, "nativeGetCircleStrokeWidth"), + METHOD(&CircleLayer::getCircleStrokeColorTransition, "nativeGetCircleStrokeColorTransition"), + METHOD(&CircleLayer::setCircleStrokeColorTransition, "nativeSetCircleStrokeColorTransition"), + METHOD(&CircleLayer::getCircleStrokeColor, "nativeGetCircleStrokeColor"), + METHOD(&CircleLayer::getCircleStrokeOpacityTransition, "nativeGetCircleStrokeOpacityTransition"), + METHOD(&CircleLayer::setCircleStrokeOpacityTransition, "nativeSetCircleStrokeOpacityTransition"), + METHOD(&CircleLayer::getCircleStrokeOpacity, "nativeGetCircleStrokeOpacity")); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/circle_layer.hpp b/src/style/layers/circle_layer.hpp new file mode 100644 index 000000000..191b3924f --- /dev/null +++ b/src/style/layers/circle_layer.hpp @@ -0,0 +1,84 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#pragma once + +#include "layer.hpp" +#include "../transition_options.hpp" +#include +#include +#include + +namespace mbgl { +namespace android { + +class CircleLayer : public Layer { +public: + using SuperTag = Layer; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/CircleLayer"; }; + + CircleLayer(jni::JNIEnv&, jni::String&, jni::String&); + + CircleLayer(mbgl::Map&, mbgl::style::CircleLayer&); + + CircleLayer(mbgl::Map&, std::unique_ptr); + + ~CircleLayer(); + + // Properties + + jni::Local> getCircleRadius(jni::JNIEnv&); + void setCircleRadiusTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getCircleRadiusTransition(jni::JNIEnv&); + + jni::Local> getCircleColor(jni::JNIEnv&); + void setCircleColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getCircleColorTransition(jni::JNIEnv&); + + jni::Local> getCircleBlur(jni::JNIEnv&); + void setCircleBlurTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getCircleBlurTransition(jni::JNIEnv&); + + jni::Local> getCircleOpacity(jni::JNIEnv&); + void setCircleOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getCircleOpacityTransition(jni::JNIEnv&); + + jni::Local> getCircleTranslate(jni::JNIEnv&); + void setCircleTranslateTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getCircleTranslateTransition(jni::JNIEnv&); + + jni::Local> getCircleTranslateAnchor(jni::JNIEnv&); + + jni::Local> getCirclePitchScale(jni::JNIEnv&); + + jni::Local> getCirclePitchAlignment(jni::JNIEnv&); + + jni::Local> getCircleStrokeWidth(jni::JNIEnv&); + void setCircleStrokeWidthTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getCircleStrokeWidthTransition(jni::JNIEnv&); + + jni::Local> getCircleStrokeColor(jni::JNIEnv&); + void setCircleStrokeColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getCircleStrokeColorTransition(jni::JNIEnv&); + + jni::Local> getCircleStrokeOpacity(jni::JNIEnv&); + void setCircleStrokeOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getCircleStrokeOpacityTransition(jni::JNIEnv&); + +}; // class CircleLayer + +class CircleJavaLayerPeerFactory final : public JavaLayerPeerFactory, public mbgl::CircleLayerFactory { +public: + ~CircleJavaLayerPeerFactory() override; + + // JavaLayerPeerFactory overrides. + jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&) final; + jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr) final; + + void registerNative(jni::JNIEnv&) final; + + LayerFactory* getLayerFactory() final { return this; } + +}; // class CircleJavaLayerPeerFactory + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/custom_layer.cpp b/src/style/layers/custom_layer.cpp new file mode 100644 index 000000000..3f44aa850 --- /dev/null +++ b/src/style/layers/custom_layer.cpp @@ -0,0 +1,70 @@ +#include "custom_layer.hpp" + +#include + +#include + +namespace mbgl { +namespace android { + + CustomLayer::CustomLayer(jni::JNIEnv& env, const jni::String& layerId, jni::jlong host) + : Layer(std::make_unique( + jni::Make(env, layerId), + std::unique_ptr(reinterpret_cast(host))) + ) { + } + + CustomLayer::CustomLayer(mbgl::Map& map, mbgl::style::CustomLayer& coreLayer) + : Layer(map, coreLayer) { + } + + CustomLayer::CustomLayer(mbgl::Map& map, std::unique_ptr coreLayer) + : Layer(map, std::move(coreLayer)) { + } + + CustomLayer::~CustomLayer() = default; + + void CustomLayer::update(jni::JNIEnv&) { + Log::Debug(mbgl::Event::JNI, "Updating map"); + if (map) { + map->triggerRepaint(); + } else { + Log::Error(mbgl::Event::JNI, "No map reference, cannot update"); + } + } + + namespace { + jni::Local> createJavaPeer(jni::JNIEnv& env, Layer* layer) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(layer)); + } + } + + CustomJavaLayerPeerFactory::~CustomJavaLayerPeerFactory() = default; + + jni::Local> CustomJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, mbgl::style::Layer& layer) { + return createJavaPeer(env, new CustomLayer(map, static_cast(layer))); + } + + jni::Local> CustomJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr layer) { + return createJavaPeer(env, new CustomLayer(map, std::unique_ptr(static_cast(layer.release())))); + } + + void CustomJavaLayerPeerFactory::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer, + "initialize", + "finalize", + METHOD(&CustomLayer::update, "nativeUpdate")); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/custom_layer.hpp b/src/style/layers/custom_layer.hpp new file mode 100644 index 000000000..0d6f4a408 --- /dev/null +++ b/src/style/layers/custom_layer.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "layer.hpp" +#include +#include +#include + +namespace mbgl { +namespace android { + +class CustomLayer : public Layer { +public: + using SuperTag = Layer; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/CustomLayer"; }; + + static void registerNative(jni::JNIEnv&); + + CustomLayer(jni::JNIEnv&, const jni::String&, jni::jlong); + CustomLayer(mbgl::Map&, mbgl::style::CustomLayer&); + CustomLayer(mbgl::Map&, std::unique_ptr); + ~CustomLayer(); + + void update(jni::JNIEnv&); + + jni::Local> createJavaPeer(jni::JNIEnv&); + +}; // class CustomLayer + +class CustomJavaLayerPeerFactory final : public JavaLayerPeerFactory, public mbgl::CustomLayerFactory { +public: + ~CustomJavaLayerPeerFactory() override; + + // JavaLayerPeerFactory overrides. + jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&) final; + jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr) final; + + void registerNative(jni::JNIEnv&) final; + + LayerFactory* getLayerFactory() final { return this; } + +}; // class CustomJavaLayerPeerFactory + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/fill_extrusion_layer.cpp b/src/style/layers/fill_extrusion_layer.cpp new file mode 100644 index 000000000..ddfbd7154 --- /dev/null +++ b/src/style/layers/fill_extrusion_layer.cpp @@ -0,0 +1,220 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#include "fill_extrusion_layer.hpp" + +#include + +#include "../conversion/property_value.hpp" +#include "../conversion/transition_options.hpp" + +#include + +namespace mbgl { +namespace android { + + inline mbgl::style::FillExtrusionLayer& toFillExtrusionLayer(mbgl::style::Layer& layer) { + return static_cast(layer); + } + + /** + * Creates an owning peer object (for layers not attached to the map) from the JVM side + */ + FillExtrusionLayer::FillExtrusionLayer(jni::JNIEnv& env, jni::String& layerId, jni::String& sourceId) + : Layer(std::make_unique(jni::Make(env, layerId), jni::Make(env, sourceId))) { + } + + /** + * Creates a non-owning peer object (for layers currently attached to the map) + */ + FillExtrusionLayer::FillExtrusionLayer(mbgl::Map& map, mbgl::style::FillExtrusionLayer& coreLayer) + : Layer(map, coreLayer) { + } + + /** + * Creates an owning peer object (for layers not attached to the map) + */ + FillExtrusionLayer::FillExtrusionLayer(mbgl::Map& map, std::unique_ptr coreLayer) + : Layer(map, std::move(coreLayer)) { + } + + FillExtrusionLayer::~FillExtrusionLayer() = default; + + // Property getters + + jni::Local> FillExtrusionLayer::getFillExtrusionOpacity(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillExtrusionLayer(layer).getFillExtrusionOpacity())); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionOpacityTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toFillExtrusionLayer(layer).getFillExtrusionOpacityTransition(); + return std::move(*convert>>(env, options)); + } + + void FillExtrusionLayer::setFillExtrusionOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toFillExtrusionLayer(layer).setFillExtrusionOpacityTransition(options); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillExtrusionLayer(layer).getFillExtrusionColor())); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toFillExtrusionLayer(layer).getFillExtrusionColorTransition(); + return std::move(*convert>>(env, options)); + } + + void FillExtrusionLayer::setFillExtrusionColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toFillExtrusionLayer(layer).setFillExtrusionColorTransition(options); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionTranslate(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillExtrusionLayer(layer).getFillExtrusionTranslate())); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionTranslateTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toFillExtrusionLayer(layer).getFillExtrusionTranslateTransition(); + return std::move(*convert>>(env, options)); + } + + void FillExtrusionLayer::setFillExtrusionTranslateTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toFillExtrusionLayer(layer).setFillExtrusionTranslateTransition(options); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionTranslateAnchor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillExtrusionLayer(layer).getFillExtrusionTranslateAnchor())); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionPattern(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillExtrusionLayer(layer).getFillExtrusionPattern())); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionPatternTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toFillExtrusionLayer(layer).getFillExtrusionPatternTransition(); + return std::move(*convert>>(env, options)); + } + + void FillExtrusionLayer::setFillExtrusionPatternTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toFillExtrusionLayer(layer).setFillExtrusionPatternTransition(options); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionHeight(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillExtrusionLayer(layer).getFillExtrusionHeight())); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionHeightTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toFillExtrusionLayer(layer).getFillExtrusionHeightTransition(); + return std::move(*convert>>(env, options)); + } + + void FillExtrusionLayer::setFillExtrusionHeightTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toFillExtrusionLayer(layer).setFillExtrusionHeightTransition(options); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionBase(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillExtrusionLayer(layer).getFillExtrusionBase())); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionBaseTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toFillExtrusionLayer(layer).getFillExtrusionBaseTransition(); + return std::move(*convert>>(env, options)); + } + + void FillExtrusionLayer::setFillExtrusionBaseTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toFillExtrusionLayer(layer).setFillExtrusionBaseTransition(options); + } + + jni::Local> FillExtrusionLayer::getFillExtrusionVerticalGradient(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillExtrusionLayer(layer).getFillExtrusionVerticalGradient())); + } + + + // FillExtrusionJavaLayerPeerFactory + + FillExtrusionJavaLayerPeerFactory::~FillExtrusionJavaLayerPeerFactory() = default; + + namespace { + jni::Local> createJavaPeer(jni::JNIEnv& env, Layer* layer) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(layer)); + } + } // namespace + + jni::Local> FillExtrusionJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, mbgl::style::Layer& layer) { + assert(layer.baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new FillExtrusionLayer(map, toFillExtrusionLayer(layer))); + } + + jni::Local> FillExtrusionJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr layer) { + assert(layer->baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new FillExtrusionLayer(map, std::unique_ptr(static_cast(layer.release())))); + } + + void FillExtrusionJavaLayerPeerFactory::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer, + "initialize", + "finalize", + METHOD(&FillExtrusionLayer::getFillExtrusionOpacityTransition, "nativeGetFillExtrusionOpacityTransition"), + METHOD(&FillExtrusionLayer::setFillExtrusionOpacityTransition, "nativeSetFillExtrusionOpacityTransition"), + METHOD(&FillExtrusionLayer::getFillExtrusionOpacity, "nativeGetFillExtrusionOpacity"), + METHOD(&FillExtrusionLayer::getFillExtrusionColorTransition, "nativeGetFillExtrusionColorTransition"), + METHOD(&FillExtrusionLayer::setFillExtrusionColorTransition, "nativeSetFillExtrusionColorTransition"), + METHOD(&FillExtrusionLayer::getFillExtrusionColor, "nativeGetFillExtrusionColor"), + METHOD(&FillExtrusionLayer::getFillExtrusionTranslateTransition, "nativeGetFillExtrusionTranslateTransition"), + METHOD(&FillExtrusionLayer::setFillExtrusionTranslateTransition, "nativeSetFillExtrusionTranslateTransition"), + METHOD(&FillExtrusionLayer::getFillExtrusionTranslate, "nativeGetFillExtrusionTranslate"), + METHOD(&FillExtrusionLayer::getFillExtrusionTranslateAnchor, "nativeGetFillExtrusionTranslateAnchor"), + METHOD(&FillExtrusionLayer::getFillExtrusionPatternTransition, "nativeGetFillExtrusionPatternTransition"), + METHOD(&FillExtrusionLayer::setFillExtrusionPatternTransition, "nativeSetFillExtrusionPatternTransition"), + METHOD(&FillExtrusionLayer::getFillExtrusionPattern, "nativeGetFillExtrusionPattern"), + METHOD(&FillExtrusionLayer::getFillExtrusionHeightTransition, "nativeGetFillExtrusionHeightTransition"), + METHOD(&FillExtrusionLayer::setFillExtrusionHeightTransition, "nativeSetFillExtrusionHeightTransition"), + METHOD(&FillExtrusionLayer::getFillExtrusionHeight, "nativeGetFillExtrusionHeight"), + METHOD(&FillExtrusionLayer::getFillExtrusionBaseTransition, "nativeGetFillExtrusionBaseTransition"), + METHOD(&FillExtrusionLayer::setFillExtrusionBaseTransition, "nativeSetFillExtrusionBaseTransition"), + METHOD(&FillExtrusionLayer::getFillExtrusionBase, "nativeGetFillExtrusionBase"), + METHOD(&FillExtrusionLayer::getFillExtrusionVerticalGradient, "nativeGetFillExtrusionVerticalGradient")); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/fill_extrusion_layer.hpp b/src/style/layers/fill_extrusion_layer.hpp new file mode 100644 index 000000000..15dad67b0 --- /dev/null +++ b/src/style/layers/fill_extrusion_layer.hpp @@ -0,0 +1,74 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#pragma once + +#include "layer.hpp" +#include "../transition_options.hpp" +#include +#include +#include + +namespace mbgl { +namespace android { + +class FillExtrusionLayer : public Layer { +public: + using SuperTag = Layer; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/FillExtrusionLayer"; }; + + FillExtrusionLayer(jni::JNIEnv&, jni::String&, jni::String&); + + FillExtrusionLayer(mbgl::Map&, mbgl::style::FillExtrusionLayer&); + + FillExtrusionLayer(mbgl::Map&, std::unique_ptr); + + ~FillExtrusionLayer(); + + // Properties + + jni::Local> getFillExtrusionOpacity(jni::JNIEnv&); + void setFillExtrusionOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getFillExtrusionOpacityTransition(jni::JNIEnv&); + + jni::Local> getFillExtrusionColor(jni::JNIEnv&); + void setFillExtrusionColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getFillExtrusionColorTransition(jni::JNIEnv&); + + jni::Local> getFillExtrusionTranslate(jni::JNIEnv&); + void setFillExtrusionTranslateTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getFillExtrusionTranslateTransition(jni::JNIEnv&); + + jni::Local> getFillExtrusionTranslateAnchor(jni::JNIEnv&); + + jni::Local> getFillExtrusionPattern(jni::JNIEnv&); + void setFillExtrusionPatternTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getFillExtrusionPatternTransition(jni::JNIEnv&); + + jni::Local> getFillExtrusionHeight(jni::JNIEnv&); + void setFillExtrusionHeightTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getFillExtrusionHeightTransition(jni::JNIEnv&); + + jni::Local> getFillExtrusionBase(jni::JNIEnv&); + void setFillExtrusionBaseTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getFillExtrusionBaseTransition(jni::JNIEnv&); + + jni::Local> getFillExtrusionVerticalGradient(jni::JNIEnv&); + +}; // class FillExtrusionLayer + +class FillExtrusionJavaLayerPeerFactory final : public JavaLayerPeerFactory, public mbgl::FillExtrusionLayerFactory { +public: + ~FillExtrusionJavaLayerPeerFactory() override; + + // JavaLayerPeerFactory overrides. + jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&) final; + jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr) final; + + void registerNative(jni::JNIEnv&) final; + + LayerFactory* getLayerFactory() final { return this; } + +}; // class FillExtrusionJavaLayerPeerFactory + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/fill_layer.cpp b/src/style/layers/fill_layer.cpp new file mode 100644 index 000000000..c54f4cd39 --- /dev/null +++ b/src/style/layers/fill_layer.cpp @@ -0,0 +1,199 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#include "fill_layer.hpp" + +#include + +#include "../conversion/property_value.hpp" +#include "../conversion/transition_options.hpp" + +#include + +namespace mbgl { +namespace android { + + inline mbgl::style::FillLayer& toFillLayer(mbgl::style::Layer& layer) { + return static_cast(layer); + } + + /** + * Creates an owning peer object (for layers not attached to the map) from the JVM side + */ + FillLayer::FillLayer(jni::JNIEnv& env, jni::String& layerId, jni::String& sourceId) + : Layer(std::make_unique(jni::Make(env, layerId), jni::Make(env, sourceId))) { + } + + /** + * Creates a non-owning peer object (for layers currently attached to the map) + */ + FillLayer::FillLayer(mbgl::Map& map, mbgl::style::FillLayer& coreLayer) + : Layer(map, coreLayer) { + } + + /** + * Creates an owning peer object (for layers not attached to the map) + */ + FillLayer::FillLayer(mbgl::Map& map, std::unique_ptr coreLayer) + : Layer(map, std::move(coreLayer)) { + } + + FillLayer::~FillLayer() = default; + + // Property getters + + jni::Local> FillLayer::getFillAntialias(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillLayer(layer).getFillAntialias())); + } + + jni::Local> FillLayer::getFillOpacity(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillLayer(layer).getFillOpacity())); + } + + jni::Local> FillLayer::getFillOpacityTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toFillLayer(layer).getFillOpacityTransition(); + return std::move(*convert>>(env, options)); + } + + void FillLayer::setFillOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toFillLayer(layer).setFillOpacityTransition(options); + } + + jni::Local> FillLayer::getFillColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillLayer(layer).getFillColor())); + } + + jni::Local> FillLayer::getFillColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toFillLayer(layer).getFillColorTransition(); + return std::move(*convert>>(env, options)); + } + + void FillLayer::setFillColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toFillLayer(layer).setFillColorTransition(options); + } + + jni::Local> FillLayer::getFillOutlineColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillLayer(layer).getFillOutlineColor())); + } + + jni::Local> FillLayer::getFillOutlineColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toFillLayer(layer).getFillOutlineColorTransition(); + return std::move(*convert>>(env, options)); + } + + void FillLayer::setFillOutlineColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toFillLayer(layer).setFillOutlineColorTransition(options); + } + + jni::Local> FillLayer::getFillTranslate(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillLayer(layer).getFillTranslate())); + } + + jni::Local> FillLayer::getFillTranslateTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toFillLayer(layer).getFillTranslateTransition(); + return std::move(*convert>>(env, options)); + } + + void FillLayer::setFillTranslateTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toFillLayer(layer).setFillTranslateTransition(options); + } + + jni::Local> FillLayer::getFillTranslateAnchor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillLayer(layer).getFillTranslateAnchor())); + } + + jni::Local> FillLayer::getFillPattern(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toFillLayer(layer).getFillPattern())); + } + + jni::Local> FillLayer::getFillPatternTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toFillLayer(layer).getFillPatternTransition(); + return std::move(*convert>>(env, options)); + } + + void FillLayer::setFillPatternTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toFillLayer(layer).setFillPatternTransition(options); + } + + + // FillJavaLayerPeerFactory + + FillJavaLayerPeerFactory::~FillJavaLayerPeerFactory() = default; + + namespace { + jni::Local> createJavaPeer(jni::JNIEnv& env, Layer* layer) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(layer)); + } + } // namespace + + jni::Local> FillJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, mbgl::style::Layer& layer) { + assert(layer.baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new FillLayer(map, toFillLayer(layer))); + } + + jni::Local> FillJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr layer) { + assert(layer->baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new FillLayer(map, std::unique_ptr(static_cast(layer.release())))); + } + + void FillJavaLayerPeerFactory::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer, + "initialize", + "finalize", + METHOD(&FillLayer::getFillAntialias, "nativeGetFillAntialias"), + METHOD(&FillLayer::getFillOpacityTransition, "nativeGetFillOpacityTransition"), + METHOD(&FillLayer::setFillOpacityTransition, "nativeSetFillOpacityTransition"), + METHOD(&FillLayer::getFillOpacity, "nativeGetFillOpacity"), + METHOD(&FillLayer::getFillColorTransition, "nativeGetFillColorTransition"), + METHOD(&FillLayer::setFillColorTransition, "nativeSetFillColorTransition"), + METHOD(&FillLayer::getFillColor, "nativeGetFillColor"), + METHOD(&FillLayer::getFillOutlineColorTransition, "nativeGetFillOutlineColorTransition"), + METHOD(&FillLayer::setFillOutlineColorTransition, "nativeSetFillOutlineColorTransition"), + METHOD(&FillLayer::getFillOutlineColor, "nativeGetFillOutlineColor"), + METHOD(&FillLayer::getFillTranslateTransition, "nativeGetFillTranslateTransition"), + METHOD(&FillLayer::setFillTranslateTransition, "nativeSetFillTranslateTransition"), + METHOD(&FillLayer::getFillTranslate, "nativeGetFillTranslate"), + METHOD(&FillLayer::getFillTranslateAnchor, "nativeGetFillTranslateAnchor"), + METHOD(&FillLayer::getFillPatternTransition, "nativeGetFillPatternTransition"), + METHOD(&FillLayer::setFillPatternTransition, "nativeSetFillPatternTransition"), + METHOD(&FillLayer::getFillPattern, "nativeGetFillPattern")); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/fill_layer.hpp b/src/style/layers/fill_layer.hpp new file mode 100644 index 000000000..b021ddca1 --- /dev/null +++ b/src/style/layers/fill_layer.hpp @@ -0,0 +1,70 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#pragma once + +#include "layer.hpp" +#include "../transition_options.hpp" +#include +#include +#include + +namespace mbgl { +namespace android { + +class FillLayer : public Layer { +public: + using SuperTag = Layer; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/FillLayer"; }; + + FillLayer(jni::JNIEnv&, jni::String&, jni::String&); + + FillLayer(mbgl::Map&, mbgl::style::FillLayer&); + + FillLayer(mbgl::Map&, std::unique_ptr); + + ~FillLayer(); + + // Properties + + jni::Local> getFillAntialias(jni::JNIEnv&); + + jni::Local> getFillOpacity(jni::JNIEnv&); + void setFillOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getFillOpacityTransition(jni::JNIEnv&); + + jni::Local> getFillColor(jni::JNIEnv&); + void setFillColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getFillColorTransition(jni::JNIEnv&); + + jni::Local> getFillOutlineColor(jni::JNIEnv&); + void setFillOutlineColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getFillOutlineColorTransition(jni::JNIEnv&); + + jni::Local> getFillTranslate(jni::JNIEnv&); + void setFillTranslateTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getFillTranslateTransition(jni::JNIEnv&); + + jni::Local> getFillTranslateAnchor(jni::JNIEnv&); + + jni::Local> getFillPattern(jni::JNIEnv&); + void setFillPatternTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getFillPatternTransition(jni::JNIEnv&); + +}; // class FillLayer + +class FillJavaLayerPeerFactory final : public JavaLayerPeerFactory, public mbgl::FillLayerFactory { +public: + ~FillJavaLayerPeerFactory() override; + + // JavaLayerPeerFactory overrides. + jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&) final; + jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr) final; + + void registerNative(jni::JNIEnv&) final; + + LayerFactory* getLayerFactory() final { return this; } + +}; // class FillJavaLayerPeerFactory + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/heatmap_layer.cpp b/src/style/layers/heatmap_layer.cpp new file mode 100644 index 000000000..509ab5d70 --- /dev/null +++ b/src/style/layers/heatmap_layer.cpp @@ -0,0 +1,161 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#include "heatmap_layer.hpp" + +#include + +#include "../conversion/property_value.hpp" +#include "../conversion/transition_options.hpp" + +#include + +namespace mbgl { +namespace android { + + inline mbgl::style::HeatmapLayer& toHeatmapLayer(mbgl::style::Layer& layer) { + return static_cast(layer); + } + + /** + * Creates an owning peer object (for layers not attached to the map) from the JVM side + */ + HeatmapLayer::HeatmapLayer(jni::JNIEnv& env, jni::String& layerId, jni::String& sourceId) + : Layer(std::make_unique(jni::Make(env, layerId), jni::Make(env, sourceId))) { + } + + /** + * Creates a non-owning peer object (for layers currently attached to the map) + */ + HeatmapLayer::HeatmapLayer(mbgl::Map& map, mbgl::style::HeatmapLayer& coreLayer) + : Layer(map, coreLayer) { + } + + /** + * Creates an owning peer object (for layers not attached to the map) + */ + HeatmapLayer::HeatmapLayer(mbgl::Map& map, std::unique_ptr coreLayer) + : Layer(map, std::move(coreLayer)) { + } + + HeatmapLayer::~HeatmapLayer() = default; + + // Property getters + + jni::Local> HeatmapLayer::getHeatmapRadius(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toHeatmapLayer(layer).getHeatmapRadius())); + } + + jni::Local> HeatmapLayer::getHeatmapRadiusTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toHeatmapLayer(layer).getHeatmapRadiusTransition(); + return std::move(*convert>>(env, options)); + } + + void HeatmapLayer::setHeatmapRadiusTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toHeatmapLayer(layer).setHeatmapRadiusTransition(options); + } + + jni::Local> HeatmapLayer::getHeatmapWeight(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toHeatmapLayer(layer).getHeatmapWeight())); + } + + jni::Local> HeatmapLayer::getHeatmapIntensity(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toHeatmapLayer(layer).getHeatmapIntensity())); + } + + jni::Local> HeatmapLayer::getHeatmapIntensityTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toHeatmapLayer(layer).getHeatmapIntensityTransition(); + return std::move(*convert>>(env, options)); + } + + void HeatmapLayer::setHeatmapIntensityTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toHeatmapLayer(layer).setHeatmapIntensityTransition(options); + } + + jni::Local> HeatmapLayer::getHeatmapColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + auto propertyValue = toHeatmapLayer(layer).getHeatmapColor(); + if (propertyValue.isUndefined()) { + propertyValue = toHeatmapLayer(layer).getDefaultHeatmapColor(); + } + return std::move(*convert>>(env, propertyValue)); + } + + jni::Local> HeatmapLayer::getHeatmapOpacity(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toHeatmapLayer(layer).getHeatmapOpacity())); + } + + jni::Local> HeatmapLayer::getHeatmapOpacityTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toHeatmapLayer(layer).getHeatmapOpacityTransition(); + return std::move(*convert>>(env, options)); + } + + void HeatmapLayer::setHeatmapOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toHeatmapLayer(layer).setHeatmapOpacityTransition(options); + } + + + // HeatmapJavaLayerPeerFactory + + HeatmapJavaLayerPeerFactory::~HeatmapJavaLayerPeerFactory() = default; + + namespace { + jni::Local> createJavaPeer(jni::JNIEnv& env, Layer* layer) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(layer)); + } + } // namespace + + jni::Local> HeatmapJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, mbgl::style::Layer& layer) { + assert(layer.baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new HeatmapLayer(map, toHeatmapLayer(layer))); + } + + jni::Local> HeatmapJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr layer) { + assert(layer->baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new HeatmapLayer(map, std::unique_ptr(static_cast(layer.release())))); + } + + void HeatmapJavaLayerPeerFactory::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer, + "initialize", + "finalize", + METHOD(&HeatmapLayer::getHeatmapRadiusTransition, "nativeGetHeatmapRadiusTransition"), + METHOD(&HeatmapLayer::setHeatmapRadiusTransition, "nativeSetHeatmapRadiusTransition"), + METHOD(&HeatmapLayer::getHeatmapRadius, "nativeGetHeatmapRadius"), + METHOD(&HeatmapLayer::getHeatmapWeight, "nativeGetHeatmapWeight"), + METHOD(&HeatmapLayer::getHeatmapIntensityTransition, "nativeGetHeatmapIntensityTransition"), + METHOD(&HeatmapLayer::setHeatmapIntensityTransition, "nativeSetHeatmapIntensityTransition"), + METHOD(&HeatmapLayer::getHeatmapIntensity, "nativeGetHeatmapIntensity"), + METHOD(&HeatmapLayer::getHeatmapColor, "nativeGetHeatmapColor"), + METHOD(&HeatmapLayer::getHeatmapOpacityTransition, "nativeGetHeatmapOpacityTransition"), + METHOD(&HeatmapLayer::setHeatmapOpacityTransition, "nativeSetHeatmapOpacityTransition"), + METHOD(&HeatmapLayer::getHeatmapOpacity, "nativeGetHeatmapOpacity")); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/heatmap_layer.hpp b/src/style/layers/heatmap_layer.hpp new file mode 100644 index 000000000..9e3db0c89 --- /dev/null +++ b/src/style/layers/heatmap_layer.hpp @@ -0,0 +1,62 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#pragma once + +#include "layer.hpp" +#include "../transition_options.hpp" +#include +#include +#include + +namespace mbgl { +namespace android { + +class HeatmapLayer : public Layer { +public: + using SuperTag = Layer; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/HeatmapLayer"; }; + + HeatmapLayer(jni::JNIEnv&, jni::String&, jni::String&); + + HeatmapLayer(mbgl::Map&, mbgl::style::HeatmapLayer&); + + HeatmapLayer(mbgl::Map&, std::unique_ptr); + + ~HeatmapLayer(); + + // Properties + + jni::Local> getHeatmapRadius(jni::JNIEnv&); + void setHeatmapRadiusTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getHeatmapRadiusTransition(jni::JNIEnv&); + + jni::Local> getHeatmapWeight(jni::JNIEnv&); + + jni::Local> getHeatmapIntensity(jni::JNIEnv&); + void setHeatmapIntensityTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getHeatmapIntensityTransition(jni::JNIEnv&); + + jni::Local> getHeatmapColor(jni::JNIEnv&); + + jni::Local> getHeatmapOpacity(jni::JNIEnv&); + void setHeatmapOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getHeatmapOpacityTransition(jni::JNIEnv&); + +}; // class HeatmapLayer + +class HeatmapJavaLayerPeerFactory final : public JavaLayerPeerFactory, public mbgl::HeatmapLayerFactory { +public: + ~HeatmapJavaLayerPeerFactory() override; + + // JavaLayerPeerFactory overrides. + jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&) final; + jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr) final; + + void registerNative(jni::JNIEnv&) final; + + LayerFactory* getLayerFactory() final { return this; } + +}; // class HeatmapJavaLayerPeerFactory + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/hillshade_layer.cpp b/src/style/layers/hillshade_layer.cpp new file mode 100644 index 000000000..bc25f666a --- /dev/null +++ b/src/style/layers/hillshade_layer.cpp @@ -0,0 +1,178 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#include "hillshade_layer.hpp" + +#include + +#include "../conversion/property_value.hpp" +#include "../conversion/transition_options.hpp" + +#include + +namespace mbgl { +namespace android { + + inline mbgl::style::HillshadeLayer& toHillshadeLayer(mbgl::style::Layer& layer) { + return static_cast(layer); + } + + /** + * Creates an owning peer object (for layers not attached to the map) from the JVM side + */ + HillshadeLayer::HillshadeLayer(jni::JNIEnv& env, jni::String& layerId, jni::String& sourceId) + : Layer(std::make_unique(jni::Make(env, layerId), jni::Make(env, sourceId))) { + } + + /** + * Creates a non-owning peer object (for layers currently attached to the map) + */ + HillshadeLayer::HillshadeLayer(mbgl::Map& map, mbgl::style::HillshadeLayer& coreLayer) + : Layer(map, coreLayer) { + } + + /** + * Creates an owning peer object (for layers not attached to the map) + */ + HillshadeLayer::HillshadeLayer(mbgl::Map& map, std::unique_ptr coreLayer) + : Layer(map, std::move(coreLayer)) { + } + + HillshadeLayer::~HillshadeLayer() = default; + + // Property getters + + jni::Local> HillshadeLayer::getHillshadeIlluminationDirection(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toHillshadeLayer(layer).getHillshadeIlluminationDirection())); + } + + jni::Local> HillshadeLayer::getHillshadeIlluminationAnchor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toHillshadeLayer(layer).getHillshadeIlluminationAnchor())); + } + + jni::Local> HillshadeLayer::getHillshadeExaggeration(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toHillshadeLayer(layer).getHillshadeExaggeration())); + } + + jni::Local> HillshadeLayer::getHillshadeExaggerationTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toHillshadeLayer(layer).getHillshadeExaggerationTransition(); + return std::move(*convert>>(env, options)); + } + + void HillshadeLayer::setHillshadeExaggerationTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toHillshadeLayer(layer).setHillshadeExaggerationTransition(options); + } + + jni::Local> HillshadeLayer::getHillshadeShadowColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toHillshadeLayer(layer).getHillshadeShadowColor())); + } + + jni::Local> HillshadeLayer::getHillshadeShadowColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toHillshadeLayer(layer).getHillshadeShadowColorTransition(); + return std::move(*convert>>(env, options)); + } + + void HillshadeLayer::setHillshadeShadowColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toHillshadeLayer(layer).setHillshadeShadowColorTransition(options); + } + + jni::Local> HillshadeLayer::getHillshadeHighlightColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toHillshadeLayer(layer).getHillshadeHighlightColor())); + } + + jni::Local> HillshadeLayer::getHillshadeHighlightColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toHillshadeLayer(layer).getHillshadeHighlightColorTransition(); + return std::move(*convert>>(env, options)); + } + + void HillshadeLayer::setHillshadeHighlightColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toHillshadeLayer(layer).setHillshadeHighlightColorTransition(options); + } + + jni::Local> HillshadeLayer::getHillshadeAccentColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toHillshadeLayer(layer).getHillshadeAccentColor())); + } + + jni::Local> HillshadeLayer::getHillshadeAccentColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toHillshadeLayer(layer).getHillshadeAccentColorTransition(); + return std::move(*convert>>(env, options)); + } + + void HillshadeLayer::setHillshadeAccentColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toHillshadeLayer(layer).setHillshadeAccentColorTransition(options); + } + + + // HillshadeJavaLayerPeerFactory + + HillshadeJavaLayerPeerFactory::~HillshadeJavaLayerPeerFactory() = default; + + namespace { + jni::Local> createJavaPeer(jni::JNIEnv& env, Layer* layer) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(layer)); + } + } // namespace + + jni::Local> HillshadeJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, mbgl::style::Layer& layer) { + assert(layer.baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new HillshadeLayer(map, toHillshadeLayer(layer))); + } + + jni::Local> HillshadeJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr layer) { + assert(layer->baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new HillshadeLayer(map, std::unique_ptr(static_cast(layer.release())))); + } + + void HillshadeJavaLayerPeerFactory::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer, + "initialize", + "finalize", + METHOD(&HillshadeLayer::getHillshadeIlluminationDirection, "nativeGetHillshadeIlluminationDirection"), + METHOD(&HillshadeLayer::getHillshadeIlluminationAnchor, "nativeGetHillshadeIlluminationAnchor"), + METHOD(&HillshadeLayer::getHillshadeExaggerationTransition, "nativeGetHillshadeExaggerationTransition"), + METHOD(&HillshadeLayer::setHillshadeExaggerationTransition, "nativeSetHillshadeExaggerationTransition"), + METHOD(&HillshadeLayer::getHillshadeExaggeration, "nativeGetHillshadeExaggeration"), + METHOD(&HillshadeLayer::getHillshadeShadowColorTransition, "nativeGetHillshadeShadowColorTransition"), + METHOD(&HillshadeLayer::setHillshadeShadowColorTransition, "nativeSetHillshadeShadowColorTransition"), + METHOD(&HillshadeLayer::getHillshadeShadowColor, "nativeGetHillshadeShadowColor"), + METHOD(&HillshadeLayer::getHillshadeHighlightColorTransition, "nativeGetHillshadeHighlightColorTransition"), + METHOD(&HillshadeLayer::setHillshadeHighlightColorTransition, "nativeSetHillshadeHighlightColorTransition"), + METHOD(&HillshadeLayer::getHillshadeHighlightColor, "nativeGetHillshadeHighlightColor"), + METHOD(&HillshadeLayer::getHillshadeAccentColorTransition, "nativeGetHillshadeAccentColorTransition"), + METHOD(&HillshadeLayer::setHillshadeAccentColorTransition, "nativeSetHillshadeAccentColorTransition"), + METHOD(&HillshadeLayer::getHillshadeAccentColor, "nativeGetHillshadeAccentColor")); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/hillshade_layer.hpp b/src/style/layers/hillshade_layer.hpp new file mode 100644 index 000000000..2f4ea30c2 --- /dev/null +++ b/src/style/layers/hillshade_layer.hpp @@ -0,0 +1,66 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#pragma once + +#include "layer.hpp" +#include "../transition_options.hpp" +#include +#include +#include + +namespace mbgl { +namespace android { + +class HillshadeLayer : public Layer { +public: + using SuperTag = Layer; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/HillshadeLayer"; }; + + HillshadeLayer(jni::JNIEnv&, jni::String&, jni::String&); + + HillshadeLayer(mbgl::Map&, mbgl::style::HillshadeLayer&); + + HillshadeLayer(mbgl::Map&, std::unique_ptr); + + ~HillshadeLayer(); + + // Properties + + jni::Local> getHillshadeIlluminationDirection(jni::JNIEnv&); + + jni::Local> getHillshadeIlluminationAnchor(jni::JNIEnv&); + + jni::Local> getHillshadeExaggeration(jni::JNIEnv&); + void setHillshadeExaggerationTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getHillshadeExaggerationTransition(jni::JNIEnv&); + + jni::Local> getHillshadeShadowColor(jni::JNIEnv&); + void setHillshadeShadowColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getHillshadeShadowColorTransition(jni::JNIEnv&); + + jni::Local> getHillshadeHighlightColor(jni::JNIEnv&); + void setHillshadeHighlightColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getHillshadeHighlightColorTransition(jni::JNIEnv&); + + jni::Local> getHillshadeAccentColor(jni::JNIEnv&); + void setHillshadeAccentColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getHillshadeAccentColorTransition(jni::JNIEnv&); + +}; // class HillshadeLayer + +class HillshadeJavaLayerPeerFactory final : public JavaLayerPeerFactory, public mbgl::HillshadeLayerFactory { +public: + ~HillshadeJavaLayerPeerFactory() override; + + // JavaLayerPeerFactory overrides. + jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&) final; + jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr) final; + + void registerNative(jni::JNIEnv&) final; + + LayerFactory* getLayerFactory() final { return this; } + +}; // class HillshadeJavaLayerPeerFactory + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/layer.cpp b/src/style/layers/layer.cpp new file mode 100644 index 000000000..2f335b6ff --- /dev/null +++ b/src/style/layers/layer.cpp @@ -0,0 +1,198 @@ +#include "layer.hpp" +#include "../android_conversion.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Java -> C++ conversion +#include +#include +#include +#include + +// C++ -> Java conversion +#include "../conversion/property_value.hpp" +#include "custom_layer.hpp" +#include "background_layer.hpp" +#include + +#include + +namespace mbgl { +namespace android { + + /** + * Invoked when the construction is initiated from the jvm through a subclass + */ + Layer::Layer(std::unique_ptr coreLayer) + : ownedLayer(std::move(coreLayer)) + , layer(*ownedLayer) { + } + + /** + * Takes a non-owning reference. For lookup methods + */ + Layer::Layer(mbgl::Map& coreMap, mbgl::style::Layer& coreLayer) : layer(coreLayer) , map(&coreMap) { + } + + /** + * Takes a owning reference. Ownership is transfered to this peer, eg after removing + * from the map + */ + Layer::Layer(mbgl::Map& coreMap, std::unique_ptr coreLayer) + : ownedLayer(std::move(coreLayer)) + , layer(*ownedLayer) + , map(&coreMap) { + } + + Layer::~Layer() { + } + + void Layer::addToMap(mbgl::Map& _map, mbgl::optional before) { + // Check to see if we own the layer first + if (!ownedLayer) { + throw std::runtime_error("Cannot add layer twice"); + } + + // Add layer to map + _map.getStyle().addLayer(releaseCoreLayer(), before); + + // Save pointer to the map + this->map = &_map; + } + + void Layer::setLayer(std::unique_ptr sourceLayer) { + this->ownedLayer = std::move(sourceLayer); + } + + std::unique_ptr Layer::releaseCoreLayer() { + assert(ownedLayer != nullptr); + return std::move(ownedLayer); + } + + jni::Local Layer::getId(jni::JNIEnv& env) { + return jni::Make(env, layer.getID()); + } + + style::Layer& Layer::get() { + return layer; + } + + void Layer::setLayoutProperty(jni::JNIEnv& env, const jni::String& jname, const jni::Object<>& jvalue) { + // Convert and set property + optional error = layer.setLayoutProperty(jni::Make(env, jname), Value(env, jvalue)); + if (error) { + mbgl::Log::Error(mbgl::Event::JNI, "Error setting property: " + jni::Make(env, jname) + " " + error->message); + return; + } + } + + void Layer::setPaintProperty(jni::JNIEnv& env, const jni::String& jname, const jni::Object<>& jvalue) { + // Convert and set property + optional error = layer.setPaintProperty(jni::Make(env, jname), Value(env, jvalue)); + if (error) { + mbgl::Log::Error(mbgl::Event::JNI, "Error setting property: " + jni::Make(env, jname) + " " + error->message); + return; + } + } + + void Layer::setFilter(jni::JNIEnv& env, const jni::Array>& jfilter) { + using namespace mbgl::style; + using namespace mbgl::style::conversion; + + Error error; + optional converted = convert(Value(env, jfilter), error); + if (!converted) { + mbgl::Log::Error(mbgl::Event::JNI, "Error setting filter: " + error.message); + return; + } + + layer.setFilter(std::move(*converted)); + } + + jni::Local> Layer::getFilter(jni::JNIEnv& env) { + using namespace mbgl::style; + using namespace mbgl::style::conversion; + + Filter filter = layer.getFilter(); + if (filter.expression) { + mbgl::Value expressionValue = (*filter.expression)->serialize(); + return gson::JsonElement::New(env, expressionValue); + } else { + return jni::Local>(env, nullptr); + } + } + + void Layer::setSourceLayer(jni::JNIEnv& env, const jni::String& sourceLayer) { + layer.setSourceLayer(jni::Make(env, sourceLayer)); + } + + jni::Local Layer::getSourceLayer(jni::JNIEnv& env) { + return jni::Make(env, layer.getSourceLayer()); + } + + jni::Local Layer::getSourceId(jni::JNIEnv& env) { + return jni::Make(env, layer.getSourceID()); + } + + jni::jfloat Layer::getMinZoom(jni::JNIEnv&){ + return layer.getMinZoom(); + } + + jni::jfloat Layer::getMaxZoom(jni::JNIEnv&) { + return layer.getMaxZoom(); + } + + void Layer::setMinZoom(jni::JNIEnv&, jni::jfloat zoom) { + layer.setMinZoom(zoom); + } + + void Layer::setMaxZoom(jni::JNIEnv&, jni::jfloat zoom) { + layer.setMaxZoom(zoom); + } + + jni::Local> Layer::getVisibility(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, layer.getVisibility())); + } + + void Layer::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer(env, javaClass, "nativePtr", + METHOD(&Layer::getId, "nativeGetId"), + METHOD(&Layer::setLayoutProperty, "nativeSetLayoutProperty"), + METHOD(&Layer::setPaintProperty, "nativeSetPaintProperty"), + METHOD(&Layer::setFilter, "nativeSetFilter"), + METHOD(&Layer::getFilter, "nativeGetFilter"), + METHOD(&Layer::setSourceLayer, "nativeSetSourceLayer"), + METHOD(&Layer::getSourceLayer, "nativeGetSourceLayer"), + METHOD(&Layer::getSourceId, "nativeGetSourceId"), + METHOD(&Layer::getMinZoom, "nativeGetMinZoom"), + METHOD(&Layer::getMaxZoom, "nativeGetMaxZoom"), + METHOD(&Layer::setMinZoom, "nativeSetMinZoom"), + METHOD(&Layer::setMaxZoom, "nativeSetMaxZoom"), + METHOD(&Layer::getVisibility, "nativeGetVisibility") + ); + + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/layer.cpp.ejs b/src/style/layers/layer.cpp.ejs new file mode 100644 index 000000000..c81fc5ab3 --- /dev/null +++ b/src/style/layers/layer.cpp.ejs @@ -0,0 +1,137 @@ +<% + const type = locals.type; + const properties = locals.properties; +-%> +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#include "<%- type.replace('-', '_') %>_layer.hpp" + +#include + +#include "../conversion/property_value.hpp" +#include "../conversion/transition_options.hpp" + +#include + +namespace mbgl { +namespace android { + + inline mbgl::style::<%- camelize(type) %>Layer& to<%- camelize(type) %>Layer(mbgl::style::Layer& layer) { + return static_castLayer&>(layer); + } + +<% if (type === 'background') { -%> + /** + * Creates an owning peer object (for layers not attached to the map) from the JVM side + */ + <%- camelize(type) %>Layer::<%- camelize(type) %>Layer(jni::JNIEnv& env, jni::String& layerId) + : Layer(std::make_uniqueLayer>(jni::Make(env, layerId))) { +<% } else { -%> + /** + * Creates an owning peer object (for layers not attached to the map) from the JVM side + */ + <%- camelize(type) %>Layer::<%- camelize(type) %>Layer(jni::JNIEnv& env, jni::String& layerId, jni::String& sourceId) + : Layer(std::make_uniqueLayer>(jni::Make(env, layerId), jni::Make(env, sourceId))) { +<% } -%> + } + + /** + * Creates a non-owning peer object (for layers currently attached to the map) + */ + <%- camelize(type) %>Layer::<%- camelize(type) %>Layer(mbgl::Map& map, mbgl::style::<%- camelize(type) %>Layer& coreLayer) + : Layer(map, coreLayer) { + } + + /** + * Creates an owning peer object (for layers not attached to the map) + */ + <%- camelize(type) %>Layer::<%- camelize(type) %>Layer(mbgl::Map& map, std::unique_ptrLayer> coreLayer) + : Layer(map, std::move(coreLayer)) { + } + + <%- camelize(type) %>Layer::~<%- camelize(type) %>Layer() = default; + + // Property getters + +<% for (const property of properties) { -%> +<% if (property.name != 'heatmap-color') { -%> + jni::Local> <%- camelize(type) %>Layer::get<%- camelize(property.name) %>(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, to<%- camelize(type) %>Layer(layer).get<%- camelize(property.name) %>())); + } + +<% } else { -%> + jni::Local> HeatmapLayer::getHeatmapColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + auto propertyValue = to<%- camelize(type) %>Layer(layer).getHeatmapColor(); + if (propertyValue.isUndefined()) { + propertyValue = to<%- camelize(type) %>Layer(layer).getDefaultHeatmapColor(); + } + return std::move(*convert>>(env, propertyValue)); + } + +<% } -%> +<% if (property.transition) { -%> + jni::Local> <%- camelize(type) %>Layer::get<%- camelize(property.name) %>Transition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = to<%- camelize(type) %>Layer(layer).get<%- camelize(property.name) %>Transition(); + return std::move(*convert>>(env, options)); + } + + void <%- camelize(type) %>Layer::set<%- camelize(property.name) %>Transition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + to<%- camelize(type) %>Layer(layer).set<%- camelize(property.name) %>Transition(options); + } + +<% } -%> +<% } -%> + + // <%- camelize(type) %>JavaLayerPeerFactory + + <%- camelize(type) %>JavaLayerPeerFactory::~<%- camelize(type) %>JavaLayerPeerFactory() = default; + + namespace { + jni::Local> createJavaPeer(jni::JNIEnv& env, Layer* layer) { + static auto& javaClass = jni::Class<<%- camelize(type) %>Layer>::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(layer)); + } + } // namespace + + jni::Local> <%- camelize(type) %>JavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, mbgl::style::Layer& layer) { + assert(layer.baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new <%- camelize(type) %>Layer(map, to<%- camelize(type) %>Layer(layer))); + } + + jni::Local> <%- camelize(type) %>JavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr layer) { + assert(layer->baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new <%- camelize(type) %>Layer(map, std::unique_ptrLayer>(static_castLayer*>(layer.release())))); + } + + void <%- camelize(type) %>JavaLayerPeerFactory::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class<<%- camelize(type) %>Layer>::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer<<%- camelize(type) %>Layer>( + env, javaClass, "nativePtr", +<% if (type === 'background') { -%> + jni::MakePeer<<%- camelize(type) %>Layer, jni::String&>, +<% } else { -%> + jni::MakePeer<<%- camelize(type) %>Layer, jni::String&, jni::String&>, +<% } -%> + "initialize", + "finalize",<% for(var i = 0; i < properties.length; i++) {%> +<% if (properties[i].transition) { -%> + METHOD(&<%- camelize(type) %>Layer::get<%- camelize(properties[i].name) %>Transition, "nativeGet<%- camelize(properties[i].name) %>Transition"), + METHOD(&<%- camelize(type) %>Layer::set<%- camelize(properties[i].name) %>Transition, "nativeSet<%- camelize(properties[i].name) %>Transition"), +<% } -%> + METHOD(&<%- camelize(type) %>Layer::get<%- camelize(properties[i].name) %>, "nativeGet<%- camelize(properties[i].name) %>")<% if(i != (properties.length -1)) {-%>,<% } -%><% } -%>); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/layer.hpp b/src/style/layers/layer.hpp new file mode 100644 index 000000000..0fb679152 --- /dev/null +++ b/src/style/layers/layer.hpp @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include +#include "../../gson/json_array.hpp" +#include "../value.hpp" +#include "../../gson/json_element.hpp" + +#include + +namespace mbgl { + +namespace android { + +class Layer { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/Layer"; }; + + static void registerNative(jni::JNIEnv&); + + virtual ~Layer(); + + /** + * Set core layer (ie return ownership after remove) + */ + void setLayer(std::unique_ptr); + + void addToMap(mbgl::Map&, mbgl::optional); + + jni::Local getId(jni::JNIEnv&); + + jni::Local getSourceId(jni::JNIEnv&); + + style::Layer& get(); + + void setLayoutProperty(jni::JNIEnv&, const jni::String&, const jni::Object<>& value); + + void setPaintProperty(jni::JNIEnv&, const jni::String&, const jni::Object<>& value); + + // Zoom + + jni::jfloat getMinZoom(jni::JNIEnv&); + + jni::jfloat getMaxZoom(jni::JNIEnv&); + + void setMinZoom(jni::JNIEnv&, jni::jfloat zoom); + + void setMaxZoom(jni::JNIEnv&, jni::jfloat zoom); + + /* common properties, but not shared by all */ + + void setFilter(jni::JNIEnv&, const jni::Array>&); + + jni::Local> getFilter(jni::JNIEnv&); + + void setSourceLayer(jni::JNIEnv&, const jni::String&); + + jni::Local getSourceLayer(jni::JNIEnv&); + + // Property getters + + jni::Local> getVisibility(jni::JNIEnv&); + +protected: + /* + * Called when a non-owning peer object is created on the c++ side + */ + Layer(mbgl::Map&, mbgl::style::Layer&); + + /* + * Called when a owning peer object is created on the c++ side + */ + Layer(mbgl::Map&, std::unique_ptr); + + /* + * Called when a Java object was created from the jvm side + */ + Layer(std::unique_ptr); + + // Release the owned view and return it + std::unique_ptr releaseCoreLayer(); + + // Owned layer is set when creating a new layer, before adding it to the map + std::unique_ptr ownedLayer; + + // Raw reference to the layer + mbgl::style::Layer& layer; + + // Map is set when the layer is retrieved or after adding to the map + mbgl::Map* map; +}; + +/** + * @brief A factory class for a layer Java peer objects of a certain type. + */ +class JavaLayerPeerFactory { +public: + virtual ~JavaLayerPeerFactory() = default; + /** + * @brief Create a non-owning peer. + */ + virtual jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&) = 0; + + /** + * @brief Create an owning peer. + */ + virtual jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr) = 0; + + /** + * @brief Register peer methods. + */ + virtual void registerNative(jni::JNIEnv&) = 0; + + /** + * @brief Get the corresponding layer factory. + * + * @return style::LayerFactory* must not be \c nullptr. + */ + virtual LayerFactory* getLayerFactory() = 0; +}; + + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/layer.hpp.ejs b/src/style/layers/layer.hpp.ejs new file mode 100644 index 000000000..28e00b273 --- /dev/null +++ b/src/style/layers/layer.hpp.ejs @@ -0,0 +1,62 @@ +<% + const type = locals.type; + const properties = locals.properties; +-%> +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#pragma once + +#include "layer.hpp" +#include "../transition_options.hpp" +#include _layer_factory.hpp> +#include _layer.hpp> +#include + +namespace mbgl { +namespace android { + +class <%- camelize(type) %>Layer : public Layer { +public: + using SuperTag = Layer; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/<%- camelize(type) %>Layer"; }; + +<% if (type === 'background') { -%> + <%- camelize(type) %>Layer(jni::JNIEnv&, jni::String&); +<% } else { -%> + <%- camelize(type) %>Layer(jni::JNIEnv&, jni::String&, jni::String&); +<% } -%> + + <%- camelize(type) %>Layer(mbgl::Map&, mbgl::style::<%- camelize(type) %>Layer&); + + <%- camelize(type) %>Layer(mbgl::Map&, std::unique_ptrLayer>); + + ~<%- camelize(type) %>Layer(); + + // Properties +<% for (const property of properties) { -%> + + jni::Local> get<%- camelize(property.name) %>(jni::JNIEnv&); +<% if (property.transition) { -%> + void set<%- camelize(property.name) %>Transition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> get<%- camelize(property.name) %>Transition(jni::JNIEnv&); +<% } -%> +<% } -%> + +}; // class <%- camelize(type) %>Layer + +class <%- camelize(type) %>JavaLayerPeerFactory final : public JavaLayerPeerFactory, public mbgl::<%- camelize(type) %>LayerFactory { +public: + ~<%- camelize(type) %>JavaLayerPeerFactory() override; + + // JavaLayerPeerFactory overrides. + jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&) final; + jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr) final; + + void registerNative(jni::JNIEnv&) final; + + LayerFactory* getLayerFactory() final { return this; } + +}; // class <%- camelize(type) %>JavaLayerPeerFactory + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/layer_manager.cpp b/src/style/layers/layer_manager.cpp new file mode 100644 index 000000000..d0c54ee00 --- /dev/null +++ b/src/style/layers/layer_manager.cpp @@ -0,0 +1,172 @@ +#include "layer_manager.hpp" + +#include + +#include +#include +#include + +#include "background_layer.hpp" +#include "circle_layer.hpp" +#include "custom_layer.hpp" +#include "fill_extrusion_layer.hpp" +#include "fill_layer.hpp" +#include "heatmap_layer.hpp" +#include "hillshade_layer.hpp" +#include "line_layer.hpp" +#include "raster_layer.hpp" +#include "symbol_layer.hpp" +#include "fill_extrusion_layer.hpp" + +namespace mbgl { + +namespace android { + +LayerManagerAndroid::LayerManagerAndroid() { +#if defined(MBGL_LAYER_FILL_DISABLE_RUNTIME) + addLayerTypeCoreOnly(std::make_unique()); +#elif !defined(MBGL_LAYER_FILL_DISABLE_ALL) + addLayerType(std::make_unique()); +#endif +#if defined(MBGL_LAYER_LINE_DISABLE_RUNTIME) + addLayerTypeCoreOnly(std::make_unique()); +#elif !defined(MBGL_LAYER_LINE_DISABLE_ALL) + addLayerType(std::make_unique()); +#endif +#if defined(MBGL_LAYER_CIRCLE_DISABLE_RUNTIME) + addLayerTypeCoreOnly(std::make_unique()); +#elif !defined(MBGL_LAYER_CIRCLE_DISABLE_ALL) + addLayerType(std::make_unique()); +#endif +#if defined(MBGL_LAYER_SYMBOL_DISABLE_RUNTIME) + addLayerTypeCoreOnly(std::make_unique()); +#elif !defined(MBGL_LAYER_SYMBOL_DISABLE_ALL) + addLayerType(std::make_unique()); +#endif +#if defined(MBGL_LAYER_RASTER_DISABLE_RUNTIME) + addLayerTypeCoreOnly(std::make_unique()); +#elif !defined(MBGL_LAYER_RASTER_DISABLE_ALL) + addLayerType(std::make_unique()); +#endif +#if defined(MBGL_LAYER_BACKGROUND_DISABLE_RUNTIME) + addLayerTypeCoreOnly(std::make_unique()); +#elif !defined(MBGL_LAYER_BACKGROUND_DISABLE_ALL) + addLayerType(std::make_unique()); +#endif +#if defined(MBGL_LAYER_HILLSHADE_DISABLE_RUNTIME) + addLayerTypeCoreOnly(std::make_unique()); +#elif !defined(MBGL_LAYER_HILLSHADE_DISABLE_ALL) + addLayerType(std::make_unique()); +#endif +#if defined(MBGL_LAYER_FILL_EXTRUSION_DISABLE_RUNTIME) + addLayerTypeCoreOnly(std::make_unique()); +#elif !defined(MBGL_LAYER_FILL_EXTRUSION_DISABLE_ALL) + addLayerType(std::make_unique()); +#endif +#if defined(MBGL_LAYER_HEATMAP_DISABLE_RUNTIME) + addLayerTypeCoreOnly(std::make_unique()); +#elif !defined(MBGL_LAYER_HEATMAP_DISABLE_ALL) + addLayerType(std::make_unique()); +#endif +#if defined(MBGL_LAYER_CUSTOM_DISABLE_RUNTIME) + addLayerTypeCoreOnly(std::make_unique()); +#elif !defined(MBGL_LAYER_CUSTOM_DISABLE_ALL) + addLayerType(std::make_unique()); +#endif +} + +LayerManagerAndroid::~LayerManagerAndroid() = default; + +jni::Local> LayerManagerAndroid::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, mbgl::style::Layer& layer) { + if (JavaLayerPeerFactory* factory = getPeerFactory(layer.getTypeInfo())) { + return factory->createJavaLayerPeer(env, map, layer); + } + return jni::Local>(); +} + +jni::Local> LayerManagerAndroid::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr layer) { + if (JavaLayerPeerFactory* factory = getPeerFactory(layer->getTypeInfo())) { + return factory->createJavaLayerPeer(env, map, std::move(layer)); + } + return jni::Local>(); +} + +void LayerManagerAndroid::registerNative(jni::JNIEnv& env) { + if (peerFactories.empty()) { + return; + } + + Layer::registerNative(env); + for (const auto& factory: peerFactories) { + factory->registerNative(env); + } +} + +void LayerManagerAndroid::addLayerType(std::unique_ptr factory) { + assert(getFactory(factory->getLayerFactory()->getTypeInfo()) == nullptr); + registerCoreFactory(factory->getLayerFactory()); + peerFactories.emplace_back(std::move(factory)); +} + +void LayerManagerAndroid::addLayerTypeCoreOnly(std::unique_ptr factory) { + assert(getFactory(factory->getTypeInfo()) == nullptr); + registerCoreFactory(factory.get()); + coreFactories.emplace_back(std::move(factory)); +} + +void LayerManagerAndroid::registerCoreFactory(mbgl::LayerFactory* factory) { + std::string type{factory->getTypeInfo()->type}; + if (!type.empty()) { + assert(typeToFactory.find(type) == typeToFactory.end()); + typeToFactory.emplace(std::make_pair(std::move(type), factory)); + } +} + +JavaLayerPeerFactory* LayerManagerAndroid::getPeerFactory(const mbgl::style::LayerTypeInfo* typeInfo) { + assert(typeInfo); + for (const auto& factory: peerFactories) { + if (factory->getLayerFactory()->getTypeInfo() == typeInfo) { + return factory.get(); + } + } + return nullptr; +} + +LayerFactory* LayerManagerAndroid::getFactory(const std::string& type) noexcept { + auto search = typeToFactory.find(type); + return (search != typeToFactory.end()) ? search->second : nullptr; +} + +LayerFactory* LayerManagerAndroid::getFactory(const mbgl::style::LayerTypeInfo* info) noexcept { + if (JavaLayerPeerFactory* peerFactory = getPeerFactory(info)) { + return peerFactory->getLayerFactory(); + } + + for (const auto& factory: coreFactories) { + if (factory->getTypeInfo() == info) { + return factory.get(); + } + } + + return nullptr; +} + +// static +LayerManagerAndroid* LayerManagerAndroid::get() noexcept { + static LayerManagerAndroid impl; + return &impl; +} + +} // namespace android + +LayerManager* LayerManager::get() noexcept { + return android::LayerManagerAndroid::get(); +} + +#if defined(MBGL_LAYER_LINE_DISABLE_ALL) || defined(MBGL_LAYER_SYMBOL_DISABLE_ALL) || defined(MBGL_LAYER_FILL_DISABLE_ALL) +const bool LayerManager::annotationsEnabled = false; +#else +const bool LayerManager::annotationsEnabled = true; +#endif + +} // namespace mbgl diff --git a/src/style/layers/layer_manager.hpp b/src/style/layers/layer_manager.hpp new file mode 100644 index 000000000..807ded173 --- /dev/null +++ b/src/style/layers/layer_manager.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +#include "layer.hpp" + +#include + +#include +#include + +namespace mbgl { +namespace android { + +/** + * @brief A singleton class forwarding calls to the corresponding \c JavaLayerPeerFactory instance. + */ +class LayerManagerAndroid final : public mbgl::LayerManager { +public: + ~LayerManagerAndroid() final; + static LayerManagerAndroid* get() noexcept; + + jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&); + jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr); + + void registerNative(jni::JNIEnv&); + +private: + LayerManagerAndroid(); + /** + * @brief Enables a layer type for both JSON style and runtime API. + */ + void addLayerType(std::unique_ptr); + /** + * @brief Enables a layer type for JSON style only. + * + * We might not want to expose runtime API for some layer types + * in order to save binary size - JNI glue code for these layer types + * won't be added to the binary. + */ + void addLayerTypeCoreOnly(std::unique_ptr); + + void registerCoreFactory(mbgl::LayerFactory*); + JavaLayerPeerFactory* getPeerFactory(const mbgl::style::LayerTypeInfo*); + // mbgl::LayerManager overrides. + LayerFactory* getFactory(const std::string& type) noexcept final; + LayerFactory* getFactory(const mbgl::style::LayerTypeInfo* info) noexcept final; + + std::vector> peerFactories; + std::vector> coreFactories; + std::map typeToFactory; +}; + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/line_layer.cpp b/src/style/layers/line_layer.cpp new file mode 100644 index 000000000..73fcd5f99 --- /dev/null +++ b/src/style/layers/line_layer.cpp @@ -0,0 +1,307 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#include "line_layer.hpp" + +#include + +#include "../conversion/property_value.hpp" +#include "../conversion/transition_options.hpp" + +#include + +namespace mbgl { +namespace android { + + inline mbgl::style::LineLayer& toLineLayer(mbgl::style::Layer& layer) { + return static_cast(layer); + } + + /** + * Creates an owning peer object (for layers not attached to the map) from the JVM side + */ + LineLayer::LineLayer(jni::JNIEnv& env, jni::String& layerId, jni::String& sourceId) + : Layer(std::make_unique(jni::Make(env, layerId), jni::Make(env, sourceId))) { + } + + /** + * Creates a non-owning peer object (for layers currently attached to the map) + */ + LineLayer::LineLayer(mbgl::Map& map, mbgl::style::LineLayer& coreLayer) + : Layer(map, coreLayer) { + } + + /** + * Creates an owning peer object (for layers not attached to the map) + */ + LineLayer::LineLayer(mbgl::Map& map, std::unique_ptr coreLayer) + : Layer(map, std::move(coreLayer)) { + } + + LineLayer::~LineLayer() = default; + + // Property getters + + jni::Local> LineLayer::getLineCap(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineCap())); + } + + jni::Local> LineLayer::getLineJoin(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineJoin())); + } + + jni::Local> LineLayer::getLineMiterLimit(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineMiterLimit())); + } + + jni::Local> LineLayer::getLineRoundLimit(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineRoundLimit())); + } + + jni::Local> LineLayer::getLineOpacity(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineOpacity())); + } + + jni::Local> LineLayer::getLineOpacityTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toLineLayer(layer).getLineOpacityTransition(); + return std::move(*convert>>(env, options)); + } + + void LineLayer::setLineOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toLineLayer(layer).setLineOpacityTransition(options); + } + + jni::Local> LineLayer::getLineColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineColor())); + } + + jni::Local> LineLayer::getLineColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toLineLayer(layer).getLineColorTransition(); + return std::move(*convert>>(env, options)); + } + + void LineLayer::setLineColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toLineLayer(layer).setLineColorTransition(options); + } + + jni::Local> LineLayer::getLineTranslate(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineTranslate())); + } + + jni::Local> LineLayer::getLineTranslateTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toLineLayer(layer).getLineTranslateTransition(); + return std::move(*convert>>(env, options)); + } + + void LineLayer::setLineTranslateTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toLineLayer(layer).setLineTranslateTransition(options); + } + + jni::Local> LineLayer::getLineTranslateAnchor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineTranslateAnchor())); + } + + jni::Local> LineLayer::getLineWidth(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineWidth())); + } + + jni::Local> LineLayer::getLineWidthTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toLineLayer(layer).getLineWidthTransition(); + return std::move(*convert>>(env, options)); + } + + void LineLayer::setLineWidthTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toLineLayer(layer).setLineWidthTransition(options); + } + + jni::Local> LineLayer::getLineGapWidth(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineGapWidth())); + } + + jni::Local> LineLayer::getLineGapWidthTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toLineLayer(layer).getLineGapWidthTransition(); + return std::move(*convert>>(env, options)); + } + + void LineLayer::setLineGapWidthTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toLineLayer(layer).setLineGapWidthTransition(options); + } + + jni::Local> LineLayer::getLineOffset(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineOffset())); + } + + jni::Local> LineLayer::getLineOffsetTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toLineLayer(layer).getLineOffsetTransition(); + return std::move(*convert>>(env, options)); + } + + void LineLayer::setLineOffsetTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toLineLayer(layer).setLineOffsetTransition(options); + } + + jni::Local> LineLayer::getLineBlur(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineBlur())); + } + + jni::Local> LineLayer::getLineBlurTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toLineLayer(layer).getLineBlurTransition(); + return std::move(*convert>>(env, options)); + } + + void LineLayer::setLineBlurTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toLineLayer(layer).setLineBlurTransition(options); + } + + jni::Local> LineLayer::getLineDasharray(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineDasharray())); + } + + jni::Local> LineLayer::getLineDasharrayTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toLineLayer(layer).getLineDasharrayTransition(); + return std::move(*convert>>(env, options)); + } + + void LineLayer::setLineDasharrayTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toLineLayer(layer).setLineDasharrayTransition(options); + } + + jni::Local> LineLayer::getLinePattern(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLinePattern())); + } + + jni::Local> LineLayer::getLinePatternTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toLineLayer(layer).getLinePatternTransition(); + return std::move(*convert>>(env, options)); + } + + void LineLayer::setLinePatternTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toLineLayer(layer).setLinePatternTransition(options); + } + + jni::Local> LineLayer::getLineGradient(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toLineLayer(layer).getLineGradient())); + } + + + // LineJavaLayerPeerFactory + + LineJavaLayerPeerFactory::~LineJavaLayerPeerFactory() = default; + + namespace { + jni::Local> createJavaPeer(jni::JNIEnv& env, Layer* layer) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(layer)); + } + } // namespace + + jni::Local> LineJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, mbgl::style::Layer& layer) { + assert(layer.baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new LineLayer(map, toLineLayer(layer))); + } + + jni::Local> LineJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr layer) { + assert(layer->baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new LineLayer(map, std::unique_ptr(static_cast(layer.release())))); + } + + void LineJavaLayerPeerFactory::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer, + "initialize", + "finalize", + METHOD(&LineLayer::getLineCap, "nativeGetLineCap"), + METHOD(&LineLayer::getLineJoin, "nativeGetLineJoin"), + METHOD(&LineLayer::getLineMiterLimit, "nativeGetLineMiterLimit"), + METHOD(&LineLayer::getLineRoundLimit, "nativeGetLineRoundLimit"), + METHOD(&LineLayer::getLineOpacityTransition, "nativeGetLineOpacityTransition"), + METHOD(&LineLayer::setLineOpacityTransition, "nativeSetLineOpacityTransition"), + METHOD(&LineLayer::getLineOpacity, "nativeGetLineOpacity"), + METHOD(&LineLayer::getLineColorTransition, "nativeGetLineColorTransition"), + METHOD(&LineLayer::setLineColorTransition, "nativeSetLineColorTransition"), + METHOD(&LineLayer::getLineColor, "nativeGetLineColor"), + METHOD(&LineLayer::getLineTranslateTransition, "nativeGetLineTranslateTransition"), + METHOD(&LineLayer::setLineTranslateTransition, "nativeSetLineTranslateTransition"), + METHOD(&LineLayer::getLineTranslate, "nativeGetLineTranslate"), + METHOD(&LineLayer::getLineTranslateAnchor, "nativeGetLineTranslateAnchor"), + METHOD(&LineLayer::getLineWidthTransition, "nativeGetLineWidthTransition"), + METHOD(&LineLayer::setLineWidthTransition, "nativeSetLineWidthTransition"), + METHOD(&LineLayer::getLineWidth, "nativeGetLineWidth"), + METHOD(&LineLayer::getLineGapWidthTransition, "nativeGetLineGapWidthTransition"), + METHOD(&LineLayer::setLineGapWidthTransition, "nativeSetLineGapWidthTransition"), + METHOD(&LineLayer::getLineGapWidth, "nativeGetLineGapWidth"), + METHOD(&LineLayer::getLineOffsetTransition, "nativeGetLineOffsetTransition"), + METHOD(&LineLayer::setLineOffsetTransition, "nativeSetLineOffsetTransition"), + METHOD(&LineLayer::getLineOffset, "nativeGetLineOffset"), + METHOD(&LineLayer::getLineBlurTransition, "nativeGetLineBlurTransition"), + METHOD(&LineLayer::setLineBlurTransition, "nativeSetLineBlurTransition"), + METHOD(&LineLayer::getLineBlur, "nativeGetLineBlur"), + METHOD(&LineLayer::getLineDasharrayTransition, "nativeGetLineDasharrayTransition"), + METHOD(&LineLayer::setLineDasharrayTransition, "nativeSetLineDasharrayTransition"), + METHOD(&LineLayer::getLineDasharray, "nativeGetLineDasharray"), + METHOD(&LineLayer::getLinePatternTransition, "nativeGetLinePatternTransition"), + METHOD(&LineLayer::setLinePatternTransition, "nativeSetLinePatternTransition"), + METHOD(&LineLayer::getLinePattern, "nativeGetLinePattern"), + METHOD(&LineLayer::getLineGradient, "nativeGetLineGradient")); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/line_layer.hpp b/src/style/layers/line_layer.hpp new file mode 100644 index 000000000..75a367ab7 --- /dev/null +++ b/src/style/layers/line_layer.hpp @@ -0,0 +1,94 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#pragma once + +#include "layer.hpp" +#include "../transition_options.hpp" +#include +#include +#include + +namespace mbgl { +namespace android { + +class LineLayer : public Layer { +public: + using SuperTag = Layer; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/LineLayer"; }; + + LineLayer(jni::JNIEnv&, jni::String&, jni::String&); + + LineLayer(mbgl::Map&, mbgl::style::LineLayer&); + + LineLayer(mbgl::Map&, std::unique_ptr); + + ~LineLayer(); + + // Properties + + jni::Local> getLineCap(jni::JNIEnv&); + + jni::Local> getLineJoin(jni::JNIEnv&); + + jni::Local> getLineMiterLimit(jni::JNIEnv&); + + jni::Local> getLineRoundLimit(jni::JNIEnv&); + + jni::Local> getLineOpacity(jni::JNIEnv&); + void setLineOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getLineOpacityTransition(jni::JNIEnv&); + + jni::Local> getLineColor(jni::JNIEnv&); + void setLineColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getLineColorTransition(jni::JNIEnv&); + + jni::Local> getLineTranslate(jni::JNIEnv&); + void setLineTranslateTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getLineTranslateTransition(jni::JNIEnv&); + + jni::Local> getLineTranslateAnchor(jni::JNIEnv&); + + jni::Local> getLineWidth(jni::JNIEnv&); + void setLineWidthTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getLineWidthTransition(jni::JNIEnv&); + + jni::Local> getLineGapWidth(jni::JNIEnv&); + void setLineGapWidthTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getLineGapWidthTransition(jni::JNIEnv&); + + jni::Local> getLineOffset(jni::JNIEnv&); + void setLineOffsetTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getLineOffsetTransition(jni::JNIEnv&); + + jni::Local> getLineBlur(jni::JNIEnv&); + void setLineBlurTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getLineBlurTransition(jni::JNIEnv&); + + jni::Local> getLineDasharray(jni::JNIEnv&); + void setLineDasharrayTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getLineDasharrayTransition(jni::JNIEnv&); + + jni::Local> getLinePattern(jni::JNIEnv&); + void setLinePatternTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getLinePatternTransition(jni::JNIEnv&); + + jni::Local> getLineGradient(jni::JNIEnv&); + +}; // class LineLayer + +class LineJavaLayerPeerFactory final : public JavaLayerPeerFactory, public mbgl::LineLayerFactory { +public: + ~LineJavaLayerPeerFactory() override; + + // JavaLayerPeerFactory overrides. + jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&) final; + jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr) final; + + void registerNative(jni::JNIEnv&) final; + + LayerFactory* getLayerFactory() final { return this; } + +}; // class LineJavaLayerPeerFactory + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/raster_layer.cpp b/src/style/layers/raster_layer.cpp new file mode 100644 index 000000000..b12267525 --- /dev/null +++ b/src/style/layers/raster_layer.cpp @@ -0,0 +1,220 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#include "raster_layer.hpp" + +#include + +#include "../conversion/property_value.hpp" +#include "../conversion/transition_options.hpp" + +#include + +namespace mbgl { +namespace android { + + inline mbgl::style::RasterLayer& toRasterLayer(mbgl::style::Layer& layer) { + return static_cast(layer); + } + + /** + * Creates an owning peer object (for layers not attached to the map) from the JVM side + */ + RasterLayer::RasterLayer(jni::JNIEnv& env, jni::String& layerId, jni::String& sourceId) + : Layer(std::make_unique(jni::Make(env, layerId), jni::Make(env, sourceId))) { + } + + /** + * Creates a non-owning peer object (for layers currently attached to the map) + */ + RasterLayer::RasterLayer(mbgl::Map& map, mbgl::style::RasterLayer& coreLayer) + : Layer(map, coreLayer) { + } + + /** + * Creates an owning peer object (for layers not attached to the map) + */ + RasterLayer::RasterLayer(mbgl::Map& map, std::unique_ptr coreLayer) + : Layer(map, std::move(coreLayer)) { + } + + RasterLayer::~RasterLayer() = default; + + // Property getters + + jni::Local> RasterLayer::getRasterOpacity(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toRasterLayer(layer).getRasterOpacity())); + } + + jni::Local> RasterLayer::getRasterOpacityTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toRasterLayer(layer).getRasterOpacityTransition(); + return std::move(*convert>>(env, options)); + } + + void RasterLayer::setRasterOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toRasterLayer(layer).setRasterOpacityTransition(options); + } + + jni::Local> RasterLayer::getRasterHueRotate(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toRasterLayer(layer).getRasterHueRotate())); + } + + jni::Local> RasterLayer::getRasterHueRotateTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toRasterLayer(layer).getRasterHueRotateTransition(); + return std::move(*convert>>(env, options)); + } + + void RasterLayer::setRasterHueRotateTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toRasterLayer(layer).setRasterHueRotateTransition(options); + } + + jni::Local> RasterLayer::getRasterBrightnessMin(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toRasterLayer(layer).getRasterBrightnessMin())); + } + + jni::Local> RasterLayer::getRasterBrightnessMinTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toRasterLayer(layer).getRasterBrightnessMinTransition(); + return std::move(*convert>>(env, options)); + } + + void RasterLayer::setRasterBrightnessMinTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toRasterLayer(layer).setRasterBrightnessMinTransition(options); + } + + jni::Local> RasterLayer::getRasterBrightnessMax(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toRasterLayer(layer).getRasterBrightnessMax())); + } + + jni::Local> RasterLayer::getRasterBrightnessMaxTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toRasterLayer(layer).getRasterBrightnessMaxTransition(); + return std::move(*convert>>(env, options)); + } + + void RasterLayer::setRasterBrightnessMaxTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toRasterLayer(layer).setRasterBrightnessMaxTransition(options); + } + + jni::Local> RasterLayer::getRasterSaturation(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toRasterLayer(layer).getRasterSaturation())); + } + + jni::Local> RasterLayer::getRasterSaturationTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toRasterLayer(layer).getRasterSaturationTransition(); + return std::move(*convert>>(env, options)); + } + + void RasterLayer::setRasterSaturationTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toRasterLayer(layer).setRasterSaturationTransition(options); + } + + jni::Local> RasterLayer::getRasterContrast(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toRasterLayer(layer).getRasterContrast())); + } + + jni::Local> RasterLayer::getRasterContrastTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toRasterLayer(layer).getRasterContrastTransition(); + return std::move(*convert>>(env, options)); + } + + void RasterLayer::setRasterContrastTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toRasterLayer(layer).setRasterContrastTransition(options); + } + + jni::Local> RasterLayer::getRasterResampling(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toRasterLayer(layer).getRasterResampling())); + } + + jni::Local> RasterLayer::getRasterFadeDuration(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toRasterLayer(layer).getRasterFadeDuration())); + } + + + // RasterJavaLayerPeerFactory + + RasterJavaLayerPeerFactory::~RasterJavaLayerPeerFactory() = default; + + namespace { + jni::Local> createJavaPeer(jni::JNIEnv& env, Layer* layer) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(layer)); + } + } // namespace + + jni::Local> RasterJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, mbgl::style::Layer& layer) { + assert(layer.baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new RasterLayer(map, toRasterLayer(layer))); + } + + jni::Local> RasterJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr layer) { + assert(layer->baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new RasterLayer(map, std::unique_ptr(static_cast(layer.release())))); + } + + void RasterJavaLayerPeerFactory::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer, + "initialize", + "finalize", + METHOD(&RasterLayer::getRasterOpacityTransition, "nativeGetRasterOpacityTransition"), + METHOD(&RasterLayer::setRasterOpacityTransition, "nativeSetRasterOpacityTransition"), + METHOD(&RasterLayer::getRasterOpacity, "nativeGetRasterOpacity"), + METHOD(&RasterLayer::getRasterHueRotateTransition, "nativeGetRasterHueRotateTransition"), + METHOD(&RasterLayer::setRasterHueRotateTransition, "nativeSetRasterHueRotateTransition"), + METHOD(&RasterLayer::getRasterHueRotate, "nativeGetRasterHueRotate"), + METHOD(&RasterLayer::getRasterBrightnessMinTransition, "nativeGetRasterBrightnessMinTransition"), + METHOD(&RasterLayer::setRasterBrightnessMinTransition, "nativeSetRasterBrightnessMinTransition"), + METHOD(&RasterLayer::getRasterBrightnessMin, "nativeGetRasterBrightnessMin"), + METHOD(&RasterLayer::getRasterBrightnessMaxTransition, "nativeGetRasterBrightnessMaxTransition"), + METHOD(&RasterLayer::setRasterBrightnessMaxTransition, "nativeSetRasterBrightnessMaxTransition"), + METHOD(&RasterLayer::getRasterBrightnessMax, "nativeGetRasterBrightnessMax"), + METHOD(&RasterLayer::getRasterSaturationTransition, "nativeGetRasterSaturationTransition"), + METHOD(&RasterLayer::setRasterSaturationTransition, "nativeSetRasterSaturationTransition"), + METHOD(&RasterLayer::getRasterSaturation, "nativeGetRasterSaturation"), + METHOD(&RasterLayer::getRasterContrastTransition, "nativeGetRasterContrastTransition"), + METHOD(&RasterLayer::setRasterContrastTransition, "nativeSetRasterContrastTransition"), + METHOD(&RasterLayer::getRasterContrast, "nativeGetRasterContrast"), + METHOD(&RasterLayer::getRasterResampling, "nativeGetRasterResampling"), + METHOD(&RasterLayer::getRasterFadeDuration, "nativeGetRasterFadeDuration")); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/raster_layer.hpp b/src/style/layers/raster_layer.hpp new file mode 100644 index 000000000..53bee85e2 --- /dev/null +++ b/src/style/layers/raster_layer.hpp @@ -0,0 +1,74 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#pragma once + +#include "layer.hpp" +#include "../transition_options.hpp" +#include +#include +#include + +namespace mbgl { +namespace android { + +class RasterLayer : public Layer { +public: + using SuperTag = Layer; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/RasterLayer"; }; + + RasterLayer(jni::JNIEnv&, jni::String&, jni::String&); + + RasterLayer(mbgl::Map&, mbgl::style::RasterLayer&); + + RasterLayer(mbgl::Map&, std::unique_ptr); + + ~RasterLayer(); + + // Properties + + jni::Local> getRasterOpacity(jni::JNIEnv&); + void setRasterOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getRasterOpacityTransition(jni::JNIEnv&); + + jni::Local> getRasterHueRotate(jni::JNIEnv&); + void setRasterHueRotateTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getRasterHueRotateTransition(jni::JNIEnv&); + + jni::Local> getRasterBrightnessMin(jni::JNIEnv&); + void setRasterBrightnessMinTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getRasterBrightnessMinTransition(jni::JNIEnv&); + + jni::Local> getRasterBrightnessMax(jni::JNIEnv&); + void setRasterBrightnessMaxTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getRasterBrightnessMaxTransition(jni::JNIEnv&); + + jni::Local> getRasterSaturation(jni::JNIEnv&); + void setRasterSaturationTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getRasterSaturationTransition(jni::JNIEnv&); + + jni::Local> getRasterContrast(jni::JNIEnv&); + void setRasterContrastTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getRasterContrastTransition(jni::JNIEnv&); + + jni::Local> getRasterResampling(jni::JNIEnv&); + + jni::Local> getRasterFadeDuration(jni::JNIEnv&); + +}; // class RasterLayer + +class RasterJavaLayerPeerFactory final : public JavaLayerPeerFactory, public mbgl::RasterLayerFactory { +public: + ~RasterJavaLayerPeerFactory() override; + + // JavaLayerPeerFactory overrides. + jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&) final; + jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr) final; + + void registerNative(jni::JNIEnv&) final; + + LayerFactory* getLayerFactory() final { return this; } + +}; // class RasterJavaLayerPeerFactory + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/symbol_layer.cpp b/src/style/layers/symbol_layer.cpp new file mode 100644 index 000000000..53b35a9a1 --- /dev/null +++ b/src/style/layers/symbol_layer.cpp @@ -0,0 +1,592 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#include "symbol_layer.hpp" + +#include + +#include "../conversion/property_value.hpp" +#include "../conversion/transition_options.hpp" + +#include + +namespace mbgl { +namespace android { + + inline mbgl::style::SymbolLayer& toSymbolLayer(mbgl::style::Layer& layer) { + return static_cast(layer); + } + + /** + * Creates an owning peer object (for layers not attached to the map) from the JVM side + */ + SymbolLayer::SymbolLayer(jni::JNIEnv& env, jni::String& layerId, jni::String& sourceId) + : Layer(std::make_unique(jni::Make(env, layerId), jni::Make(env, sourceId))) { + } + + /** + * Creates a non-owning peer object (for layers currently attached to the map) + */ + SymbolLayer::SymbolLayer(mbgl::Map& map, mbgl::style::SymbolLayer& coreLayer) + : Layer(map, coreLayer) { + } + + /** + * Creates an owning peer object (for layers not attached to the map) + */ + SymbolLayer::SymbolLayer(mbgl::Map& map, std::unique_ptr coreLayer) + : Layer(map, std::move(coreLayer)) { + } + + SymbolLayer::~SymbolLayer() = default; + + // Property getters + + jni::Local> SymbolLayer::getSymbolPlacement(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getSymbolPlacement())); + } + + jni::Local> SymbolLayer::getSymbolSpacing(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getSymbolSpacing())); + } + + jni::Local> SymbolLayer::getSymbolAvoidEdges(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getSymbolAvoidEdges())); + } + + jni::Local> SymbolLayer::getSymbolSortKey(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getSymbolSortKey())); + } + + jni::Local> SymbolLayer::getSymbolZOrder(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getSymbolZOrder())); + } + + jni::Local> SymbolLayer::getIconAllowOverlap(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconAllowOverlap())); + } + + jni::Local> SymbolLayer::getIconIgnorePlacement(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconIgnorePlacement())); + } + + jni::Local> SymbolLayer::getIconOptional(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconOptional())); + } + + jni::Local> SymbolLayer::getIconRotationAlignment(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconRotationAlignment())); + } + + jni::Local> SymbolLayer::getIconSize(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconSize())); + } + + jni::Local> SymbolLayer::getIconTextFit(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconTextFit())); + } + + jni::Local> SymbolLayer::getIconTextFitPadding(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconTextFitPadding())); + } + + jni::Local> SymbolLayer::getIconImage(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconImage())); + } + + jni::Local> SymbolLayer::getIconRotate(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconRotate())); + } + + jni::Local> SymbolLayer::getIconPadding(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconPadding())); + } + + jni::Local> SymbolLayer::getIconKeepUpright(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconKeepUpright())); + } + + jni::Local> SymbolLayer::getIconOffset(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconOffset())); + } + + jni::Local> SymbolLayer::getIconAnchor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconAnchor())); + } + + jni::Local> SymbolLayer::getIconPitchAlignment(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconPitchAlignment())); + } + + jni::Local> SymbolLayer::getTextPitchAlignment(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextPitchAlignment())); + } + + jni::Local> SymbolLayer::getTextRotationAlignment(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextRotationAlignment())); + } + + jni::Local> SymbolLayer::getTextField(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextField())); + } + + jni::Local> SymbolLayer::getTextFont(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextFont())); + } + + jni::Local> SymbolLayer::getTextSize(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextSize())); + } + + jni::Local> SymbolLayer::getTextMaxWidth(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextMaxWidth())); + } + + jni::Local> SymbolLayer::getTextLineHeight(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextLineHeight())); + } + + jni::Local> SymbolLayer::getTextLetterSpacing(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextLetterSpacing())); + } + + jni::Local> SymbolLayer::getTextJustify(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextJustify())); + } + + jni::Local> SymbolLayer::getTextRadialOffset(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextRadialOffset())); + } + + jni::Local> SymbolLayer::getTextVariableAnchor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextVariableAnchor())); + } + + jni::Local> SymbolLayer::getTextAnchor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextAnchor())); + } + + jni::Local> SymbolLayer::getTextMaxAngle(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextMaxAngle())); + } + + jni::Local> SymbolLayer::getTextWritingMode(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextWritingMode())); + } + + jni::Local> SymbolLayer::getTextRotate(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextRotate())); + } + + jni::Local> SymbolLayer::getTextPadding(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextPadding())); + } + + jni::Local> SymbolLayer::getTextKeepUpright(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextKeepUpright())); + } + + jni::Local> SymbolLayer::getTextTransform(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextTransform())); + } + + jni::Local> SymbolLayer::getTextOffset(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextOffset())); + } + + jni::Local> SymbolLayer::getTextAllowOverlap(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextAllowOverlap())); + } + + jni::Local> SymbolLayer::getTextIgnorePlacement(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextIgnorePlacement())); + } + + jni::Local> SymbolLayer::getTextOptional(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextOptional())); + } + + jni::Local> SymbolLayer::getIconOpacity(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconOpacity())); + } + + jni::Local> SymbolLayer::getIconOpacityTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toSymbolLayer(layer).getIconOpacityTransition(); + return std::move(*convert>>(env, options)); + } + + void SymbolLayer::setIconOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toSymbolLayer(layer).setIconOpacityTransition(options); + } + + jni::Local> SymbolLayer::getIconColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconColor())); + } + + jni::Local> SymbolLayer::getIconColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toSymbolLayer(layer).getIconColorTransition(); + return std::move(*convert>>(env, options)); + } + + void SymbolLayer::setIconColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toSymbolLayer(layer).setIconColorTransition(options); + } + + jni::Local> SymbolLayer::getIconHaloColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconHaloColor())); + } + + jni::Local> SymbolLayer::getIconHaloColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toSymbolLayer(layer).getIconHaloColorTransition(); + return std::move(*convert>>(env, options)); + } + + void SymbolLayer::setIconHaloColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toSymbolLayer(layer).setIconHaloColorTransition(options); + } + + jni::Local> SymbolLayer::getIconHaloWidth(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconHaloWidth())); + } + + jni::Local> SymbolLayer::getIconHaloWidthTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toSymbolLayer(layer).getIconHaloWidthTransition(); + return std::move(*convert>>(env, options)); + } + + void SymbolLayer::setIconHaloWidthTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toSymbolLayer(layer).setIconHaloWidthTransition(options); + } + + jni::Local> SymbolLayer::getIconHaloBlur(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconHaloBlur())); + } + + jni::Local> SymbolLayer::getIconHaloBlurTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toSymbolLayer(layer).getIconHaloBlurTransition(); + return std::move(*convert>>(env, options)); + } + + void SymbolLayer::setIconHaloBlurTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toSymbolLayer(layer).setIconHaloBlurTransition(options); + } + + jni::Local> SymbolLayer::getIconTranslate(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconTranslate())); + } + + jni::Local> SymbolLayer::getIconTranslateTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toSymbolLayer(layer).getIconTranslateTransition(); + return std::move(*convert>>(env, options)); + } + + void SymbolLayer::setIconTranslateTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toSymbolLayer(layer).setIconTranslateTransition(options); + } + + jni::Local> SymbolLayer::getIconTranslateAnchor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getIconTranslateAnchor())); + } + + jni::Local> SymbolLayer::getTextOpacity(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextOpacity())); + } + + jni::Local> SymbolLayer::getTextOpacityTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toSymbolLayer(layer).getTextOpacityTransition(); + return std::move(*convert>>(env, options)); + } + + void SymbolLayer::setTextOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toSymbolLayer(layer).setTextOpacityTransition(options); + } + + jni::Local> SymbolLayer::getTextColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextColor())); + } + + jni::Local> SymbolLayer::getTextColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toSymbolLayer(layer).getTextColorTransition(); + return std::move(*convert>>(env, options)); + } + + void SymbolLayer::setTextColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toSymbolLayer(layer).setTextColorTransition(options); + } + + jni::Local> SymbolLayer::getTextHaloColor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextHaloColor())); + } + + jni::Local> SymbolLayer::getTextHaloColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toSymbolLayer(layer).getTextHaloColorTransition(); + return std::move(*convert>>(env, options)); + } + + void SymbolLayer::setTextHaloColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toSymbolLayer(layer).setTextHaloColorTransition(options); + } + + jni::Local> SymbolLayer::getTextHaloWidth(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextHaloWidth())); + } + + jni::Local> SymbolLayer::getTextHaloWidthTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toSymbolLayer(layer).getTextHaloWidthTransition(); + return std::move(*convert>>(env, options)); + } + + void SymbolLayer::setTextHaloWidthTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toSymbolLayer(layer).setTextHaloWidthTransition(options); + } + + jni::Local> SymbolLayer::getTextHaloBlur(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextHaloBlur())); + } + + jni::Local> SymbolLayer::getTextHaloBlurTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toSymbolLayer(layer).getTextHaloBlurTransition(); + return std::move(*convert>>(env, options)); + } + + void SymbolLayer::setTextHaloBlurTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toSymbolLayer(layer).setTextHaloBlurTransition(options); + } + + jni::Local> SymbolLayer::getTextTranslate(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextTranslate())); + } + + jni::Local> SymbolLayer::getTextTranslateTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = toSymbolLayer(layer).getTextTranslateTransition(); + return std::move(*convert>>(env, options)); + } + + void SymbolLayer::setTextTranslateTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + toSymbolLayer(layer).setTextTranslateTransition(options); + } + + jni::Local> SymbolLayer::getTextTranslateAnchor(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + return std::move(*convert>>(env, toSymbolLayer(layer).getTextTranslateAnchor())); + } + + + // SymbolJavaLayerPeerFactory + + SymbolJavaLayerPeerFactory::~SymbolJavaLayerPeerFactory() = default; + + namespace { + jni::Local> createJavaPeer(jni::JNIEnv& env, Layer* layer) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(layer)); + } + } // namespace + + jni::Local> SymbolJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, mbgl::style::Layer& layer) { + assert(layer.baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new SymbolLayer(map, toSymbolLayer(layer))); + } + + jni::Local> SymbolJavaLayerPeerFactory::createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr layer) { + assert(layer->baseImpl->getTypeInfo() == getTypeInfo()); + return createJavaPeer(env, new SymbolLayer(map, std::unique_ptr(static_cast(layer.release())))); + } + + void SymbolJavaLayerPeerFactory::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer, + "initialize", + "finalize", + METHOD(&SymbolLayer::getSymbolPlacement, "nativeGetSymbolPlacement"), + METHOD(&SymbolLayer::getSymbolSpacing, "nativeGetSymbolSpacing"), + METHOD(&SymbolLayer::getSymbolAvoidEdges, "nativeGetSymbolAvoidEdges"), + METHOD(&SymbolLayer::getSymbolSortKey, "nativeGetSymbolSortKey"), + METHOD(&SymbolLayer::getSymbolZOrder, "nativeGetSymbolZOrder"), + METHOD(&SymbolLayer::getIconAllowOverlap, "nativeGetIconAllowOverlap"), + METHOD(&SymbolLayer::getIconIgnorePlacement, "nativeGetIconIgnorePlacement"), + METHOD(&SymbolLayer::getIconOptional, "nativeGetIconOptional"), + METHOD(&SymbolLayer::getIconRotationAlignment, "nativeGetIconRotationAlignment"), + METHOD(&SymbolLayer::getIconSize, "nativeGetIconSize"), + METHOD(&SymbolLayer::getIconTextFit, "nativeGetIconTextFit"), + METHOD(&SymbolLayer::getIconTextFitPadding, "nativeGetIconTextFitPadding"), + METHOD(&SymbolLayer::getIconImage, "nativeGetIconImage"), + METHOD(&SymbolLayer::getIconRotate, "nativeGetIconRotate"), + METHOD(&SymbolLayer::getIconPadding, "nativeGetIconPadding"), + METHOD(&SymbolLayer::getIconKeepUpright, "nativeGetIconKeepUpright"), + METHOD(&SymbolLayer::getIconOffset, "nativeGetIconOffset"), + METHOD(&SymbolLayer::getIconAnchor, "nativeGetIconAnchor"), + METHOD(&SymbolLayer::getIconPitchAlignment, "nativeGetIconPitchAlignment"), + METHOD(&SymbolLayer::getTextPitchAlignment, "nativeGetTextPitchAlignment"), + METHOD(&SymbolLayer::getTextRotationAlignment, "nativeGetTextRotationAlignment"), + METHOD(&SymbolLayer::getTextField, "nativeGetTextField"), + METHOD(&SymbolLayer::getTextFont, "nativeGetTextFont"), + METHOD(&SymbolLayer::getTextSize, "nativeGetTextSize"), + METHOD(&SymbolLayer::getTextMaxWidth, "nativeGetTextMaxWidth"), + METHOD(&SymbolLayer::getTextLineHeight, "nativeGetTextLineHeight"), + METHOD(&SymbolLayer::getTextLetterSpacing, "nativeGetTextLetterSpacing"), + METHOD(&SymbolLayer::getTextJustify, "nativeGetTextJustify"), + METHOD(&SymbolLayer::getTextRadialOffset, "nativeGetTextRadialOffset"), + METHOD(&SymbolLayer::getTextVariableAnchor, "nativeGetTextVariableAnchor"), + METHOD(&SymbolLayer::getTextAnchor, "nativeGetTextAnchor"), + METHOD(&SymbolLayer::getTextMaxAngle, "nativeGetTextMaxAngle"), + METHOD(&SymbolLayer::getTextWritingMode, "nativeGetTextWritingMode"), + METHOD(&SymbolLayer::getTextRotate, "nativeGetTextRotate"), + METHOD(&SymbolLayer::getTextPadding, "nativeGetTextPadding"), + METHOD(&SymbolLayer::getTextKeepUpright, "nativeGetTextKeepUpright"), + METHOD(&SymbolLayer::getTextTransform, "nativeGetTextTransform"), + METHOD(&SymbolLayer::getTextOffset, "nativeGetTextOffset"), + METHOD(&SymbolLayer::getTextAllowOverlap, "nativeGetTextAllowOverlap"), + METHOD(&SymbolLayer::getTextIgnorePlacement, "nativeGetTextIgnorePlacement"), + METHOD(&SymbolLayer::getTextOptional, "nativeGetTextOptional"), + METHOD(&SymbolLayer::getIconOpacityTransition, "nativeGetIconOpacityTransition"), + METHOD(&SymbolLayer::setIconOpacityTransition, "nativeSetIconOpacityTransition"), + METHOD(&SymbolLayer::getIconOpacity, "nativeGetIconOpacity"), + METHOD(&SymbolLayer::getIconColorTransition, "nativeGetIconColorTransition"), + METHOD(&SymbolLayer::setIconColorTransition, "nativeSetIconColorTransition"), + METHOD(&SymbolLayer::getIconColor, "nativeGetIconColor"), + METHOD(&SymbolLayer::getIconHaloColorTransition, "nativeGetIconHaloColorTransition"), + METHOD(&SymbolLayer::setIconHaloColorTransition, "nativeSetIconHaloColorTransition"), + METHOD(&SymbolLayer::getIconHaloColor, "nativeGetIconHaloColor"), + METHOD(&SymbolLayer::getIconHaloWidthTransition, "nativeGetIconHaloWidthTransition"), + METHOD(&SymbolLayer::setIconHaloWidthTransition, "nativeSetIconHaloWidthTransition"), + METHOD(&SymbolLayer::getIconHaloWidth, "nativeGetIconHaloWidth"), + METHOD(&SymbolLayer::getIconHaloBlurTransition, "nativeGetIconHaloBlurTransition"), + METHOD(&SymbolLayer::setIconHaloBlurTransition, "nativeSetIconHaloBlurTransition"), + METHOD(&SymbolLayer::getIconHaloBlur, "nativeGetIconHaloBlur"), + METHOD(&SymbolLayer::getIconTranslateTransition, "nativeGetIconTranslateTransition"), + METHOD(&SymbolLayer::setIconTranslateTransition, "nativeSetIconTranslateTransition"), + METHOD(&SymbolLayer::getIconTranslate, "nativeGetIconTranslate"), + METHOD(&SymbolLayer::getIconTranslateAnchor, "nativeGetIconTranslateAnchor"), + METHOD(&SymbolLayer::getTextOpacityTransition, "nativeGetTextOpacityTransition"), + METHOD(&SymbolLayer::setTextOpacityTransition, "nativeSetTextOpacityTransition"), + METHOD(&SymbolLayer::getTextOpacity, "nativeGetTextOpacity"), + METHOD(&SymbolLayer::getTextColorTransition, "nativeGetTextColorTransition"), + METHOD(&SymbolLayer::setTextColorTransition, "nativeSetTextColorTransition"), + METHOD(&SymbolLayer::getTextColor, "nativeGetTextColor"), + METHOD(&SymbolLayer::getTextHaloColorTransition, "nativeGetTextHaloColorTransition"), + METHOD(&SymbolLayer::setTextHaloColorTransition, "nativeSetTextHaloColorTransition"), + METHOD(&SymbolLayer::getTextHaloColor, "nativeGetTextHaloColor"), + METHOD(&SymbolLayer::getTextHaloWidthTransition, "nativeGetTextHaloWidthTransition"), + METHOD(&SymbolLayer::setTextHaloWidthTransition, "nativeSetTextHaloWidthTransition"), + METHOD(&SymbolLayer::getTextHaloWidth, "nativeGetTextHaloWidth"), + METHOD(&SymbolLayer::getTextHaloBlurTransition, "nativeGetTextHaloBlurTransition"), + METHOD(&SymbolLayer::setTextHaloBlurTransition, "nativeSetTextHaloBlurTransition"), + METHOD(&SymbolLayer::getTextHaloBlur, "nativeGetTextHaloBlur"), + METHOD(&SymbolLayer::getTextTranslateTransition, "nativeGetTextTranslateTransition"), + METHOD(&SymbolLayer::setTextTranslateTransition, "nativeSetTextTranslateTransition"), + METHOD(&SymbolLayer::getTextTranslate, "nativeGetTextTranslate"), + METHOD(&SymbolLayer::getTextTranslateAnchor, "nativeGetTextTranslateAnchor")); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/layers/symbol_layer.hpp b/src/style/layers/symbol_layer.hpp new file mode 100644 index 000000000..9e494e678 --- /dev/null +++ b/src/style/layers/symbol_layer.hpp @@ -0,0 +1,180 @@ +// This file is generated. Edit android/platform/scripts/generate-style-code.js, then run `make android-style-code`. + +#pragma once + +#include "layer.hpp" +#include "../transition_options.hpp" +#include +#include +#include + +namespace mbgl { +namespace android { + +class SymbolLayer : public Layer { +public: + using SuperTag = Layer; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/SymbolLayer"; }; + + SymbolLayer(jni::JNIEnv&, jni::String&, jni::String&); + + SymbolLayer(mbgl::Map&, mbgl::style::SymbolLayer&); + + SymbolLayer(mbgl::Map&, std::unique_ptr); + + ~SymbolLayer(); + + // Properties + + jni::Local> getSymbolPlacement(jni::JNIEnv&); + + jni::Local> getSymbolSpacing(jni::JNIEnv&); + + jni::Local> getSymbolAvoidEdges(jni::JNIEnv&); + + jni::Local> getSymbolSortKey(jni::JNIEnv&); + + jni::Local> getSymbolZOrder(jni::JNIEnv&); + + jni::Local> getIconAllowOverlap(jni::JNIEnv&); + + jni::Local> getIconIgnorePlacement(jni::JNIEnv&); + + jni::Local> getIconOptional(jni::JNIEnv&); + + jni::Local> getIconRotationAlignment(jni::JNIEnv&); + + jni::Local> getIconSize(jni::JNIEnv&); + + jni::Local> getIconTextFit(jni::JNIEnv&); + + jni::Local> getIconTextFitPadding(jni::JNIEnv&); + + jni::Local> getIconImage(jni::JNIEnv&); + + jni::Local> getIconRotate(jni::JNIEnv&); + + jni::Local> getIconPadding(jni::JNIEnv&); + + jni::Local> getIconKeepUpright(jni::JNIEnv&); + + jni::Local> getIconOffset(jni::JNIEnv&); + + jni::Local> getIconAnchor(jni::JNIEnv&); + + jni::Local> getIconPitchAlignment(jni::JNIEnv&); + + jni::Local> getTextPitchAlignment(jni::JNIEnv&); + + jni::Local> getTextRotationAlignment(jni::JNIEnv&); + + jni::Local> getTextField(jni::JNIEnv&); + + jni::Local> getTextFont(jni::JNIEnv&); + + jni::Local> getTextSize(jni::JNIEnv&); + + jni::Local> getTextMaxWidth(jni::JNIEnv&); + + jni::Local> getTextLineHeight(jni::JNIEnv&); + + jni::Local> getTextLetterSpacing(jni::JNIEnv&); + + jni::Local> getTextJustify(jni::JNIEnv&); + + jni::Local> getTextRadialOffset(jni::JNIEnv&); + + jni::Local> getTextVariableAnchor(jni::JNIEnv&); + + jni::Local> getTextAnchor(jni::JNIEnv&); + + jni::Local> getTextMaxAngle(jni::JNIEnv&); + + jni::Local> getTextWritingMode(jni::JNIEnv&); + + jni::Local> getTextRotate(jni::JNIEnv&); + + jni::Local> getTextPadding(jni::JNIEnv&); + + jni::Local> getTextKeepUpright(jni::JNIEnv&); + + jni::Local> getTextTransform(jni::JNIEnv&); + + jni::Local> getTextOffset(jni::JNIEnv&); + + jni::Local> getTextAllowOverlap(jni::JNIEnv&); + + jni::Local> getTextIgnorePlacement(jni::JNIEnv&); + + jni::Local> getTextOptional(jni::JNIEnv&); + + jni::Local> getIconOpacity(jni::JNIEnv&); + void setIconOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getIconOpacityTransition(jni::JNIEnv&); + + jni::Local> getIconColor(jni::JNIEnv&); + void setIconColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getIconColorTransition(jni::JNIEnv&); + + jni::Local> getIconHaloColor(jni::JNIEnv&); + void setIconHaloColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getIconHaloColorTransition(jni::JNIEnv&); + + jni::Local> getIconHaloWidth(jni::JNIEnv&); + void setIconHaloWidthTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getIconHaloWidthTransition(jni::JNIEnv&); + + jni::Local> getIconHaloBlur(jni::JNIEnv&); + void setIconHaloBlurTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getIconHaloBlurTransition(jni::JNIEnv&); + + jni::Local> getIconTranslate(jni::JNIEnv&); + void setIconTranslateTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getIconTranslateTransition(jni::JNIEnv&); + + jni::Local> getIconTranslateAnchor(jni::JNIEnv&); + + jni::Local> getTextOpacity(jni::JNIEnv&); + void setTextOpacityTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getTextOpacityTransition(jni::JNIEnv&); + + jni::Local> getTextColor(jni::JNIEnv&); + void setTextColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getTextColorTransition(jni::JNIEnv&); + + jni::Local> getTextHaloColor(jni::JNIEnv&); + void setTextHaloColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getTextHaloColorTransition(jni::JNIEnv&); + + jni::Local> getTextHaloWidth(jni::JNIEnv&); + void setTextHaloWidthTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getTextHaloWidthTransition(jni::JNIEnv&); + + jni::Local> getTextHaloBlur(jni::JNIEnv&); + void setTextHaloBlurTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getTextHaloBlurTransition(jni::JNIEnv&); + + jni::Local> getTextTranslate(jni::JNIEnv&); + void setTextTranslateTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getTextTranslateTransition(jni::JNIEnv&); + + jni::Local> getTextTranslateAnchor(jni::JNIEnv&); + +}; // class SymbolLayer + +class SymbolJavaLayerPeerFactory final : public JavaLayerPeerFactory, public mbgl::SymbolLayerFactory { +public: + ~SymbolJavaLayerPeerFactory() override; + + // JavaLayerPeerFactory overrides. + jni::Local> createJavaLayerPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Layer&) final; + jni::Local> createJavaLayerPeer(jni::JNIEnv& env, mbgl::Map& map, std::unique_ptr) final; + + void registerNative(jni::JNIEnv&) final; + + LayerFactory* getLayerFactory() final { return this; } + +}; // class SymbolJavaLayerPeerFactory + +} // namespace android +} // namespace mbgl diff --git a/src/style/light.cpp b/src/style/light.cpp new file mode 100644 index 000000000..d8ed4d9d9 --- /dev/null +++ b/src/style/light.cpp @@ -0,0 +1,143 @@ +#include +#include "light.hpp" +#include "conversion/transition_options.hpp" +#include "conversion/position.hpp" + +namespace mbgl { +namespace android { + +Light::Light(mbgl::Map& coreMap, mbgl::style::Light& coreLight) + : light(coreLight) , map(&coreMap) { +} + +static Light* initializeLightPeer(mbgl::Map& map, mbgl::style::Light& coreLight) { + return new Light(map, coreLight); +} + +jni::Local> Light::createJavaLightPeer(jni::JNIEnv& env, Map& map, mbgl::style::Light& coreLight) { + std::unique_ptr peerLight = std::unique_ptr(initializeLightPeer(map, coreLight)); + auto result = peerLight->createJavaPeer(env); + peerLight.release(); + return result; +} + +jni::Local> Light::createJavaPeer(jni::JNIEnv& env) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(this)); +} + +void Light::setAnchor(jni::JNIEnv& env, const jni::String& property) { + std::string anchorStr = jni::Make(env, property); + if (anchorStr.compare("map") == 0) { + light.setAnchor(LightAnchorType::Map); + } else if (anchorStr.compare("viewport") == 0) { + light.setAnchor(LightAnchorType::Viewport); + } +} + +jni::Local Light::getAnchor(jni::JNIEnv& env) { + auto anchorType = light.getAnchor(); + if (anchorType == LightAnchorType::Map) { + return jni::Make(env, "map"); + } else { + return jni::Make(env, "viewport"); + } +} + +void Light::setPosition(jni::JNIEnv& env, const jni::Object& jposition) { + using namespace mbgl::android::conversion; + auto position = *convert(env, jposition); + light.setPosition(position); +} + +jni::Local> Light::getPosition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::Position position = light.getPosition().asConstant(); + return std::move(*convert>>(env, position)); +} + +jni::Local> Light::getPositionTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = light.getPositionTransition(); + return std::move(*convert>>(env, options)); +} + +void Light::setPositionTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + light.setPositionTransition(options); +} + +void Light::setColor(jni::JNIEnv& env, const jni::String& property) { + auto color = Color::parse(jni::Make(env, property)); + if (color) { + light.setColor(color.value()); + } +} + +jni::Local Light::getColor(jni::JNIEnv &env) { + auto color = light.getColor().asConstant(); + return jni::Make(env, color.stringify()); +} + +jni::Local> Light::getColorTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = light.getColorTransition(); + return std::move(*convert>>(env, options)); +} + +void Light::setColorTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + light.setColorTransition(options); +} + +void Light::setIntensity(jni::JNIEnv&, jni::jfloat property) { + light.setIntensity(property); +} + +jni::jfloat Light::getIntensity(jni::JNIEnv&) { + return light.getIntensity().asConstant(); +} + +jni::Local> Light::getIntensityTransition(jni::JNIEnv& env) { + using namespace mbgl::android::conversion; + mbgl::style::TransitionOptions options = light.getIntensityTransition(); + return std::move(*convert>>(env, options)); +} + +void Light::setIntensityTransition(jni::JNIEnv&, jlong duration, jlong delay) { + mbgl::style::TransitionOptions options; + options.duration.emplace(mbgl::Milliseconds(duration)); + options.delay.emplace(mbgl::Milliseconds(delay)); + light.setIntensityTransition(options); +} + +void Light::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + +#define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + // Register the peer + jni::RegisterNativePeer(env, javaClass, "nativePtr", + METHOD(&Light::getAnchor, "nativeGetAnchor"), + METHOD(&Light::setAnchor, "nativeSetAnchor"), + METHOD(&Light::getPositionTransition, "nativeGetPositionTransition"), + METHOD(&Light::setPositionTransition, "nativeSetPositionTransition"), + METHOD(&Light::getPosition, "nativeGetPosition"), + METHOD(&Light::setPosition, "nativeSetPosition"), + METHOD(&Light::getColorTransition, "nativeGetColorTransition"), + METHOD(&Light::setColorTransition, "nativeSetColorTransition"), + METHOD(&Light::getColor, "nativeGetColor"), + METHOD(&Light::setColor, "nativeSetColor"), + METHOD(&Light::getIntensityTransition, "nativeGetIntensityTransition"), + METHOD(&Light::setIntensityTransition, "nativeSetIntensityTransition"), + METHOD(&Light::getIntensity, "nativeGetIntensity"), + METHOD(&Light::setIntensity, "nativeSetIntensity")); +} + +} // namespace android +} // namespace mb diff --git a/src/style/light.hpp b/src/style/light.hpp new file mode 100644 index 000000000..97767c4f7 --- /dev/null +++ b/src/style/light.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include +#include +#include "transition_options.hpp" +#include "position.hpp" +#include +#include + +namespace mbgl { +namespace android { + +using namespace style; + +class Light : private mbgl::util::noncopyable { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/light/Light"; }; + + static void registerNative(jni::JNIEnv&); + + static jni::Local> createJavaLightPeer(jni::JNIEnv&, mbgl::Map&, mbgl::style::Light&); + + Light(mbgl::Map&, mbgl::style::Light&); + + void setAnchor(jni::JNIEnv&, const jni::String&); + jni::Local getAnchor(jni::JNIEnv&); + void setPosition(jni::JNIEnv&, const jni::Object&); + jni::Local> getPosition(jni::JNIEnv&); + void setPositionTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getPositionTransition(jni::JNIEnv&); + void setColor(jni::JNIEnv&, const jni::String&); + jni::Local getColor(jni::JNIEnv&); + void setColorTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getColorTransition(jni::JNIEnv&); + void setIntensity(jni::JNIEnv&, jni::jfloat); + jni::jfloat getIntensity(jni::JNIEnv&); + void setIntensityTransition(jni::JNIEnv&, jlong duration, jlong delay); + jni::Local> getIntensityTransition(jni::JNIEnv&); + jni::Local> createJavaPeer(jni::JNIEnv&); + +protected: + + // Raw reference to the light + mbgl::style::Light& light; + + // Map is set when the light is retrieved + mbgl::Map* map; +}; +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/style/position.cpp b/src/style/position.cpp new file mode 100644 index 000000000..326310be0 --- /dev/null +++ b/src/style/position.cpp @@ -0,0 +1,35 @@ +#include "position.hpp" + +namespace mbgl { +namespace android { + +jni::Local> Position::fromPosition(jni::JNIEnv& env, jfloat radialCoordinate, jfloat azimuthalAngle, jfloat polarAngle) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jfloat, jfloat, jfloat)>(env, "fromPosition"); + return javaClass.Call(env, method, radialCoordinate, azimuthalAngle, polarAngle); +} + +void Position::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +float Position::getRadialCoordinate(jni::JNIEnv& env, const jni::Object& position) { + static auto& javaClass = jni::Class::Singleton(env); + static auto field = javaClass.GetField(env, "radialCoordinate"); + return position.Get(env, field); +} + +float Position::getAzimuthalAngle(jni::JNIEnv& env, const jni::Object& position) { + static auto& javaClass = jni::Class::Singleton(env); + static auto field = javaClass.GetField(env, "azimuthalAngle"); + return position.Get(env, field); +} + +float Position::getPolarAngle(jni::JNIEnv& env, const jni::Object& position) { + static auto& javaClass = jni::Class::Singleton(env); + static auto field = javaClass.GetField(env, "polarAngle"); + return position.Get(env, field); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/style/position.hpp b/src/style/position.hpp new file mode 100644 index 000000000..eb4f5ac67 --- /dev/null +++ b/src/style/position.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include + +namespace mbgl { +namespace android { + +class Position : private mbgl::util::noncopyable { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/light/Position"; }; + + static jni::Local> fromPosition(jni::JNIEnv&, jfloat, jfloat, jfloat); + + static void registerNative(jni::JNIEnv&); + + static float getRadialCoordinate(jni::JNIEnv&, const jni::Object&); + static float getAzimuthalAngle(jni::JNIEnv&, const jni::Object&); + static float getPolarAngle(jni::JNIEnv&, const jni::Object&); +}; + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/style/sources/custom_geometry_source.cpp b/src/style/sources/custom_geometry_source.cpp new file mode 100644 index 000000000..dccca4cf4 --- /dev/null +++ b/src/style/sources/custom_geometry_source.cpp @@ -0,0 +1,210 @@ +#include "custom_geometry_source.hpp" +#include "../../attach_env.hpp" + +#include + +// Java -> C++ conversion +#include "../android_conversion.hpp" +#include "../conversion/filter.hpp" + +// C++ -> Java conversion +#include "../../conversion/conversion.hpp" +#include "../../conversion/collection.hpp" +#include "../../geojson/feature.hpp" +#include + +#include + +namespace mbgl { +namespace android { + + // This conversion is expected not to fail because it's used only in contexts where + // the value was originally a CustomGeometrySourceOptions 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, + const jni::Object<>& options, + style::TileFunction fetchFn, + style::TileFunction cancelFn) { + using namespace mbgl::style::conversion; + if (!options) { + return style::CustomGeometrySource::Options(); + } + Error error; + optional result = convert(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, + const jni::String& sourceId, + const jni::Object<>& options) + : Source(env, std::make_unique( + jni::Make(env, sourceId), + convertCustomGeometrySourceOptions(env, options, + std::bind(&CustomGeometrySource::fetchTile, this, std::placeholders::_1), + std::bind(&CustomGeometrySource::cancelTile, this, std::placeholders::_1)))) { + } + + CustomGeometrySource::CustomGeometrySource(jni::JNIEnv& env, + mbgl::style::Source& coreSource, + AndroidRendererFrontend& frontend) + : Source(env, coreSource, createJavaPeer(env), frontend) { + } + + CustomGeometrySource::~CustomGeometrySource() { + releaseThreads(); + } + + void CustomGeometrySource::fetchTile (const mbgl::CanonicalTileID& tileID) { + android::UniqueEnv _env = android::AttachEnv(); + + static auto& javaClass = jni::Class::Singleton(*_env); + static auto fetchTile = javaClass.GetMethod(*_env, "fetchTile"); + + // The source is removed on the main thread, but it still exists on the Render thread until the frame is complete. + // This might cause fetchTile/cancelTile invocations when the Java thread is shutting down and the peer has already been released. + // See https://github.com/mapbox/mapbox-gl-native/issues/12551. + if(!javaPeer) { + return; + } + + auto peer = jni::Cast(*_env, javaClass, javaPeer); + peer.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& javaClass = jni::Class::Singleton(*_env); + static auto cancelTile = javaClass.GetMethod(*_env, "cancelTile"); + + // The source is removed on the main thread, but it still exists on the Render thread until the frame is complete. + // This might cause fetchTile/cancelTile invocations when the Java thread is shutting down and the peer has already been released. + // See https://github.com/mapbox/mapbox-gl-native/issues/12551. + if(!javaPeer) { + return; + } + + auto peer = jni::Cast(*_env, javaClass, javaPeer); + peer.Call(*_env, cancelTile, (int)tileID.z, (int)tileID.x, (int)tileID.y); + }; + + void CustomGeometrySource::startThreads() { + android::UniqueEnv _env = android::AttachEnv(); + + static auto& javaClass = jni::Class::Singleton(*_env); + static auto startThreads = javaClass.GetMethod(*_env, "startThreads"); + + assert(javaPeer); + + auto peer = jni::Cast(*_env, javaClass, javaPeer); + peer.Call(*_env, startThreads); + } + + void CustomGeometrySource::releaseThreads() { + android::UniqueEnv _env = android::AttachEnv(); + + static auto& javaClass = jni::Class::Singleton(*_env); + static auto releaseThreads = javaClass.GetMethod(*_env, "releaseThreads"); + + assert(javaPeer); + + auto peer = jni::Cast(*_env, javaClass, javaPeer); + peer.Call(*_env, releaseThreads); + }; + + bool CustomGeometrySource::isCancelled(jni::jint z, + jni::jint x, + jni::jint y) { + android::UniqueEnv _env = android::AttachEnv(); + + static auto& javaClass = jni::Class::Singleton(*_env); + static auto isCancelled = javaClass.GetMethod(*_env, "isCancelled"); + + assert(javaPeer); + + auto peer = jni::Cast(*_env, javaClass, javaPeer); + return peer.Call(*_env, isCancelled, z, x, y); + }; + + void CustomGeometrySource::setTileData(jni::JNIEnv& env, + jni::jint z, + jni::jint x, + jni::jint y, + const jni::Object& jFeatures) { + using namespace mbgl::android::geojson; + + // Convert the jni object + auto geometry = geojson::FeatureCollection::convert(env, jFeatures); + + // Update the core source if not cancelled + if (!isCancelled(z, x ,y)) { + source.as()->CustomGeometrySource::setTileData(CanonicalTileID(z, x, y), GeoJSON(geometry)); + } + } + + void CustomGeometrySource::invalidateTile(jni::JNIEnv&, jni::jint z, jni::jint x, jni::jint y) { + source.as()->CustomGeometrySource::invalidateTile(CanonicalTileID(z, x, y)); + } + + void CustomGeometrySource::invalidateBounds(jni::JNIEnv& env, const jni::Object& jBounds) { + auto bounds = LatLngBounds::getLatLngBounds(env, jBounds); + source.as()->CustomGeometrySource::invalidateRegion(bounds); + } + + jni::Local>> CustomGeometrySource::querySourceFeatures(jni::JNIEnv& env, + const jni::Array>& jfilter) { + using namespace mbgl::android::conversion; + using namespace mbgl::android::geojson; + + std::vector features; + if (rendererFrontend) { + features = rendererFrontend->querySourceFeatures(source.getID(), { {}, toFilter(env, jfilter) }); + } + return Feature::convert(env, features); + } + + jni::Local> CustomGeometrySource::createJavaPeer(jni::JNIEnv& env) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(this)); + } + + void CustomGeometrySource::addToMap(JNIEnv& env, const jni::Object& obj, mbgl::Map& map, AndroidRendererFrontend& frontend) { + Source::addToMap(env, obj, map, frontend); + startThreads(); + } + + bool CustomGeometrySource::removeFromMap(JNIEnv& env, const jni::Object& source, mbgl::Map& map) { + bool successfullyRemoved = Source::removeFromMap(env, source, map); + if (successfullyRemoved) { + releaseThreads(); + } + return successfullyRemoved; + } + + void CustomGeometrySource::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer&>, + "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/src/style/sources/custom_geometry_source.hpp b/src/style/sources/custom_geometry_source.hpp new file mode 100644 index 000000000..52e4027dd --- /dev/null +++ b/src/style/sources/custom_geometry_source.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "source.hpp" +#include +#include +#include +#include "../../geojson/geometry.hpp" +#include "../../geojson/feature.hpp" +#include "../../geojson/feature_collection.hpp" +#include "../../geometry/lat_lng_bounds.hpp" +#include + +namespace mbgl { +namespace android { + +class CustomGeometrySource : public Source { +public: + using SuperTag = Source; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/sources/CustomGeometrySource"; }; + + static void registerNative(jni::JNIEnv&); + + CustomGeometrySource(jni::JNIEnv&, const jni::String&, const jni::Object<>&); + CustomGeometrySource(jni::JNIEnv&, mbgl::style::Source&, AndroidRendererFrontend&); + ~CustomGeometrySource(); + + bool removeFromMap(JNIEnv&, const jni::Object&, mbgl::Map&) override; + void addToMap(JNIEnv&, const jni::Object&, mbgl::Map&, AndroidRendererFrontend&) override; + + void fetchTile(const mbgl::CanonicalTileID& tileID); + void cancelTile(const mbgl::CanonicalTileID& tileID); + bool isCancelled(jni::jint z, jni::jint x, jni::jint y); + void startThreads(); + void releaseThreads(); + +private: + void setTileData(jni::JNIEnv& env, jni::jint z, jni::jint x, jni::jint y, const jni::Object& jf); + + void invalidateTile(jni::JNIEnv& env, jni::jint z, jni::jint x, jni::jint y); + void invalidateBounds(jni::JNIEnv& env, const jni::Object& bounds); + + jni::Local>> querySourceFeatures(jni::JNIEnv&, + const jni::Array>& ); + + jni::Local> createJavaPeer(jni::JNIEnv&); + +}; // class CustomGeometrySource + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/geojson_source.cpp b/src/style/sources/geojson_source.cpp new file mode 100644 index 000000000..5ff486427 --- /dev/null +++ b/src/style/sources/geojson_source.cpp @@ -0,0 +1,265 @@ +#include "geojson_source.hpp" +#include "../../attach_env.hpp" + +#include + +// Java -> C++ conversion +#include "../android_conversion.hpp" +#include "../conversion/filter.hpp" +#include +#include +#include + +// C++ -> Java conversion +#include "../../conversion/conversion.hpp" +#include "../../conversion/collection.hpp" +#include "../../geojson/feature.hpp" +#include "../conversion/url_or_tileset.hpp" + +#include + +// GeoJSONSource uses a "coalescing" model for high frequency asynchronous data update calls, +// which in practice means, that any update that started processing is going to finish +// and the last scheduled update is going to finish as well. Any updates scheduled during processing can be canceled. +// Conversion from Java features to core ones is done on a worker thread and once finished, +// the ownership of the converted features is returned to the calling thread. +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::GeoJSONOptions convertGeoJSONOptions(jni::JNIEnv& env, const jni::Object<>& options) { + using namespace mbgl::style::conversion; + if (!options) { + return style::GeoJSONOptions(); + } + Error error; + optional result = convert( + mbgl::android::Value(env, options), error); + if (!result) { + throw std::logic_error(error.message); + } + return *result; + } + + GeoJSONSource::GeoJSONSource(jni::JNIEnv& env, const jni::String& sourceId, const jni::Object<>& options) + : Source(env, std::make_unique( + jni::Make(env, sourceId), + convertGeoJSONOptions(env, options))) + , converter(std::make_unique>(Scheduler::GetBackground())) { + } + + GeoJSONSource::GeoJSONSource(jni::JNIEnv& env, + mbgl::style::Source& coreSource, + AndroidRendererFrontend& frontend) + : Source(env, coreSource, createJavaPeer(env), frontend) + , converter(std::make_unique>(Scheduler::GetBackground())) { + } + + GeoJSONSource::~GeoJSONSource() = default; + + void GeoJSONSource::setGeoJSONString(jni::JNIEnv& env, const jni::String& jString) { + + std::shared_ptr json = std::make_shared(jni::Make(env, jString)); + + Update::Converter converterFn = [this, json](ActorRef _callback) { + converter->self().invoke(&FeatureConverter::convertJson, json, _callback); + }; + + setAsync(converterFn); + } + + void GeoJSONSource::setFeatureCollection(jni::JNIEnv& env, const jni::Object& jFeatures) { + setCollectionAsync(env, jFeatures); + } + + void GeoJSONSource::setFeature(jni::JNIEnv& env, const jni::Object& jFeature) { + setCollectionAsync(env, jFeature); + } + + void GeoJSONSource::setGeometry(jni::JNIEnv& env, const jni::Object& jGeometry) { + setCollectionAsync(env, jGeometry); + } + + void GeoJSONSource::setURL(jni::JNIEnv& env, const jni::String& url) { + // Update the core source + source.as()->GeoJSONSource::setURL(jni::Make(env, url)); + } + + jni::Local GeoJSONSource::getURL(jni::JNIEnv& env) { + optional url = source.as()->GeoJSONSource::getURL(); + return url ? jni::Make(env, *url) : jni::Local(); + } + + jni::Local>> GeoJSONSource::querySourceFeatures(jni::JNIEnv& env, + const jni::Array>& jfilter) { + using namespace mbgl::android::conversion; + using namespace mbgl::android::geojson; + + std::vector features; + if (rendererFrontend) { + features = rendererFrontend->querySourceFeatures(source.getID(), + { {}, toFilter(env, jfilter) }); + } + return Feature::convert(env, features); + } + + jni::Local>> GeoJSONSource::getClusterChildren(jni::JNIEnv& env, const jni::Object& feature) { + using namespace mbgl::android::conversion; + using namespace mbgl::android::geojson; + + if (rendererFrontend) { + mbgl::Feature _feature = Feature::convert(env, feature); + _feature.properties["cluster_id"] = static_cast(_feature.properties["cluster_id"].get()); + const auto featureExtension = rendererFrontend->queryFeatureExtensions(source.getID(), _feature, "supercluster", "children", {}); + if (featureExtension.is()) { + return Feature::convert(env, featureExtension.get()); + } + } + return jni::Array>::New(env, 0); + } + + jni::Local>> GeoJSONSource::getClusterLeaves(jni::JNIEnv& env, const jni::Object& feature, jni::jlong limit, jni::jlong offset) { + using namespace mbgl::android::conversion; + using namespace mbgl::android::geojson; + + if (rendererFrontend) { + mbgl::Feature _feature = Feature::convert(env, feature); + _feature.properties["cluster_id"] = static_cast(_feature.properties["cluster_id"].get()); + const std::map options = { {"limit", static_cast(limit)}, + {"offset", static_cast(offset)} }; + auto featureExtension = rendererFrontend->queryFeatureExtensions(source.getID(), _feature, "supercluster", "leaves", options); + if (featureExtension.is()) { + return Feature::convert(env, featureExtension.get()); + } + } + return jni::Array>::New(env, 0);; + } + + jint GeoJSONSource::getClusterExpansionZoom(jni::JNIEnv& env, const jni::Object& feature) { + using namespace mbgl::android::conversion; + using namespace mbgl::android::geojson; + + if (rendererFrontend) { + mbgl::Feature _feature = Feature::convert(env, feature); + _feature.properties["cluster_id"] = static_cast(_feature.properties["cluster_id"].get()); + auto featureExtension = rendererFrontend->queryFeatureExtensions(source.getID(), _feature, "supercluster", "expansion-zoom", {}); + if (featureExtension.is()) { + auto value = featureExtension.get(); + if (value.is()) { + return value.get(); + } + } + } + return 0; + } + + jni::Local> GeoJSONSource::createJavaPeer(jni::JNIEnv& env) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(this)); + } + + template + void GeoJSONSource::setCollectionAsync(jni::JNIEnv& env, const jni::Object& jObject) { + auto global = jni::NewGlobal(env, jObject); + auto object = std::make_shared(std::move(global)); + + Update::Converter converterFn = [this, object](ActorRef _callback) { + converter->self().invoke(&FeatureConverter::convertObject, object, _callback); + }; + + setAsync(converterFn); + } + + void GeoJSONSource::setAsync(Update::Converter converterFn) { + awaitingUpdate = std::make_unique( + std::move(converterFn), + std::make_unique>( + *Scheduler::GetCurrent(), + [this](GeoJSON geoJSON) { + // conversion from Java features to core ones finished + android::UniqueEnv _env = android::AttachEnv(); + + // Update the core source + source.as()->GeoJSONSource::setGeoJSON(geoJSON); + + // if there is an awaiting update, execute it, otherwise, release resources + if (awaitingUpdate) { + update = std::move(awaitingUpdate); + update->converterFn(update->callback->self()); + } else { + update.reset(); + } + }) + ); + + // If another update is running, wait + if (update) { + return; + } + + // no updates are being processed, execute this one + update = std::move(awaitingUpdate); + update->converterFn(update->callback->self()); + } + + void GeoJSONSource::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer&>, + "initialize", + "finalize", + METHOD(&GeoJSONSource::setGeoJSONString, "nativeSetGeoJsonString"), + METHOD(&GeoJSONSource::setFeatureCollection, "nativeSetFeatureCollection"), + METHOD(&GeoJSONSource::setFeature, "nativeSetFeature"), + METHOD(&GeoJSONSource::setGeometry, "nativeSetGeometry"), + METHOD(&GeoJSONSource::setURL, "nativeSetUrl"), + METHOD(&GeoJSONSource::getURL, "nativeGetUrl"), + METHOD(&GeoJSONSource::querySourceFeatures, "querySourceFeatures"), + METHOD(&GeoJSONSource::getClusterChildren, "nativeGetClusterChildren"), + METHOD(&GeoJSONSource::getClusterLeaves, "nativeGetClusterLeaves"), + METHOD(&GeoJSONSource::getClusterExpansionZoom, "nativeGetClusterExpansionZoom") + ); + } + + void FeatureConverter::convertJson(std::shared_ptr json, + ActorRef callback) { + using namespace mbgl::style::conversion; + + android::UniqueEnv _env = android::AttachEnv(); + + // Convert the jni object + Error error; + optional converted = parseGeoJSON(*json, error); + if(!converted) { + mbgl::Log::Error(mbgl::Event::JNI, "Error setting geo json: " + error.message); + return; + } + + callback.invoke(&Callback::operator(), *converted); + } + + template + void FeatureConverter::convertObject(std::shared_ptr, jni::EnvAttachingDeleter>> jObject, ActorRef callback) { + using namespace mbgl::android::geojson; + + android::UniqueEnv _env = android::AttachEnv(); + // Convert the jni object + auto geometry = JNIType::convert(*_env, *jObject); + callback.invoke(&Callback::operator(), GeoJSON(geometry)); + } + + Update::Update(Converter _converterFn, std::unique_ptr> _callback) + : converterFn(std::move(_converterFn)) + , callback(std::move(_callback)) {} + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/geojson_source.hpp b/src/style/sources/geojson_source.hpp new file mode 100644 index 000000000..e737e4192 --- /dev/null +++ b/src/style/sources/geojson_source.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "source.hpp" +#include +#include "../../geojson/geometry.hpp" +#include "../../geojson/feature.hpp" +#include "../../geojson/feature_collection.hpp" +#include "../../android_renderer_frontend.hpp" +#include + +namespace mbgl { +namespace android { + +using Callback = std::function; + +struct FeatureConverter { + void convertJson(std::shared_ptr, ActorRef); + + template + void convertObject(std::shared_ptr, jni::EnvAttachingDeleter>>, ActorRef); +}; + +struct Update { + using Converter = std::function)>; + Converter converterFn; + + std::unique_ptr> callback; + + Update(Converter, std::unique_ptr>); +}; + +class GeoJSONSource : public Source { +public: + using SuperTag = Source; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/sources/GeoJsonSource"; }; + + static void registerNative(jni::JNIEnv&); + + GeoJSONSource(jni::JNIEnv&, const jni::String&, const jni::Object<>&); + GeoJSONSource(jni::JNIEnv&, mbgl::style::Source&, AndroidRendererFrontend&); + ~GeoJSONSource(); + +private: + void setGeoJSONString(jni::JNIEnv&, const jni::String&); + void setFeatureCollection(jni::JNIEnv&, const jni::Object&); + void setFeature(jni::JNIEnv&, const jni::Object&); + void setGeometry(jni::JNIEnv&, const jni::Object&); + void setURL(jni::JNIEnv&, const jni::String&); + + jni::Local>> querySourceFeatures(jni::JNIEnv&, + const jni::Array>&); + + jni::Local>> getClusterChildren(jni::JNIEnv&, const jni::Object&); + jni::Local>> getClusterLeaves(jni::JNIEnv&, const jni::Object&, jni::jlong, jni::jlong); + jint getClusterExpansionZoom(jni::JNIEnv&, const jni::Object&); + + jni::Local getURL(jni::JNIEnv&); + + jni::Local> createJavaPeer(jni::JNIEnv&); + std::unique_ptr awaitingUpdate; + std::unique_ptr update; + std::shared_ptr threadPool; + std::unique_ptr> converter; + + template + void setCollectionAsync(jni::JNIEnv&, const jni::Object&); + + void setAsync(Update::Converter); + +}; // class GeoJSONSource + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/image_source.cpp b/src/style/sources/image_source.cpp new file mode 100644 index 000000000..b42e0e5a5 --- /dev/null +++ b/src/style/sources/image_source.cpp @@ -0,0 +1,79 @@ +#include "image_source.hpp" + +// Java -> C++ conversion +#include "../android_conversion.hpp" + +// C++ -> Java conversion +#include "../../conversion/conversion.hpp" +#include +#include + +#include "../../bitmap.hpp" +#include +#include + +namespace mbgl { +namespace android { + + ImageSource::ImageSource(jni::JNIEnv& env, const jni::String& sourceId, const jni::Object& coordinatesObject) + : Source(env, std::make_unique( + jni::Make(env, sourceId), + LatLngQuad::getLatLngArray(env, coordinatesObject) + ) + ) { + } + + ImageSource::ImageSource(jni::JNIEnv& env, + mbgl::style::Source& coreSource, + AndroidRendererFrontend& frontend) + : Source(env, coreSource, createJavaPeer(env), frontend) { + } + + ImageSource::~ImageSource() = default; + + void ImageSource::setURL(jni::JNIEnv& env, const jni::String& url) { + // Update the core source + source.as()->ImageSource::setURL(jni::Make(env, url)); + } + + jni::Local ImageSource::getURL(jni::JNIEnv& env) { + optional url = source.as()->ImageSource::getURL(); + return url ? jni::Make(env, *url) : jni::Local(); + } + + void ImageSource::setImage(jni::JNIEnv& env, const jni::Object& bitmap) { + source.as()->setImage(Bitmap::GetImage(env, bitmap)); + } + + void ImageSource::setCoordinates(jni::JNIEnv& env, const jni::Object& coordinatesObject) { + source.as()->setCoordinates( + LatLngQuad::getLatLngArray(env, coordinatesObject)); + } + + jni::Local> ImageSource::createJavaPeer(jni::JNIEnv& env) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(this)); + } + + void ImageSource::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer&>, + "initialize", + "finalize", + METHOD(&ImageSource::setURL, "nativeSetUrl"), + METHOD(&ImageSource::getURL, "nativeGetUrl"), + METHOD(&ImageSource::setImage, "nativeSetImage"), + METHOD(&ImageSource::setCoordinates, "nativeSetCoordinates") + ); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/image_source.hpp b/src/style/sources/image_source.hpp new file mode 100644 index 000000000..6c359bf0e --- /dev/null +++ b/src/style/sources/image_source.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "source.hpp" +#include "../../geometry/lat_lng_quad.hpp" +#include +#include + +namespace mbgl { +namespace android { + +class Bitmap; + +class ImageSource : public Source { +public: + using SuperTag = Source; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/sources/ImageSource"; }; + + static void registerNative(jni::JNIEnv&); + + ImageSource(jni::JNIEnv&, const jni::String&, const jni::Object&); + + ImageSource(jni::JNIEnv&, mbgl::style::Source&, AndroidRendererFrontend&); + + ~ImageSource(); + + void setURL(jni::JNIEnv&, const jni::String&); + jni::Local getURL(jni::JNIEnv&); + + void setImage(jni::JNIEnv&, const jni::Object&); + + void setCoordinates(jni::JNIEnv&, const jni::Object&); + +private: + jni::Local> createJavaPeer(jni::JNIEnv&); + +}; // class ImageSource + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/raster_dem_source.cpp b/src/style/sources/raster_dem_source.cpp new file mode 100644 index 000000000..f60526a9b --- /dev/null +++ b/src/style/sources/raster_dem_source.cpp @@ -0,0 +1,62 @@ +#include "raster_dem_source.hpp" + +#include "../android_conversion.hpp" +#include "../value.hpp" +#include "../conversion/url_or_tileset.hpp" +#include "source.hpp" + +#include + +#include + +namespace mbgl { +namespace android { + + RasterDEMSource::RasterDEMSource(jni::JNIEnv& env, const jni::String& sourceId, const jni::Object<>& urlOrTileSet, jni::jint tileSize) + : Source( + env, + std::make_unique( + jni::Make(env, sourceId), + convertURLOrTileset(Value(env, urlOrTileSet)), + tileSize + ) + ) { + } + + RasterDEMSource::RasterDEMSource(jni::JNIEnv& env, + mbgl::style::Source& coreSource, + AndroidRendererFrontend& frontend) + : Source(env, coreSource, createJavaPeer(env), frontend) { + } + + RasterDEMSource::~RasterDEMSource() = default; + + jni::Local RasterDEMSource::getURL(jni::JNIEnv& env) { + optional url = source.as()->RasterDEMSource::getURL(); + return url ? jni::Make(env, *url) : jni::Local(); + } + + jni::Local> RasterDEMSource::createJavaPeer(jni::JNIEnv& env) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(this)); + } + + void RasterDEMSource::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer&, jni::jint>, + "initialize", + "finalize", + METHOD(&RasterDEMSource::getURL, "nativeGetUrl") + ); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/raster_dem_source.hpp b/src/style/sources/raster_dem_source.hpp new file mode 100644 index 000000000..0b31c4ecd --- /dev/null +++ b/src/style/sources/raster_dem_source.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "source.hpp" +#include +#include + +namespace mbgl { +namespace android { + +class RasterDEMSource : public Source { +public: + using SuperTag = Source; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/sources/RasterDemSource"; }; + + static void registerNative(jni::JNIEnv&); + + RasterDEMSource(jni::JNIEnv&, const jni::String&, const jni::Object<>&, jni::jint); + RasterDEMSource(jni::JNIEnv&, mbgl::style::Source&, AndroidRendererFrontend&); + ~RasterDEMSource(); + + jni::Local getURL(jni::JNIEnv&); + +private: + jni::Local> createJavaPeer(jni::JNIEnv&); + +}; // class RasterDEMSource + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/raster_source.cpp b/src/style/sources/raster_source.cpp new file mode 100644 index 000000000..535f899c9 --- /dev/null +++ b/src/style/sources/raster_source.cpp @@ -0,0 +1,61 @@ +#include "raster_source.hpp" + +#include "../android_conversion.hpp" +#include "../value.hpp" +#include "../conversion/url_or_tileset.hpp" + +#include + +#include + +namespace mbgl { +namespace android { + + RasterSource::RasterSource(jni::JNIEnv& env, const jni::String& sourceId, const jni::Object<>& urlOrTileSet, jni::jint tileSize) + : Source( + env, + std::make_unique( + jni::Make(env, sourceId), + convertURLOrTileset(Value(env, urlOrTileSet)), + tileSize + ) + ) { + } + + RasterSource::RasterSource(jni::JNIEnv& env, + mbgl::style::Source& coreSource, + AndroidRendererFrontend& frontend) + : Source(env, coreSource, createJavaPeer(env), frontend) { + } + + RasterSource::~RasterSource() = default; + + jni::Local RasterSource::getURL(jni::JNIEnv& env) { + optional url = source.as()->RasterSource::getURL(); + return url ? jni::Make(env, *url) : jni::Local(); + } + + jni::Local> RasterSource::createJavaPeer(jni::JNIEnv& env) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(this)); + } + + void RasterSource::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer&, jni::jint>, + "initialize", + "finalize", + METHOD(&RasterSource::getURL, "nativeGetUrl") + ); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/raster_source.hpp b/src/style/sources/raster_source.hpp new file mode 100644 index 000000000..08648f79a --- /dev/null +++ b/src/style/sources/raster_source.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "source.hpp" +#include +#include + +namespace mbgl { +namespace android { + +class RasterSource : public Source { +public: + using SuperTag = Source; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/sources/RasterSource"; }; + + static void registerNative(jni::JNIEnv&); + + RasterSource(jni::JNIEnv&, const jni::String&, const jni::Object<>&, jni::jint); + RasterSource(jni::JNIEnv&, mbgl::style::Source&, AndroidRendererFrontend&); + ~RasterSource(); + + jni::Local getURL(jni::JNIEnv&); + +private: + jni::Local> createJavaPeer(jni::JNIEnv&); + +}; // class RasterSource + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/source.cpp b/src/style/sources/source.cpp new file mode 100644 index 000000000..be4c0367f --- /dev/null +++ b/src/style/sources/source.cpp @@ -0,0 +1,168 @@ +#include "source.hpp" +#include "../android_conversion.hpp" +#include "../../attach_env.hpp" + +#include + +#include +#include + +// Java -> C++ conversion +#include +#include + +// C++ -> Java conversion +#include "../conversion/property_value.hpp" + +#include + +// Core Sources +#include +#include +#include +#include + +// Android Source peers +#include "geojson_source.hpp" +#include "image_source.hpp" +#include "raster_source.hpp" +#include "unknown_source.hpp" +#include "vector_source.hpp" +#include "custom_geometry_source.hpp" +#include "raster_dem_source.hpp" + +namespace mbgl { +namespace android { + + static std::unique_ptr createSourcePeer(jni::JNIEnv& env, mbgl::style::Source& coreSource, AndroidRendererFrontend& frontend) { + if (coreSource.is()) { + return std::make_unique(env, *coreSource.as(), frontend); + } else if (coreSource.is()) { + return std::make_unique(env, *coreSource.as(), frontend); + } else if (coreSource.is()) { + return std::make_unique(env, *coreSource.as(), frontend); + } else if (coreSource.is()) { + return std::make_unique(env, *coreSource.as(), frontend); + } else { + return std::make_unique(env, coreSource, frontend); + } + } + + const jni::Object& Source::peerForCoreSource(jni::JNIEnv& env, mbgl::style::Source& coreSource, AndroidRendererFrontend& frontend) { + if (!coreSource.peer.has_value()) { + coreSource.peer = createSourcePeer(env, coreSource, frontend); + } + return coreSource.peer.get>()->javaPeer; + } + + Source::Source(jni::JNIEnv& env, mbgl::style::Source& coreSource, const jni::Object& obj, AndroidRendererFrontend& frontend) + : source(coreSource) + , javaPeer(jni::NewGlobal(env, obj)) + , rendererFrontend(&frontend) { + } + + Source::Source(jni::JNIEnv&, std::unique_ptr coreSource) + : ownedSource(std::move(coreSource)) + , source(*ownedSource) { + } + + Source::~Source() { + // Before being added to a map, the Java peer owns this C++ peer and cleans + // up after itself correctly through the jni native peer bindings. + // After being added to the map, the ownership is flipped and the C++ peer has a strong reference + // to it's Java peer, preventing the Java peer from being GC'ed. + // In this case, the core source initiates the destruction, which requires releasing the Java peer, + // while also resetting it's nativePtr to 0 to prevent the subsequent GC of the Java peer from + // re-entering this dtor. + if (ownedSource.get() == nullptr && javaPeer.get() != nullptr) { + // Manually clear the java peer + android::UniqueEnv env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*env); + static auto nativePtrField = javaClass.GetField(*env, "nativePtr"); + javaPeer.Set(*env, nativePtrField, (jlong) 0); + javaPeer.reset(); + } + } + + jni::Local Source::getId(jni::JNIEnv& env) { + return jni::Make(env, source.getID()); + } + + jni::Local Source::getAttribution(jni::JNIEnv& env) { + auto attribution = source.getAttribution(); + return attribution ? jni::Make(env, attribution.value()) : jni::Make(env,""); + } + + void Source::addToMap(JNIEnv& env, const jni::Object& obj, mbgl::Map& map, AndroidRendererFrontend& frontend) { + // Check to see if we own the source first + if (!ownedSource) { + throw std::runtime_error("Cannot add source twice"); + } + + // Add source to map and release ownership + map.getStyle().addSource(std::move(ownedSource)); + + // Add peer to core source + source.peer = std::unique_ptr(this); + + // Add strong reference to java source + javaPeer = jni::NewGlobal(env, obj); + + rendererFrontend = &frontend; + } + + bool Source::removeFromMap(JNIEnv&, const jni::Object&, mbgl::Map& map) { + // Cannot remove if not attached yet + if (ownedSource) { + throw std::runtime_error("Cannot remove detached source"); + } + + // Remove the source from the map and take ownership + ownedSource = map.getStyle().removeSource(source.getID()); + + // The source may not be removed if any layers still reference it + return ownedSource != nullptr; + } + + void Source::releaseJavaPeer() { + // We can't release the peer if the source was not removed from the map + if (!ownedSource) { + return; + } + + // Release the peer relationships. These will be re-established when the source is added to a map + assert(ownedSource->peer.has_value()); + ownedSource->peer.get>().release(); + ownedSource->peer = util::peer(); + + // Release the strong reference to the java peer + assert(javaPeer); + javaPeer.release(); + + rendererFrontend = nullptr; + } + + void Source::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer(env, javaClass, "nativePtr", + METHOD(&Source::getId, "nativeGetId"), + METHOD(&Source::getAttribution, "nativeGetAttribution") + ); + + // Register subclasses + GeoJSONSource::registerNative(env); + ImageSource::registerNative(env); + RasterSource::registerNative(env); + UnknownSource::registerNative(env); + VectorSource::registerNative(env); + CustomGeometrySource::registerNative(env); + RasterDEMSource::registerNative(env); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/source.hpp b/src/style/sources/source.hpp new file mode 100644 index 000000000..93b706425 --- /dev/null +++ b/src/style/sources/source.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include "../value.hpp" +#include "../../android_renderer_frontend.hpp" + +#include + +namespace mbgl { +namespace android { + +class Source : private mbgl::util::noncopyable { +public: + + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/sources/Source"; }; + + static void registerNative(jni::JNIEnv&); + + static const jni::Object& peerForCoreSource(jni::JNIEnv&, mbgl::style::Source&, AndroidRendererFrontend&); + + /* + * Called when a Java object is created for a core source that belongs to a map. + */ + Source(jni::JNIEnv&, mbgl::style::Source&, const jni::Object&, AndroidRendererFrontend&); + + /* + * Called when a Java object is created for a new core source that does not belong to a map. + */ + Source(jni::JNIEnv&, std::unique_ptr); + + virtual ~Source(); + + virtual void addToMap(JNIEnv&, const jni::Object&, mbgl::Map&, AndroidRendererFrontend&); + + virtual bool removeFromMap(JNIEnv&, const jni::Object&, mbgl::Map&); + + void releaseJavaPeer(); + + jni::Local getId(jni::JNIEnv&); + + jni::Local getAttribution(jni::JNIEnv&); + +protected: + // Set on newly created sources until added to the map. + std::unique_ptr ownedSource; + + // Raw pointer that is valid at all times. + mbgl::style::Source& source; + + // Set when the source is added to a map. + jni::Global> javaPeer; + + // RendererFrontend pointer is valid only when added to the map. + AndroidRendererFrontend* rendererFrontend { nullptr }; +}; + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/unknown_source.cpp b/src/style/sources/unknown_source.cpp new file mode 100644 index 000000000..b5285a5f9 --- /dev/null +++ b/src/style/sources/unknown_source.cpp @@ -0,0 +1,43 @@ +#include "unknown_source.hpp" + +namespace { + + // Dummy initializer (We don't support initializing this from the JVM) + std::unique_ptr init(jni::JNIEnv&) { + throw std::runtime_error("UnknownSource should not be initialized from the JVM"); + } + +} // namespace + +namespace mbgl { +namespace android { + + UnknownSource::UnknownSource(jni::JNIEnv& env, + mbgl::style::Source& coreSource, + AndroidRendererFrontend& frontend) + : Source(env, coreSource, createJavaPeer(env), frontend) { + } + + jni::Local> UnknownSource::createJavaPeer(jni::JNIEnv& env) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(this)); + } + + void UnknownSource::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + init, + "initialize", + "finalize" + ); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/unknown_source.hpp b/src/style/sources/unknown_source.hpp new file mode 100644 index 000000000..f042e2042 --- /dev/null +++ b/src/style/sources/unknown_source.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "source.hpp" +#include +#include + +namespace mbgl { +namespace android { + +class UnknownSource : public Source { +public: + using SuperTag = Source; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/sources/UnknownSource"; }; + + static void registerNative(jni::JNIEnv&); + + UnknownSource(jni::JNIEnv&, mbgl::style::Source&, AndroidRendererFrontend&); + + ~UnknownSource() = default; + +private: + jni::Local> createJavaPeer(jni::JNIEnv&); + +}; // class UnknownSource + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/vector_source.cpp b/src/style/sources/vector_source.cpp new file mode 100644 index 000000000..e46fc1a94 --- /dev/null +++ b/src/style/sources/vector_source.cpp @@ -0,0 +1,84 @@ +#include "vector_source.hpp" + +#include + +// Java -> C++ conversion +#include "../android_conversion.hpp" +#include "../conversion/filter.hpp" + +// C++ -> Java conversion +#include "../../conversion/conversion.hpp" +#include "../../conversion/collection.hpp" +#include "../../geojson/feature.hpp" +#include "../conversion/url_or_tileset.hpp" + +#include + +#include +#include + +namespace mbgl { +namespace android { + + VectorSource::VectorSource(jni::JNIEnv& env, const jni::String& sourceId, const jni::Object<>& urlOrTileSet) + : Source( + env, + std::make_unique( + jni::Make(env, sourceId), + convertURLOrTileset(Value(env, urlOrTileSet)) + ) + ) { + } + + VectorSource::VectorSource(jni::JNIEnv& env, + mbgl::style::Source& coreSource, + AndroidRendererFrontend& frontend) + : Source(env, coreSource, createJavaPeer(env), frontend) { + } + + VectorSource::~VectorSource() = default; + + jni::Local VectorSource::getURL(jni::JNIEnv& env) { + optional url = source.as()->VectorSource::getURL(); + return url ? jni::Make(env, *url) : jni::Local(); + } + + jni::Local>> VectorSource::querySourceFeatures(jni::JNIEnv& env, + const jni::Array& jSourceLayerIds, + const jni::Array>& jfilter) { + using namespace mbgl::android::conversion; + using namespace mbgl::android::geojson; + + std::vector features; + if (rendererFrontend) { + features = rendererFrontend->querySourceFeatures(source.getID(), + { toVector(env, jSourceLayerIds), toFilter(env, jfilter) }); + } + return Feature::convert(env, features); + } + + jni::Local> VectorSource::createJavaPeer(jni::JNIEnv& env) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, reinterpret_cast(this)); + } + + void VectorSource::registerNative(jni::JNIEnv& env) { + // Lookup the class + static auto& javaClass = jni::Class::Singleton(env); + + #define METHOD(MethodPtr, name) jni::MakeNativePeerMethod(name) + + // Register the peer + jni::RegisterNativePeer( + env, javaClass, "nativePtr", + jni::MakePeer&>, + "initialize", + "finalize", + METHOD(&VectorSource::querySourceFeatures, "querySourceFeatures"), + METHOD(&VectorSource::getURL, "nativeGetUrl") + ); + } + +} // namespace android +} // namespace mbgl diff --git a/src/style/sources/vector_source.hpp b/src/style/sources/vector_source.hpp new file mode 100644 index 000000000..4cb46c6c6 --- /dev/null +++ b/src/style/sources/vector_source.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "source.hpp" +#include +#include "../../geojson/feature.hpp" +#include + +namespace mbgl { +namespace android { + +class VectorSource : public Source { +public: + using SuperTag = Source; + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/sources/VectorSource"; }; + + static void registerNative(jni::JNIEnv&); + + VectorSource(jni::JNIEnv&, const jni::String&, const jni::Object<>&); + VectorSource(jni::JNIEnv&, mbgl::style::Source&, AndroidRendererFrontend&); + ~VectorSource(); + +private: + jni::Local>> querySourceFeatures(jni::JNIEnv&, const jni::Array&, + const jni::Array>& jfilter); + + jni::Local getURL(jni::JNIEnv&); + + jni::Local> createJavaPeer(jni::JNIEnv&); + +}; // class VectorSource + +} // namespace android +} // namespace mbgl diff --git a/src/style/transition_options.cpp b/src/style/transition_options.cpp new file mode 100644 index 000000000..d70a04e0d --- /dev/null +++ b/src/style/transition_options.cpp @@ -0,0 +1,35 @@ +#include "transition_options.hpp" + +namespace mbgl { +namespace android { + +jni::Local> TransitionOptions::fromTransitionOptions(jni::JNIEnv& env, jlong duration, jlong delay, jboolean enablePlacementTransitions) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jlong, jlong, jboolean)>(env, "fromTransitionOptions"); + return javaClass.Call(env, method, duration, delay, enablePlacementTransitions); +} + +long TransitionOptions::getDuration(jni::JNIEnv& env, const jni::Object& transitionOptions) { + static auto& javaClass = jni::Class::Singleton(env); + static auto field = javaClass.GetField(env, "duration"); + return transitionOptions.Get(env, field); +} + +long TransitionOptions::getDelay(jni::JNIEnv& env, const jni::Object& transitionOptions) { + static auto& javaClass = jni::Class::Singleton(env); + static auto field = javaClass.GetField(env, "delay"); + return transitionOptions.Get(env, field); +} + +bool TransitionOptions::isEnablePlacementTransitions(jni::JNIEnv& env, const jni::Object& transitionOptions) { + static auto& javaClass = jni::Class::Singleton(env); + static auto field = javaClass.GetField(env, "enablePlacementTransitions"); + return transitionOptions.Get(env, field); +} + +void TransitionOptions::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/style/transition_options.hpp b/src/style/transition_options.hpp new file mode 100644 index 000000000..58656a946 --- /dev/null +++ b/src/style/transition_options.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include + +namespace mbgl { +namespace android { + +class TransitionOptions : private mbgl::util::noncopyable { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/style/layers/TransitionOptions"; }; + + static jni::Local> fromTransitionOptions(jni::JNIEnv&, jlong duration, jlong delay, jboolean enablePlacementTransitions); + + static long getDuration(jni::JNIEnv&, const jni::Object&); + + static long getDelay(jni::JNIEnv&, const jni::Object&); + + static bool isEnablePlacementTransitions(jni::JNIEnv&, const jni::Object&); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace android +} // namespace mbgl \ No newline at end of file diff --git a/src/style/value.cpp b/src/style/value.cpp new file mode 100644 index 000000000..2f0484072 --- /dev/null +++ b/src/style/value.cpp @@ -0,0 +1,83 @@ +#include "value.hpp" + +#include "../java_types.hpp" + +namespace mbgl { +namespace android { + // Instance + + Value::Value(jni::JNIEnv& _env, jni::Local> _value) + : env(_env), + value(std::move(_value)) {} + + Value::Value(jni::JNIEnv& _env, const jni::Object<>& _value) + : env(_env), + value(jni::NewLocal(_env, _value)) {} + + bool Value::isNull() const { + return !value; + } + + bool Value::isArray() const { + return jni::IsInstanceOf(env, value.get(), *java::ObjectArray::jclass); + } + + bool Value::isObject() const { + return jni::IsInstanceOf(env, value.get(), *java::Map::jclass); + } + + bool Value::isString() const { + return jni::IsInstanceOf(env, value.get(), *java::String::jclass); + } + + bool Value::isBool() const { + return jni::IsInstanceOf(env, value.get(), *java::Boolean::jclass); + } + + bool Value::isNumber() const { + return jni::IsInstanceOf(env, value.get(), *java::Number::jclass); + } + + std::string Value::toString() const { + return jni::Make(env, jni::Cast(env, jni::Class::Singleton(env), value)); + } + + float Value::toFloat() const { + return jni::CallMethod(env, value.get(), *java::Number::floatValueMethodId); + } + + double Value::toDouble() const { + return jni::CallMethod(env, value.get(), *java::Number::doubleValueMethodId); + } + + long Value::toLong() const { + return jni::CallMethod(env, value.get(), *java::Number::longValueMethodId); + } + + bool Value::toBool() const { + return jni::CallMethod(env, value.get(), *java::Boolean::booleanValueMethodId); + } + + Value Value::get(const char* key) const { + jni::jobject* member = jni::CallMethod(env, value.get(), *java::Map::getMethodId, jni::Make(env, std::string(key)).get()); + return Value(env, jni::Local>(env, member)); + } + + Value Value::keyArray() const{ + jni::jobject* set = jni::CallMethod(env, value.get(), *java::Map::keySetMethodId); + jni::jobject* array = jni::CallMethod(env, set, *java::Set::toArrayMethodId); + return Value(env, jni::Local>(env, array)); + } + + int Value::getLength() const { + auto array = (jni::jarray*) value.get(); + return jni::GetArrayLength(env, *array); + } + + Value Value::get(const int index) const { + auto array = (jni::jarray*) value.get(); + return Value(env, jni::Local>(env, jni::GetObjectArrayElement(env, *array, index))); + } +} +} + diff --git a/src/style/value.hpp b/src/style/value.hpp new file mode 100644 index 000000000..b507c5ed1 --- /dev/null +++ b/src/style/value.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include + +namespace mbgl { +namespace android { + +class Value { +public: + Value(jni::JNIEnv&, jni::Local>); + Value(jni::JNIEnv&, const jni::Object<>&); + + Value(Value&&) = default; + Value& operator=(Value&&) = delete; + + Value(const Value&) = delete; + Value& operator=(const Value&) = delete; + + bool isNull() const; + bool isArray() const; + bool isObject() const; + bool isString() const; + bool isBool() const; + bool isNumber() const; + + std::string toString() const; + float toFloat() const; + double toDouble() const; + long toLong() const; + bool toBool() const; + Value get(const char* key) const; + Value keyArray() const; + int getLength() const; + Value get(const int index ) const; + + jni::JNIEnv& env; + jni::Local> value; +}; + +} +} diff --git a/src/test/benchmark_runner.cpp b/src/test/benchmark_runner.cpp new file mode 100644 index 000000000..321a37a02 --- /dev/null +++ b/src/test/benchmark_runner.cpp @@ -0,0 +1,10 @@ +#include "runtime.hpp" +#include + +int main(int argc, char *argv[]) { + if (!mbgl::android::initRuntime(argc, argv)) { + return 1; + } + + return mbgl::runBenchmark(argc, argv); +} diff --git a/src/test/runtime.cpp b/src/test/runtime.cpp new file mode 100644 index 000000000..1f8f16da3 --- /dev/null +++ b/src/test/runtime.cpp @@ -0,0 +1,74 @@ +#include "runtime.hpp" +#include "../jni.hpp" +#include "../jni_native.hpp" + +#include +#include +#include +#include +#include + +// Required for art / libsigchain +extern "C" JNIEXPORT void EnsureFrontOfChain(int, struct sigaction*) { } +extern "C" JNIEXPORT void AddSpecialSignalHandlerFn(int, void*) { } +extern "C" JNIEXPORT void RemoveSpecialSignalHandlerFn(int, bool (*) (int, siginfo_t*, void*)) { } + +namespace { +const std::string kClassPathCommand{"--class_path="}; +const std::string kClassPath{"-Djava.class.path="}; +const std::string kDefaultDex{"/data/local/tmp/core-tests/classes.dex"}; +} // namespace + +namespace mbgl { +namespace android { + +bool initRuntime(int argc, char *argv[]) { + void* vmHandle = dlopen("libart.so", RTLD_NOW); + assert(vmHandle != nullptr); + + using CreateJavaVMFn = jint (*)(JavaVM** vm, JNIEnv** env, void* vmArgs); + CreateJavaVMFn createJavaVMFn = reinterpret_cast(dlsym(vmHandle, "JNI_CreateJavaVM")); + assert(createJavaVMFn != nullptr); + + std::string classPath = kClassPath + kDefaultDex; + for (int i = 0; i < argc; ++i) { + const std::string arg{argv[i]}; + if (arg.compare(0, kClassPathCommand.length(), kClassPathCommand) == 0) { + classPath = kClassPath + arg.substr(kClassPathCommand.length()); + break; + } + } + + JavaVMOption options[1]; + options[0].optionString = classPath.c_str(); + + JavaVMInitArgs args; + args.version = JNI_VERSION_1_6; + args.nOptions = 1; + args.options = options; + args.ignoreUnrecognized = JNI_FALSE; + + JavaVM* vm; + JNIEnv* env; + + if (createJavaVMFn(&vm, &env, &args) != JNI_OK) { + return false; + } + + void* runtimeHandle = dlopen("libandroid_runtime.so", RTLD_NOW); + assert(runtimeHandle != nullptr); + + using RegisterNativesFn = jint (*)(JNIEnv* env); + RegisterNativesFn registerNativesFn = reinterpret_cast(dlsym(runtimeHandle, "registerFrameworkNatives")); + assert(registerNativesFn != nullptr); + + if (registerNativesFn(env) != JNI_OK) { + return false; + } + + mbgl::android::registerNatives(vm); + return true; +} + +} // namespace android +} // namespace mbgl diff --git a/src/test/runtime.hpp b/src/test/runtime.hpp new file mode 100644 index 000000000..c5e69d61c --- /dev/null +++ b/src/test/runtime.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace mbgl { +namespace android { + +bool initRuntime(int argc, char *argv[]); + +} // namespace android +} // namespace mbgl diff --git a/src/test/test_runner.cpp b/src/test/test_runner.cpp new file mode 100644 index 000000000..008e3c574 --- /dev/null +++ b/src/test/test_runner.cpp @@ -0,0 +1,10 @@ +#include "runtime.hpp" +#include + +int main(int argc, char *argv[]) { + if (!mbgl::android::initRuntime(argc, argv)) { + return 1; + } + + mbgl::runTests(argc, argv); +} diff --git a/src/text/collator.cpp b/src/text/collator.cpp new file mode 100644 index 000000000..acb4f36ee --- /dev/null +++ b/src/text/collator.cpp @@ -0,0 +1,199 @@ +#include +#include +#include + +#include + +#include + +#include "../attach_env.hpp" +#include "collator_jni.hpp" + +namespace mbgl { +namespace android { + +void Collator::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +jni::Local> Collator::getInstance(jni::JNIEnv& env, const jni::Object& locale) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jni::Object)>(env, "getInstance"); + return javaClass.Call(env, method, locale); +} + +void Collator::setStrength(jni::JNIEnv& env, const jni::Object& collator, jni::jint strength) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "setStrength"); + collator.Call(env, method, strength); +} + +jni::jint Collator::compare(jni::JNIEnv& env, const jni::Object& collator, const jni::String& lhs, const jni::String& rhs) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "compare"); + return collator.Call(env, method, lhs, rhs); +} + +void StringUtils::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +jni::Local StringUtils::unaccent(jni::JNIEnv& env, const jni::String& value) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod(env, "unaccent"); + return javaClass.Call(env, method, value); +} + +void Locale::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +/* +We would prefer to use for/toLanguageTag, but they're only available in API level 21+ + +jni::Object Locale::forLanguageTag(jni::JNIEnv& env, jni::String languageTag) { + using Signature = jni::Object(jni::String); + auto method = javaClass.GetStaticMethod(env, "forLanguageTag"); + return javaClass.Call(env, method, languageTag); +} + +jni::String Locale::toLanguageTag(jni::JNIEnv& env, jni::Object locale) { + using Signature = jni::String(); + auto static method = javaClass.GetMethod(env, "toLanguageTag"); + return locale.Call(env, method); +} +*/ + +jni::Local Locale::getLanguage(jni::JNIEnv& env, const jni::Object& locale) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "getLanguage"); + return locale.Call(env, method); +} + +jni::Local Locale::getCountry(jni::JNIEnv& env, const jni::Object& locale) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "getCountry"); + return locale.Call(env, method); +} + +jni::Local> Locale::getDefault(jni::JNIEnv& env) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod ()>(env, "getDefault"); + return javaClass.Call(env, method); +} + +jni::Local> Locale::New(jni::JNIEnv& env, const jni::String& language) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, language); +} + +jni::Local> Locale::New(jni::JNIEnv& env, const jni::String& language, const jni::String& region) { + static auto& javaClass = jni::Class::Singleton(env); + static auto constructor = javaClass.GetConstructor(env); + return javaClass.New(env, constructor, language, region); +} + +} // namespace android + +namespace style { +namespace expression { + +class Collator::Impl { +public: + Impl(bool caseSensitive_, bool diacriticSensitive_, optional locale_) + : caseSensitive(caseSensitive_) + , diacriticSensitive(diacriticSensitive_) + , env(android::AttachEnv()) + { + LanguageTag languageTag = locale_ ? LanguageTag::fromBCP47(*locale_) : LanguageTag(); + if (!languageTag.language) { + locale = jni::NewGlobal(*env, + android::Locale::getDefault(*env)); + } else if (!languageTag.region) { + locale = jni::NewGlobal(*env, + android::Locale::New(*env, jni::Make(*env, *languageTag.language))); + } else { + locale = jni::NewGlobal(*env, + android::Locale::New(*env, jni::Make(*env, *languageTag.language), + jni::Make(*env, *languageTag.region))); + } + collator = jni::NewGlobal(*env, android::Collator::getInstance(*env, locale)); + if (!diacriticSensitive && !caseSensitive) { + android::Collator::setStrength(*env, collator, 0 /*PRIMARY*/); + } else if (diacriticSensitive && !caseSensitive) { + android::Collator::setStrength(*env, collator, 1 /*SECONDARY*/); + } else if (caseSensitive) { + // If we're case-sensitive and diacritic-sensitive, we use a case-sensitive collator + // and a fallback implementation of diacritic-insensitivity. + android::Collator::setStrength(*env, collator, 2 /*TERTIARY*/); + } + } + + bool operator==(const Impl& other) const { + return caseSensitive == other.caseSensitive && + diacriticSensitive == other.diacriticSensitive && + resolvedLocale() == other.resolvedLocale(); + } + + int compare(const std::string& lhs, const std::string& rhs) const { + bool useUnaccent = !diacriticSensitive && caseSensitive; + // java.text.Collator doesn't support a diacritic-insensitive/case-sensitive collation + // order, so we have to compromise here. We use Android's case-sensitive Collator + // against strings that have been "unaccented" using non-locale-aware nunicode logic. + // Because of the difference in locale-awareness, this means turning on case-sensitivity + // can _potentially_ change compare results for strings that don't actually have any case + // differences. + jni::Local jlhs = jni::Make(*env, useUnaccent ? + platform::unaccent(lhs) : + lhs); + jni::Local jrhs = jni::Make(*env, useUnaccent ? + platform::unaccent(rhs) : + rhs); + + jni::jint result = android::Collator::compare(*env, collator, jlhs, jrhs); + + return result; + } + + std::string resolvedLocale() const { + std::string language = jni::Make(*env, android::Locale::getLanguage(*env, locale)); + std::string region = jni::Make(*env, android::Locale::getCountry(*env, locale)); + + optional resultLanguage; + if (!language.empty()) resultLanguage = language; + optional resultRegion; + if (!region.empty()) resultRegion = region; + + return LanguageTag(resultLanguage, {}, resultRegion).toBCP47(); + } + +private: + bool caseSensitive; + bool diacriticSensitive; + + android::UniqueEnv env; + jni::Global> collator; + jni::Global> locale; +}; + + +Collator::Collator(bool caseSensitive, bool diacriticSensitive, optional locale_) + : impl(std::make_shared(caseSensitive, diacriticSensitive, std::move(locale_))) +{} + +bool Collator::operator==(const Collator& other) const { + return *impl == *(other.impl); +} + +int Collator::compare(const std::string& lhs, const std::string& rhs) const { + return impl->compare(lhs, rhs); +} + +std::string Collator::resolvedLocale() const { + return impl->resolvedLocale(); +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/text/collator_jni.hpp b/src/text/collator_jni.hpp new file mode 100644 index 000000000..dd3f84566 --- /dev/null +++ b/src/text/collator_jni.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include + +#include + +/* + android::Collator and android::Locale are + the JNI wrappers of + java/text/Collator and java/util/Locale + + mbgl::Collator is the portable interface + Both implementations are in collator.cpp + */ + +namespace mbgl { +namespace android { + +class Locale { +public: + static constexpr auto Name() { return "java/util/Locale"; }; + + /* Requires API level 21+ in order to support script/variant + static jni::Object forLanguageTag(jni::JNIEnv&, jni::String); + static jni::String toLanguageTag(jni::JNIEnv&, jni::Object); + */ + static jni::Local> getDefault(jni::JNIEnv&); + static jni::Local getLanguage(jni::JNIEnv&, const jni::Object&); + static jni::Local getCountry(jni::JNIEnv&, const jni::Object&); + + static jni::Local> New(jni::JNIEnv&, const jni::String&); + static jni::Local> New(jni::JNIEnv&, const jni::String&, const jni::String&); + + + static void registerNative(jni::JNIEnv&); +}; + +class Collator { +public: + static constexpr auto Name() { return "java/text/Collator"; }; + + static jni::Local> getInstance(jni::JNIEnv&, const jni::Object&); + + static void setStrength(jni::JNIEnv&, const jni::Object&, jni::jint); + + static jni::jint compare(jni::JNIEnv&, const jni::Object&, const jni::String&, const jni::String&); + + + static void registerNative(jni::JNIEnv&); +}; + + +class StringUtils { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/utils/StringUtils"; }; + + static jni::Local unaccent(jni::JNIEnv&, const jni::String&); + + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace android +} // namespace mbgl diff --git a/src/text/format_number.cpp b/src/text/format_number.cpp new file mode 100644 index 000000000..3a41175ec --- /dev/null +++ b/src/text/format_number.cpp @@ -0,0 +1,81 @@ +#include +#include +#include + +#include + +#include "../attach_env.hpp" +#include "format_number_jni.hpp" + +namespace mbgl { +namespace android { + +void NumberFormat::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +jni::Local> NumberFormat::getInstance(jni::JNIEnv& env, const jni::Object& locale) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jni::Object)>(env, "getInstance"); + return javaClass.Call(env, method, locale); +} + +jni::Local> NumberFormat::getCurrencyInstance(jni::JNIEnv& env, const jni::Object& locale) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetStaticMethod (jni::Object)>(env, "getCurrencyInstance"); + return javaClass.Call(env, method, locale); +} + +jni::Local NumberFormat::format(jni::JNIEnv& env, const jni::Object& nf, jni::jdouble number) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "format"); + return nf.Call(env, method, number); +} + +void NumberFormat::setMinimumFractionDigits(jni::JNIEnv& env, const jni::Object& nf, jni::jint value) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "setMinimumFractionDigits"); + return nf.Call(env, method, value); +} + +void NumberFormat::setMaximumFractionDigits(jni::JNIEnv& env, const jni::Object& nf, jni::jint value) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "setMaximumFractionDigits"); + return nf.Call(env, method, value); +} + +} // namespace android + +namespace platform { + +std::string formatNumber(double number, const std::string& localeId, const std::string& currency, + uint8_t minFractionDigits, uint8_t maxFractionDigits) { + + auto env{ android::AttachEnv() }; + + jni::Global> locale; + LanguageTag languageTag = !localeId.empty() ? LanguageTag::fromBCP47(localeId) : LanguageTag(); + if (!languageTag.language) { + locale = jni::NewGlobal(*env, android::Locale::getDefault(*env)); + } else if (!languageTag.region) { + locale = jni::NewGlobal(*env, android::Locale::New(*env, jni::Make(*env, *languageTag.language))); + } else { + locale = jni::NewGlobal(*env, android::Locale::New(*env, jni::Make(*env, *languageTag.language), + jni::Make(*env, *languageTag.region))); + } + + jni::Global> formatter; + if (currency.empty()) { + formatter = jni::NewGlobal(*env, android::NumberFormat::getInstance(*env, locale)); + android::NumberFormat::setMinimumFractionDigits(*env, formatter, static_cast(minFractionDigits)); + android::NumberFormat::setMaximumFractionDigits(*env, formatter, static_cast(maxFractionDigits)); + } else { + formatter = jni::NewGlobal(*env, android::NumberFormat::getCurrencyInstance(*env, locale)); + } + + auto result = android::NumberFormat::format(*env, formatter, static_cast(number)); + return jni::Make(*env, result); +} + +} // namespace platform +} // namespace mbgl diff --git a/src/text/format_number_jni.hpp b/src/text/format_number_jni.hpp new file mode 100644 index 000000000..172003892 --- /dev/null +++ b/src/text/format_number_jni.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "collator_jni.hpp" + +/* + android::NumberFormat is the JNI wrapper + of java/text/NumberFormat. + */ + +namespace mbgl { +namespace android { + +class NumberFormat { +public: + static constexpr auto Name() { return "java/text/NumberFormat"; }; + + static jni::Local> getInstance(jni::JNIEnv&, const jni::Object&); + static jni::Local> getCurrencyInstance(jni::JNIEnv&, const jni::Object&); + static jni::Local format(jni::JNIEnv&, const jni::Object&, jni::jdouble); + static void setMinimumFractionDigits(jni::JNIEnv&, const jni::Object&, jni::jint); + static void setMaximumFractionDigits(jni::JNIEnv&, const jni::Object&, jni::jint); + + static void registerNative(jni::JNIEnv&); +}; + +} // namespace android +} // namespace mbgl diff --git a/src/text/local_glyph_rasterizer.cpp b/src/text/local_glyph_rasterizer.cpp new file mode 100644 index 000000000..8892ee3f3 --- /dev/null +++ b/src/text/local_glyph_rasterizer.cpp @@ -0,0 +1,130 @@ +#include +#include +#include + +#include + +#include "../attach_env.hpp" +#include "../bitmap.hpp" + +#include "local_glyph_rasterizer_jni.hpp" + +/* + Android implementation of LocalGlyphRasterizer: + Draws CJK glyphs using locally available fonts. + + Follows pattern of GL JS implementation in that: + - Only CJK glyphs are drawn locally (because we can guess their metrics effectively) + * Render size/metrics determined experimentally using Noto Sans + - Configuration is done at map creation time by setting a "font family" + * JS uses a CSS font-family, this uses android.graphics.Typeface + https://developer.android.com/reference/android/graphics/Typeface.html + - We use heuristics to extract a font-weight based on the incoming font stack + * JS tries to extract multiple weights, this implementation only looks for + "bold" + + mbgl::LocalGlyphRasterizer is the portable interface + mbgl::LocalGlyphRasterizer::Impl stores platform-specific configuration data + mbgl::android::LocalGlyphRasterizer is the JNI wrapper + com.mapbox.mapboxsdk.text.LocalGlyphRasterizer is the Java implementation that + actually does the drawing + */ + +namespace mbgl { +namespace android { + +LocalGlyphRasterizer::LocalGlyphRasterizer() { + UniqueEnv env { AttachEnv() }; + + static auto& javaClass = jni::Class::Singleton(*env); + static auto constructor = javaClass.GetConstructor(*env); + + javaObject = jni::NewGlobal(*env, javaClass.New(*env, constructor)); +} + +PremultipliedImage LocalGlyphRasterizer::drawGlyphBitmap(const std::string& fontFamily, const bool bold, const GlyphID glyphID) { + UniqueEnv env { AttachEnv() }; + + static auto& javaClass = jni::Class::Singleton(*env); + static auto drawGlyphBitmap = javaClass.GetMethod (jni::String, jni::jboolean, jni::jchar)>(*env, "drawGlyphBitmap"); + + return Bitmap::GetImage(*env, + javaObject.Call(*env, + drawGlyphBitmap, + jni::Make(*env, fontFamily), + static_cast(bold), + static_cast(glyphID))); +} + +void LocalGlyphRasterizer::registerNative(jni::JNIEnv& env) { + jni::Class::Singleton(env); +} + +} // namespace android + +class LocalGlyphRasterizer::Impl { +public: + Impl(const optional fontFamily_) + : fontFamily(fontFamily_) + {} + + bool isConfigured() const { + return bool(fontFamily); + } + + PremultipliedImage drawGlyphBitmap(const FontStack& fontStack, GlyphID glyphID) { + bool bold = false; + for (auto font : fontStack) { + std::string lowercaseFont = platform::lowercase(font); + if (lowercaseFont.find("bold") != std::string::npos) { + bold = true; + break; + } + } + return androidLocalGlyphRasterizer.drawGlyphBitmap(*fontFamily, bold, glyphID); + } + +private: + optional fontFamily; + android::LocalGlyphRasterizer androidLocalGlyphRasterizer; +}; + +LocalGlyphRasterizer::LocalGlyphRasterizer(const optional fontFamily) + : impl(std::make_unique(fontFamily)) +{} + +LocalGlyphRasterizer::~LocalGlyphRasterizer() +{} + +bool LocalGlyphRasterizer::canRasterizeGlyph(const FontStack&, GlyphID glyphID) { + return util::i18n::allowsFixedWidthGlyphGeneration(glyphID) && impl->isConfigured(); +} + +Glyph LocalGlyphRasterizer::rasterizeGlyph(const FontStack& fontStack, GlyphID glyphID) { + Glyph fixedMetrics; + if (!impl->isConfigured()) { + return fixedMetrics; + } + + fixedMetrics.id = glyphID; + + Size size(35, 35); + + fixedMetrics.metrics.width = size.width; + fixedMetrics.metrics.height = size.height; + fixedMetrics.metrics.left = 3; + fixedMetrics.metrics.top = -10; + fixedMetrics.metrics.advance = 24; + + PremultipliedImage rgbaBitmap = impl->drawGlyphBitmap(fontStack, glyphID); + + // Copy alpha values from RGBA bitmap into the AlphaImage output + fixedMetrics.bitmap = AlphaImage(size); + for (uint32_t i = 0; i < size.width * size.height; i++) { + fixedMetrics.bitmap.data[i] = rgbaBitmap.data[4 * i + 3]; + } + + return fixedMetrics; +} + +} // namespace mbgl diff --git a/src/text/local_glyph_rasterizer_jni.hpp b/src/text/local_glyph_rasterizer_jni.hpp new file mode 100644 index 000000000..e80e84833 --- /dev/null +++ b/src/text/local_glyph_rasterizer_jni.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include + +/* + android::LocalGlyphRasterizer is the JNI wrapper of + com/mapbox/mapboxsdk/text/LocalGlyphRasterizer + + mbgl::LocalGlyphRasterizer is the portable interface + Both implementations are in local_glyph_rasterizer.cpp + */ + +namespace mbgl { +namespace android { + +class LocalGlyphRasterizer { +public: + static constexpr auto Name() { return "com/mapbox/mapboxsdk/text/LocalGlyphRasterizer"; }; + + static void registerNative(jni::JNIEnv&); + + LocalGlyphRasterizer(); + + PremultipliedImage drawGlyphBitmap(const std::string& fontFamily, const bool bold, const char16_t glyphID); + +private: + jni::Global> javaObject; +}; + +} // namespace android +} // namespace mbgl diff --git a/src/thread.cpp b/src/thread.cpp new file mode 100644 index 000000000..cd0a72306 --- /dev/null +++ b/src/thread.cpp @@ -0,0 +1,56 @@ +#include +#include +#include + +#include +#include + +#include +#include "jni.hpp" + +// Implementation based on Chromium's platform_thread_android.cc. + +namespace mbgl { +namespace platform { + +thread_local static JNIEnv* env; +thread_local static bool detach; + +std::string getCurrentThreadName() { + char name[32] = "unknown"; + + if (prctl(PR_GET_NAME, name) == -1) { + Log::Warning(Event::General, "Couldn't get thread name"); + } + + return name; +} + +void setCurrentThreadName(const std::string& name) { + if (prctl(PR_SET_NAME, name.c_str()) == -1) { + Log::Warning(Event::General, "Couldn't set thread name"); + } +} + +void makeThreadLowPriority() { + // ANDROID_PRIORITY_LOWEST = 19 + // + // Supposedly would set the priority for the whole process, but + // on Linux/Android it only sets for the current thread. + setpriority(PRIO_PROCESS, 0, 19); +} + +void attachThread() { + using namespace android; + assert(env == nullptr); + detach = attach_jni_thread(theJVM, &env, platform::getCurrentThreadName()); +} + +void detachThread() { + using namespace android; + assert(env); + detach_jni_thread(theJVM, &env, detach); +} + +} // namespace platform +} // namespace mbgl diff --git a/src/timer.cpp b/src/timer.cpp new file mode 100644 index 000000000..a45c48702 --- /dev/null +++ b/src/timer.cpp @@ -0,0 +1,75 @@ +#include "run_loop_impl.hpp" + +#include +#include + +#include + +namespace mbgl { +namespace util { + +class Timer::Impl : public RunLoop::Impl::Runnable { +public: + Impl() = default; + + ~Impl() { + stop(); + } + + void start(Duration timeout, Duration repeat_, std::function&& task_) { + stop(); + + repeat = repeat_; + task = std::move(task_); + // Prevent overflows when timeout is set to Duration::max() + due = (timeout == Duration::max()) ? std::chrono::time_point::max() : Clock::now() + + timeout; + loop->addRunnable(this); + } + + void stop() { + loop->removeRunnable(this); + } + + void reschedule() { + if (repeat != Duration::zero()) { + due = Clock::now() + repeat; + loop->wake(); + } else { + stop(); + } + } + + TimePoint dueTime() const override { + return due; + } + + void runTask() override { + reschedule(); + task(); + } + +private: + TimePoint due; + Duration repeat; + + RunLoop::Impl* loop = reinterpret_cast(RunLoop::getLoopHandle()); + + std::function task; +}; + +Timer::Timer() : impl(std::make_unique()) { +} + +Timer::~Timer() = default; + +void Timer::start(Duration timeout, Duration repeat, std::function&& cb) { + impl->start(timeout, repeat, std::move(cb)); +} + +void Timer::stop() { + impl->stop(); +} + +} // namespace util +} // namespace mbgl diff --git a/src/unaccent.cpp b/src/unaccent.cpp new file mode 100644 index 000000000..8da0ce493 --- /dev/null +++ b/src/unaccent.cpp @@ -0,0 +1,18 @@ +#include +#include +#include "attach_env.hpp" +#include "text/collator_jni.hpp" +#include + +namespace mbgl { +namespace platform { + +std::string unaccent(const std::string& str) { + android::UniqueEnv env = android::AttachEnv(); + jni::Local input = jni::Make(*env, str); + jni::Local unaccented = android::StringUtils::unaccent(*env, input); + return jni::Make(*env, unaccented); +} + +} // namespace platform +} // namespace mbgl diff --git a/vendor/mapbox-events-android b/vendor/mapbox-events-android new file mode 160000 index 000000000..58fbec16c --- /dev/null +++ b/vendor/mapbox-events-android @@ -0,0 +1 @@ +Subproject commit 58fbec16cdedb909eb63c68e46708fe9268b6fed diff --git a/vendor/mapbox-gestures-android b/vendor/mapbox-gestures-android new file mode 160000 index 000000000..ae08de82c --- /dev/null +++ b/vendor/mapbox-gestures-android @@ -0,0 +1 @@ +Subproject commit ae08de82ca178dca7f005dfc8056554cf77124ad diff --git a/vendor/mapbox-gl-native b/vendor/mapbox-gl-native new file mode 160000 index 000000000..89eaac05e --- /dev/null +++ b/vendor/mapbox-gl-native @@ -0,0 +1 @@ +Subproject commit 89eaac05ebc2928e59d3f0404f7b92bdb42b92bb diff --git a/vendor/mapbox-java b/vendor/mapbox-java new file mode 160000 index 000000000..84de2d4b0 --- /dev/null +++ b/vendor/mapbox-java @@ -0,0 +1 @@ +Subproject commit 84de2d4b02e93a9eda9d0254332cd356d17445de