diff options
Diffstat (limited to 'platform')
107 files changed, 4892 insertions, 801 deletions
diff --git a/platform/android/CHANGELOG.md b/platform/android/CHANGELOG.md index ff03df2236..b3b41ecbd1 100644 --- a/platform/android/CHANGELOG.md +++ b/platform/android/CHANGELOG.md @@ -4,11 +4,110 @@ Mapbox welcomes participation and contributions from everyone. If you'd like to ## 5.2.0 - TBA -* 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)) -* 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)) +## 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 +## 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) @@ -101,7 +200,7 @@ Mapbox welcomes participation and contributions from everyone. If you'd like to * 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) +* 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) @@ -503,12 +602,6 @@ Mapbox Android 4.0.0 is the most ambitious Android release to date with 3 major - 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. - -Known issues: - -- Black Screen On Ice Cream Sandwich and Jelly Bean devices ([#2802](https://github.com/mapbox/mapbox-gl-native/issues/2802)) - - Resolved in 2.2.0 diff --git a/platform/android/MapboxGLAndroidSDK/build.gradle b/platform/android/MapboxGLAndroidSDK/build.gradle index 688c632326..f3e9433a0b 100644 --- a/platform/android/MapboxGLAndroidSDK/build.gradle +++ b/platform/android/MapboxGLAndroidSDK/build.gradle @@ -2,8 +2,7 @@ apply plugin: 'com.android.library' dependencies { compile rootProject.ext.dep.supportAnnotations - compile rootProject.ext.dep.supportV4 - compile rootProject.ext.dep.supportDesign + compile rootProject.ext.dep.supportFragmentV4 compile rootProject.ext.dep.timber compile rootProject.ext.dep.okhttp3 provided(rootProject.ext.dep.lost) { @@ -21,6 +20,7 @@ dependencies { // Mapbox Android Services (Telemetry support) compile(rootProject.ext.dep.mapboxAndroidTelemetry) { transitive = true + exclude group: 'com.android.support' } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/AndroidManifest.xml b/platform/android/MapboxGLAndroidSDK/src/main/AndroidManifest.xml index 231e36e092..b61035a008 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/AndroidManifest.xml +++ b/platform/android/MapboxGLAndroidSDK/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ <uses-feature android:glEsVersion="0x00020000" android:required="true" /> <uses-feature android:name="android.hardware.wifi" android:required="false" /> <!-- Implied by ACCESS_WIFI_STATE. --> + <uses-feature android:name="android.hardware.location.gps" android:required="false"/> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/LibraryLoader.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/LibraryLoader.java index 8a75176ccd..a024f0ab70 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/LibraryLoader.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/LibraryLoader.java @@ -1,5 +1,7 @@ package com.mapbox.mapboxsdk; +import timber.log.Timber; + /** * Centralises the knowledge about "mapbox-gl" library loading. */ @@ -9,7 +11,10 @@ public class LibraryLoader { * Loads "libmapbox-gl.so" native shared library. */ public static void load() { - System.loadLibrary("mapbox-gl"); + try { + System.loadLibrary("mapbox-gl"); + } catch (UnsatisfiedLinkError error) { + Timber.e(error, "Failed to load native shared library."); + } } - } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java index 97a9ea94ee..fc448ccf7b 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/MapboxConstants.java @@ -80,12 +80,12 @@ public class MapboxConstants { /** * The currently used minimun scale factor to clamp to when a quick zoom gesture occurs */ - public static final float MINIMUM_SCALE_FACTOR_CLAMP = 0.65f; + 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 */ - public static final float MAXIMUM_SCALE_FACTOR_CLAMP = 1.35f; + public static final float MAXIMUM_SCALE_FACTOR_CLAMP = 0.45f; /** * Fragment Argument Key for MapboxMapOptions diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HTTPRequest.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HTTPRequest.java index e2626a026b..32aa250997 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HTTPRequest.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/http/HTTPRequest.java @@ -85,7 +85,14 @@ class HTTPRequest implements Callback { } mRequest = builder.build(); mCall = mClient.newCall(mRequest); - mCall.enqueue(this); + + // TODO remove code block for workaround in #10303 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) { + mCall.enqueue(this); + } else { + // Calling execute instead of enqueue is a workaround for #10303 + onResponse(mCall, mCall.execute()); + } } catch (Exception exception) { onFailure(exception); } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationManager.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationManager.java index c09c926eb5..9f256c341b 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationManager.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AnnotationManager.java @@ -21,7 +21,6 @@ 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.services.commons.geojson.Feature; import java.util.ArrayList; import java.util.List; @@ -41,7 +40,6 @@ import timber.log.Timber; */ class AnnotationManager { - private static final String LAYER_ID_SHAPE_ANNOTATIONS = "com.mapbox.annotations.shape."; private static final long NO_ANNOTATION_ID = -1; private final MapView mapView; @@ -50,7 +48,6 @@ class AnnotationManager { private final MarkerViewManager markerViewManager; private final LongSparseArray<Annotation> annotationsArray; private final List<Marker> selectedMarkers = new ArrayList<>(); - private final List<String> shapeAnnotationIds = new ArrayList<>(); private MapboxMap mapboxMap; private MapboxMap.OnMarkerClickListener onMarkerClickListener; @@ -58,13 +55,14 @@ class AnnotationManager { private MapboxMap.OnPolylineClickListener onPolylineClickListener; private Annotations annotations; + private ShapeAnnotations shapeAnnotations; private Markers markers; private Polygons polygons; private Polylines polylines; AnnotationManager(NativeMapView view, MapView mapView, LongSparseArray<Annotation> annotationsArray, MarkerViewManager markerViewManager, IconManager iconManager, Annotations annotations, - Markers markers, Polygons polygons, Polylines polylines) { + Markers markers, Polygons polygons, Polylines polylines, ShapeAnnotations shapeAnnotations) { this.mapView = mapView; this.annotationsArray = annotationsArray; this.markerViewManager = markerViewManager; @@ -73,6 +71,7 @@ class AnnotationManager { this.markers = markers; this.polygons = polygons; this.polylines = polylines; + this.shapeAnnotations = shapeAnnotations; if (view != null) { // null checking needed for unit tests view.addOnMapChangedListener(markerViewManager); @@ -122,9 +121,6 @@ class AnnotationManager { // do icon cleanup iconManager.iconCleanup(marker.getIcon()); } - } else { - // instanceOf Polygon/Polyline - shapeAnnotationIds.remove(annotation.getId()); } annotations.removeBy(annotation); } @@ -143,9 +139,6 @@ class AnnotationManager { } else { iconManager.iconCleanup(marker.getIcon()); } - } else { - // instanceOf Polygon/Polyline - shapeAnnotationIds.remove(annotation.getId()); } } annotations.removeBy(annotationList); @@ -167,9 +160,6 @@ class AnnotationManager { } else { iconManager.iconCleanup(marker.getIcon()); } - } else { - // instanceOf Polygon/Polyline - shapeAnnotationIds.remove(annotation.getId()); } } annotations.removeAll(); @@ -227,17 +217,11 @@ class AnnotationManager { // Polygon addPolygon(@NonNull PolygonOptions polygonOptions, @NonNull MapboxMap mapboxMap) { - Polygon polygon = polygons.addBy(polygonOptions, mapboxMap); - shapeAnnotationIds.add(LAYER_ID_SHAPE_ANNOTATIONS + polygon.getId()); - return polygon; + return polygons.addBy(polygonOptions, mapboxMap); } List<Polygon> addPolygons(@NonNull List<PolygonOptions> polygonOptionsList, @NonNull MapboxMap mapboxMap) { - List<Polygon> polygonList = polygons.addBy(polygonOptionsList, mapboxMap); - for (Polygon polygon : polygonList) { - shapeAnnotationIds.add(LAYER_ID_SHAPE_ANNOTATIONS + polygon.getId()); - } - return polygonList; + return polygons.addBy(polygonOptionsList, mapboxMap); } void updatePolygon(Polygon polygon) { @@ -257,17 +241,11 @@ class AnnotationManager { // Polyline addPolyline(@NonNull PolylineOptions polylineOptions, @NonNull MapboxMap mapboxMap) { - Polyline polyline = polylines.addBy(polylineOptions, mapboxMap); - shapeAnnotationIds.add(LAYER_ID_SHAPE_ANNOTATIONS + polyline.getId()); - return polyline; + return polylines.addBy(polylineOptions, mapboxMap); } List<Polyline> addPolylines(@NonNull List<PolylineOptions> polylineOptionsList, @NonNull MapboxMap mapboxMap) { - List<Polyline> polylineList = polylines.addBy(polylineOptionsList, mapboxMap); - for (Polyline polyline : polylineList) { - shapeAnnotationIds.add(LAYER_ID_SHAPE_ANNOTATIONS + polyline.getId()); - } - return polylineList; + return polylines.addBy(polylineOptionsList, mapboxMap); } void updatePolyline(Polyline polyline) { @@ -397,11 +375,11 @@ class AnnotationManager { // boolean onTap(PointF tapPoint) { - if (!shapeAnnotationIds.isEmpty()) { - ShapeAnnotationHit shapeAnnotationHit = getShapeAnnotationHitFromTap(tapPoint); - long shapeAnnotationId = new ShapeAnnotationHitResolver(mapboxMap).execute(shapeAnnotationHit); - if (shapeAnnotationId != NO_ANNOTATION_ID) { - handleClickForShapeAnnotation(shapeAnnotationId); + ShapeAnnotationHit shapeAnnotationHit = getShapeAnnotationHitFromTap(tapPoint); + Annotation annotation = new ShapeAnnotationHitResolver(shapeAnnotations).execute(shapeAnnotationHit); + if (annotation != null) { + if (handleClickForShapeAnnotation(annotation)) { + return true; } } @@ -418,16 +396,18 @@ class AnnotationManager { tapPoint.x + touchTargetSide, tapPoint.y + touchTargetSide ); - return new ShapeAnnotationHit(tapRect, shapeAnnotationIds.toArray(new String[shapeAnnotationIds.size()])); + return new ShapeAnnotationHit(tapRect); } - private void handleClickForShapeAnnotation(long shapeAnnotationId) { - Annotation annotation = getAnnotation(shapeAnnotationId); + 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) { @@ -470,28 +450,19 @@ class AnnotationManager { private static class ShapeAnnotationHitResolver { - private MapboxMap mapboxMap; - - ShapeAnnotationHitResolver(MapboxMap mapboxMap) { - this.mapboxMap = mapboxMap; - } + private ShapeAnnotations shapeAnnotations; - public long execute(ShapeAnnotationHit shapeHit) { - long foundAnnotationId = NO_ANNOTATION_ID; - List<Feature> features = mapboxMap.queryRenderedFeatures(shapeHit.tapPoint, shapeHit.layerIds); - if (!features.isEmpty()) { - foundAnnotationId = getIdFromFeature(features.get(0)); - } - return foundAnnotationId; + ShapeAnnotationHitResolver(ShapeAnnotations shapeAnnotations) { + this.shapeAnnotations = shapeAnnotations; } - private long getIdFromFeature(Feature feature) { - try { - return Long.valueOf(feature.getId()); - } catch (NumberFormatException exception) { - Timber.e(exception, "Couldn't parse feature id to a long, with id: %s", feature.getId()); - return NO_ANNOTATION_ID; + public Annotation execute(ShapeAnnotationHit shapeHit) { + Annotation foundAnnotation = null; + List<Annotation> annotations = shapeAnnotations.obtainAllIn(shapeHit.tapPoint); + if (annotations.size() > 0) { + foundAnnotation = annotations.get(0); } + return foundAnnotation; } } @@ -567,11 +538,9 @@ class AnnotationManager { private static class ShapeAnnotationHit { private final RectF tapPoint; - private final String[] layerIds; - ShapeAnnotationHit(RectF tapRect, String[] layerIds) { - this.tapPoint = tapRect; - this.layerIds = layerIds; + ShapeAnnotationHit(RectF tapPoint) { + this.tapPoint = tapPoint; } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AttributionDialogManager.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AttributionDialogManager.java index 5113a0cc73..9ccff387f5 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AttributionDialogManager.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/AttributionDialogManager.java @@ -1,12 +1,12 @@ package com.mapbox.mapboxsdk.maps; +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.v7.app.AlertDialog; import android.text.Html; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -56,7 +56,7 @@ class AttributionDialogManager implements View.OnClickListener, DialogInterface. private void showAttributionDialog() { attributionKeys = attributionMap.keySet().toArray(new String[attributionMap.size()]); - AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.mapbox_AlertDialogStyle); + AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.mapbox_attributionsDialogTitle); builder.setAdapter(new ArrayAdapter<>(context, R.layout.mapbox_attribution_list_item, attributionKeys), this); builder.show(); @@ -77,7 +77,7 @@ class AttributionDialogManager implements View.OnClickListener, DialogInterface. } private void showTelemetryDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.mapbox_AlertDialogStyle); + 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() { diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/CameraChangeDispatcher.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/CameraChangeDispatcher.java index 6f7d7c0080..cf780dcc3f 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/CameraChangeDispatcher.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/CameraChangeDispatcher.java @@ -1,5 +1,10 @@ package com.mapbox.mapboxsdk.maps; +import android.support.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraIdleListener; import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveCanceledListener; import static com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveListener; @@ -10,23 +15,32 @@ class CameraChangeDispatcher implements MapboxMap.OnCameraMoveStartedListener, M private boolean idle = true; + private final List<OnCameraMoveStartedListener> onCameraMoveStartedListenerList = new ArrayList<>(); + private final List<OnCameraMoveCanceledListener> onCameraMoveCanceledListenerList = new ArrayList<>(); + private final List<OnCameraMoveListener> onCameraMoveListenerList = new ArrayList<>(); + private final List<OnCameraIdleListener> onCameraIdleListenerList = new ArrayList<>(); + private OnCameraMoveStartedListener onCameraMoveStartedListener; private OnCameraMoveCanceledListener onCameraMoveCanceledListener; private OnCameraMoveListener onCameraMoveListener; private OnCameraIdleListener onCameraIdleListener; + @Deprecated void setOnCameraMoveStartedListener(OnCameraMoveStartedListener onCameraMoveStartedListener) { this.onCameraMoveStartedListener = onCameraMoveStartedListener; } + @Deprecated void setOnCameraMoveCanceledListener(OnCameraMoveCanceledListener onCameraMoveCanceledListener) { this.onCameraMoveCanceledListener = onCameraMoveCanceledListener; } + @Deprecated void setOnCameraMoveListener(OnCameraMoveListener onCameraMoveListener) { this.onCameraMoveListener = onCameraMoveListener; } + @Deprecated void setOnCameraIdleListener(OnCameraIdleListener onCameraIdleListener) { this.onCameraIdleListener = onCameraIdleListener; } @@ -36,34 +50,106 @@ class CameraChangeDispatcher implements MapboxMap.OnCameraMoveStartedListener, M if (!idle) { return; } - idle = false; + + // deprecated API if (onCameraMoveStartedListener != null) { onCameraMoveStartedListener.onCameraMoveStarted(reason); } + + // new API + if (!onCameraMoveStartedListenerList.isEmpty()) { + for (OnCameraMoveStartedListener cameraMoveStartedListener : onCameraMoveStartedListenerList) { + cameraMoveStartedListener.onCameraMoveStarted(reason); + } + } } @Override public void onCameraMove() { + // deprecated API if (onCameraMoveListener != null && !idle) { onCameraMoveListener.onCameraMove(); } + + // new API + if (!onCameraMoveListenerList.isEmpty() && !idle) { + for (OnCameraMoveListener cameraMoveListener : onCameraMoveListenerList) { + cameraMoveListener.onCameraMove(); + } + } } @Override public void onCameraMoveCanceled() { + // deprecated API if (onCameraMoveCanceledListener != null && !idle) { onCameraMoveCanceledListener.onCameraMoveCanceled(); } + + // new API + if (!onCameraMoveCanceledListenerList.isEmpty() && !idle) { + for (OnCameraMoveCanceledListener cameraMoveCanceledListener : onCameraMoveCanceledListenerList) { + cameraMoveCanceledListener.onCameraMoveCanceled(); + } + } } @Override public void onCameraIdle() { if (!idle) { idle = true; + // deprecated API if (onCameraIdleListener != null) { onCameraIdleListener.onCameraIdle(); } + + // new API + if (!onCameraIdleListenerList.isEmpty()) { + for (OnCameraIdleListener cameraIdleListener : onCameraIdleListenerList) { + cameraIdleListener.onCameraIdle(); + } + } + } + } + + void addOnCameraIdleListener(@NonNull OnCameraIdleListener listener) { + onCameraIdleListenerList.add(listener); + } + + void removeOnCameraIdleListener(@NonNull OnCameraIdleListener listener) { + if (onCameraIdleListenerList.contains(listener)) { + onCameraIdleListenerList.remove(listener); + } + } + + void addOnCameraMoveCancelListener(OnCameraMoveCanceledListener listener) { + onCameraMoveCanceledListenerList.add(listener); + } + + void removeOnCameraMoveCancelListener(OnCameraMoveCanceledListener listener) { + if (onCameraMoveCanceledListenerList.contains(listener)) { + onCameraMoveCanceledListenerList.remove(listener); + } + } + + void addOnCameraMoveStartedListener(OnCameraMoveStartedListener listener) { + onCameraMoveStartedListenerList.add(listener); + } + + void removeOnCameraMoveStartedListener(OnCameraMoveStartedListener listener) { + if (onCameraMoveStartedListenerList.contains(listener)) { + onCameraMoveStartedListenerList.remove(listener); + } + } + + void addOnCameraMoveListener(OnCameraMoveListener listener) { + onCameraMoveListenerList.add(listener); + } + + void removeOnCameraMoveListener(OnCameraMoveListener listener) { + if (onCameraMoveListenerList.contains(listener)) { + onCameraMoveListenerList.remove(listener); } } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/IconManager.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/IconManager.java index b1d6df2103..80ffa973e7 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/IconManager.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/IconManager.java @@ -143,11 +143,14 @@ class IconManager { } void iconCleanup(Icon icon) { - int refCounter = iconMap.get(icon) - 1; - if (refCounter == 0) { - remove(icon); - } else { - updateIconRefCounter(icon, refCounter); + Integer refCounter = iconMap.get(icon); + if (refCounter != null) { + refCounter--; + if (refCounter == 0) { + remove(icon); + } else { + updateIconRefCounter(icon, refCounter); + } } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Image.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Image.java new file mode 100644 index 0000000000..b2f6cef3b0 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Image.java @@ -0,0 +1,17 @@ +package com.mapbox.mapboxsdk.maps; + +class Image { + private final byte[] buffer; + private final float pixelRatio; + private final String name; + private final int width; + private final int height; + + public Image(byte[] buffer, float pixelRatio, String name, int width, int height) { + this.buffer = buffer; + this.pixelRatio = pixelRatio; + this.name = name; + this.width = width; + this.height = height; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java index 2394e52193..4120e164a4 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapGestureDetector.java @@ -1,14 +1,19 @@ package com.mapbox.mapboxsdk.maps; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.content.Context; import android.graphics.PointF; import android.location.Location; import android.support.annotation.Nullable; import android.support.v4.view.GestureDetectorCompat; import android.support.v4.view.ScaleGestureDetectorCompat; +import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.view.InputDevice; import android.view.MotionEvent; import android.view.ScaleGestureDetector; +import android.view.VelocityTracker; import android.view.ViewConfiguration; import com.almeros.android.multitouch.gesturedetectors.RotateGestureDetector; @@ -57,8 +62,14 @@ final class MapGestureDetector { private boolean scaleGestureOccurred; private boolean recentScaleGestureOccurred; + private boolean scaleAnimating; private long scaleBeginTime; + private VelocityTracker velocityTracker; + private boolean wasZoomingIn; + private boolean wasClockwiseRotating; + private boolean rotateGestureOccurred; + MapGestureDetector(Context context, Transform transform, Projection projection, UiSettings uiSettings, TrackingSettings trackingSettings, AnnotationManager annotationManager, CameraChangeDispatcher cameraChangeDispatcher) { @@ -153,6 +164,12 @@ final class MapGestureDetector { // Handle two finger tap switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain(); + } else { + velocityTracker.clear(); + } + velocityTracker.addMovement(event); // First pointer down, reset scaleGestureOccurred, used to avoid triggering a fling after a scale gesture #7666 recentScaleGestureOccurred = false; transform.setGestureInProgress(true); @@ -203,11 +220,23 @@ final class MapGestureDetector { twoTap = false; transform.setGestureInProgress(false); + if (velocityTracker != null) { + velocityTracker.recycle(); + } + velocityTracker = null; break; case MotionEvent.ACTION_CANCEL: twoTap = false; transform.setGestureInProgress(false); + if (velocityTracker != null) { + velocityTracker.recycle(); + } + velocityTracker = null; + break; + case MotionEvent.ACTION_MOVE: + velocityTracker.addMovement(event); + velocityTracker.computeCurrentVelocity(1000); break; } @@ -430,7 +459,11 @@ final class MapGestureDetector { */ private class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + private static final int ANIMATION_TIME_MULTIPLIER = 77; + private static final double ZOOM_DISTANCE_DIVIDER = 5; + private float scaleFactor = 1.0f; + private PointF scalePointBegin; // Called when two fingers first touch the screen @Override @@ -440,6 +473,7 @@ final class MapGestureDetector { } recentScaleGestureOccurred = true; + scalePointBegin = new PointF(detector.getFocusX(), detector.getFocusY()); scaleBeginTime = detector.getEventTime(); MapboxTelemetry.getInstance().pushEvent(MapboxEventWrapper.buildMapClickEvent( getLocationFromGesture(detector.getFocusX(), detector.getFocusY()), @@ -447,15 +481,6 @@ final class MapGestureDetector { return true; } - // Called when fingers leave screen - @Override - public void onScaleEnd(ScaleGestureDetector detector) { - scaleGestureOccurred = false; - scaleBeginTime = 0; - scaleFactor = 1.0f; - cameraChangeDispatcher.onCameraIdle(); - } - // Called each time a finger moves // Called for pinch zooms and quickzooms/quickscales @Override @@ -464,6 +489,7 @@ final class MapGestureDetector { return super.onScale(detector); } + wasZoomingIn = (Math.log(detector.getScaleFactor()) / Math.log(Math.PI / 2)) >= 0; if (tiltGestureOccurred) { return false; } @@ -478,7 +504,7 @@ final class MapGestureDetector { // If scale is large enough ignore a tap scaleFactor *= detector.getScaleFactor(); - if ((scaleFactor > 1.05f) || (scaleFactor < 0.95f)) { + if ((scaleFactor > 1.1f) || (scaleFactor < 0.9f)) { // notify camera change listener cameraChangeDispatcher.onCameraMoveStarted(REASON_API_GESTURE); scaleGestureOccurred = true; @@ -497,7 +523,6 @@ final class MapGestureDetector { // make an assumption here; if the zoom center is specified by the gesture, it's NOT going // to be in the center of the map. Therefore the zoom will translate the map center, so tracking // should be disabled. - trackingSettings.resetTrackingModesIfRequired(!quickZoom, false, false); // Scale the map if (focalPoint != null) { @@ -506,19 +531,79 @@ final class MapGestureDetector { } else if (quickZoom) { cameraChangeDispatcher.onCameraMove(); // clamp scale factors we feed to core #7514 - float scaleFactor = MathUtils.clamp(detector.getScaleFactor(), + float scaleFactor = detector.getScaleFactor(); + // around center map + double zoomBy = Math.log(scaleFactor) / Math.log(Math.PI / 2); + boolean negative = zoomBy < 0; + zoomBy = MathUtils.clamp(Math.abs(zoomBy), MapboxConstants.MINIMUM_SCALE_FACTOR_CLAMP, MapboxConstants.MAXIMUM_SCALE_FACTOR_CLAMP); - // around center map - transform.zoomBy(Math.log(scaleFactor) / Math.log(Math.PI / 2), - uiSettings.getWidth() / 2, uiSettings.getHeight() / 2); + transform.zoomBy(negative ? -zoomBy : zoomBy, uiSettings.getWidth() / 2, uiSettings.getHeight() / 2); + recentScaleGestureOccurred = true; } else { // around gesture transform.zoomBy(Math.log(detector.getScaleFactor()) / Math.log(Math.PI / 2), - detector.getFocusX(), detector.getFocusY()); + scalePointBegin.x, scalePointBegin.y); } return true; } + + // Called when fingers leave screen + @Override + public void onScaleEnd(final ScaleGestureDetector detector) { + if (rotateGestureOccurred || quickZoom) { + reset(); + return; + } + + double velocityXY = Math.abs(velocityTracker.getYVelocity()) + Math.abs(velocityTracker.getXVelocity()); + if (velocityXY > MapboxConstants.VELOCITY_THRESHOLD_IGNORE_FLING / 2) { + scaleAnimating = true; + double zoomAddition = calculateScale(velocityXY); + double currentZoom = transform.getRawZoom(); + long animationTime = (long) (Math.log(velocityXY) * ANIMATION_TIME_MULTIPLIER); + createScaleAnimator(currentZoom, zoomAddition, animationTime).start(); + } else if (!scaleAnimating) { + reset(); + } + } + + private void reset() { + scaleAnimating = false; + scaleGestureOccurred = false; + scaleBeginTime = 0; + scaleFactor = 1.0f; + cameraChangeDispatcher.onCameraIdle(); + } + + private double calculateScale(double velocityXY) { + double zoomAddition = (float) (Math.log(velocityXY) / ZOOM_DISTANCE_DIVIDER); + if (!wasZoomingIn) { + zoomAddition = -zoomAddition; + } + return zoomAddition; + } + + private Animator createScaleAnimator(double currentZoom, double zoomAddition, long animationTime) { + ValueAnimator animator = ValueAnimator.ofFloat((float) currentZoom, (float) (currentZoom + zoomAddition)); + animator.setDuration(animationTime); + animator.setInterpolator(new FastOutSlowInInterpolator()); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + transform.setZoom((Float) animation.getAnimatedValue(), scalePointBegin); + } + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + reset(); + } + }); + return animator; + } } /** @@ -526,11 +611,13 @@ final class MapGestureDetector { */ private class RotateGestureListener extends RotateGestureDetector.SimpleOnRotateGestureListener { - private static final long ROTATE_INVOKE_WAIT_TIME = 1500; + private static final float ROTATE_INVOKE_ANGLE = 15.30f; + private static final float ROTATE_LIMITATION_ANGLE = 3.35f; + private static final float ROTATE_LIMITATION_DURATION = ROTATE_LIMITATION_ANGLE * 1.85f; private long beginTime = 0; - private float totalAngle = 0.0f; private boolean started = false; + private boolean animating = false; // Called when two fingers first touch the screen @Override @@ -546,15 +633,6 @@ final class MapGestureDetector { return true; } - // Called when the fingers leave the screen - @Override - public void onRotateEnd(RotateGestureDetector detector) { - // notify camera change listener - beginTime = 0; - totalAngle = 0.0f; - started = false; - } - // Called each time one of the two fingers moves // Called for rotation @Override @@ -563,18 +641,10 @@ final class MapGestureDetector { return false; } - // Ignore short touches in case it is a tap - // Also ignore small rotate - long time = detector.getEventTime(); - long interval = time - beginTime; - if (!started && (interval <= ViewConfiguration.getTapTimeout() || isScaleGestureActive(time))) { - return false; - } - // If rotate is large enough ignore a tap // Also is zoom already started, don't rotate - totalAngle += detector.getRotationDegreesDelta(); - if (totalAngle > 35.0f || totalAngle < -35.0f) { + float angle = detector.getRotationDegreesDelta(); + if (Math.abs(angle) >= ROTATE_INVOKE_ANGLE) { MapboxTelemetry.getInstance().pushEvent(MapboxEventWrapper.buildMapClickEvent( getLocationFromGesture(detector.getFocusX(), detector.getFocusY()), MapboxEvent.GESTURE_ROTATION_START, transform)); @@ -585,13 +655,17 @@ final class MapGestureDetector { return false; } + wasClockwiseRotating = detector.getRotationDegreesDelta() > 0; + if (scaleBeginTime != 0) { + rotateGestureOccurred = true; + } + // rotation constitutes translation of anything except the center of // rotation, so cancel both location and bearing tracking if required trackingSettings.resetTrackingModesIfRequired(true, true, false); - // Get rotate value - double bearing = transform.getRawBearing(); - bearing += detector.getRotationDegreesDelta(); + // Calculate map bearing value + double bearing = transform.getRawBearing() + angle; // Rotate the map if (focalPoint != null) { @@ -604,11 +678,81 @@ final class MapGestureDetector { return true; } - private boolean isScaleGestureActive(long time) { - long scaleExecutionTime = time - scaleBeginTime; - boolean scaleGestureStarted = scaleBeginTime != 0; - boolean scaleOffsetTimeValid = scaleExecutionTime > ROTATE_INVOKE_WAIT_TIME; - return (scaleGestureStarted && scaleOffsetTimeValid) || scaleGestureOccurred; + // Called when the fingers leave the screen + @Override + public void onRotateEnd(RotateGestureDetector detector) { + long interval = detector.getEventTime() - beginTime; + if ((!started && (interval <= ViewConfiguration.getTapTimeout())) || scaleAnimating || interval > 500) { + reset(); + return; + } + + double angularVelocity = calculateVelocityVector(detector); + if (Math.abs(angularVelocity) > 0.001 && rotateGestureOccurred && !animating) { + animateRotateVelocity(); + } else if (!animating) { + reset(); + } + } + + private void reset() { + beginTime = 0; + started = false; + animating = false; + rotateGestureOccurred = false; + } + + private void animateRotateVelocity() { + animating = true; + double currentRotation = transform.getRawBearing(); + double rotateAdditionDegrees = calculateVelocityInDegrees(); + createAnimator(currentRotation, rotateAdditionDegrees).start(); + } + + private double calculateVelocityVector(RotateGestureDetector detector) { + return ((detector.getFocusX() * velocityTracker.getYVelocity()) + + (detector.getFocusY() * velocityTracker.getXVelocity())) + / (Math.pow(detector.getFocusX(), 2) + Math.pow(detector.getFocusY(), 2)); + } + + private double calculateVelocityInDegrees() { + double angleRadians = Math.atan2(velocityTracker.getXVelocity(), velocityTracker.getYVelocity()); + double angle = angleRadians / (Math.PI / 180); + if (angle <= 0) { + angle += 360; + } + + // limit the angle + angle = angle / ROTATE_LIMITATION_ANGLE; + + // correct direction + if (!wasClockwiseRotating) { + angle = -angle; + } + + return angle; + } + + private Animator createAnimator(double currentRotation, double rotateAdditionDegrees) { + ValueAnimator animator = ValueAnimator.ofFloat( + (float) currentRotation, + (float) (currentRotation + rotateAdditionDegrees) + ); + animator.setDuration((long) (Math.abs(rotateAdditionDegrees) * ROTATE_LIMITATION_DURATION)); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + transform.setBearing((Float) animation.getAnimatedValue()); + } + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + reset(); + } + }); + return animator; } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java index 92153f7f0b..c025a119b7 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java @@ -28,13 +28,14 @@ import com.mapbox.mapboxsdk.annotations.Annotation; import com.mapbox.mapboxsdk.annotations.MarkerViewManager; import com.mapbox.mapboxsdk.constants.MapboxConstants; import com.mapbox.mapboxsdk.constants.Style; -import com.mapbox.mapboxsdk.maps.renderer.glsurfaceview.GLSurfaceViewMapRenderer; import com.mapbox.mapboxsdk.maps.renderer.MapRenderer; +import com.mapbox.mapboxsdk.maps.renderer.glsurfaceview.GLSurfaceViewMapRenderer; import com.mapbox.mapboxsdk.maps.renderer.textureview.TextureViewMapRenderer; import com.mapbox.mapboxsdk.maps.widgets.CompassView; import com.mapbox.mapboxsdk.maps.widgets.MyLocationView; import com.mapbox.mapboxsdk.maps.widgets.MyLocationViewSettings; import com.mapbox.mapboxsdk.net.ConnectivityReceiver; +import com.mapbox.mapboxsdk.storage.FileSource; import com.mapbox.services.android.telemetry.MapboxTelemetry; import java.lang.annotation.Retention; @@ -175,8 +176,9 @@ public class MapView extends FrameLayout { Markers markers = new MarkerContainer(nativeMapView, this, annotationsArray, iconManager, markerViewManager); Polygons polygons = new PolygonContainer(nativeMapView, annotationsArray); Polylines polylines = new PolylineContainer(nativeMapView, annotationsArray); + ShapeAnnotations shapeAnnotations = new ShapeAnnotationContainer(nativeMapView, annotationsArray); AnnotationManager annotationManager = new AnnotationManager(nativeMapView, this, annotationsArray, - markerViewManager, iconManager, annotations, markers, polygons, polylines); + markerViewManager, iconManager, annotations, markers, polygons, polylines, shapeAnnotations); Transform transform = new Transform(nativeMapView, annotationManager.getMarkerViewManager(), trackingSettings, cameraChangeDispatcher); @@ -191,8 +193,10 @@ public class MapView extends FrameLayout { annotationManager, cameraChangeDispatcher); mapKeyListener = new MapKeyListener(transform, trackingSettings, uiSettings); + // overlain zoom buttons mapZoomButtonController = new MapZoomButtonController(new ZoomButtonsController(this)); - MapZoomControllerListener zoomListener = new MapZoomControllerListener(mapGestureDetector, uiSettings, transform); + MapZoomControllerListener zoomListener = new MapZoomControllerListener(mapGestureDetector, uiSettings, transform, + cameraChangeDispatcher, getWidth(), getHeight()); mapZoomButtonController.bind(uiSettings, zoomListener); compassView.injectCompassAnimationListener(createCompassAnimationListener(cameraChangeDispatcher)); @@ -345,8 +349,10 @@ public class MapView extends FrameLayout { */ @UiThread public void onSaveInstanceState(@NonNull Bundle outState) { - outState.putBoolean(MapboxConstants.STATE_HAS_SAVED_STATE, true); - mapboxMap.onSaveInstanceState(outState); + if (mapboxMap != null) { + outState.putBoolean(MapboxConstants.STATE_HAS_SAVED_STATE, true); + mapboxMap.onSaveInstanceState(outState); + } } /** @@ -355,6 +361,7 @@ public class MapView extends FrameLayout { @UiThread public void onStart() { ConnectivityReceiver.instance(getContext()).activate(); + FileSource.getInstance(getContext()).activate(); if (mapboxMap != null) { mapboxMap.onStart(); } @@ -385,8 +392,12 @@ public class MapView extends FrameLayout { */ @UiThread public void onStop() { - mapboxMap.onStop(); + if (mapboxMap != null) { + // map was destroyed before it was started + mapboxMap.onStop(); + } ConnectivityReceiver.instance(getContext()).deactivate(); + FileSource.getInstance(getContext()).deactivate(); } /** @@ -406,6 +417,10 @@ public class MapView extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent event) { + if (!isMapInitialized() || !isZoomButtonControllerInitialized()) { + return super.onTouchEvent(event); + } + if (event.getAction() == MotionEvent.ACTION_DOWN) { mapZoomButtonController.setVisible(true); } @@ -434,11 +449,18 @@ public class MapView extends FrameLayout { @Override public boolean onGenericMotionEvent(MotionEvent event) { + if (mapGestureDetector == null) { + return super.onGenericMotionEvent(event); + } return mapGestureDetector.onGenericMotionEvent(event) || super.onGenericMotionEvent(event); } @Override public boolean onHoverEvent(MotionEvent event) { + if (!isZoomButtonControllerInitialized()) { + return super.onHoverEvent(event); + } + switch (event.getActionMasked()) { case MotionEvent.ACTION_HOVER_ENTER: case MotionEvent.ACTION_HOVER_MOVE: @@ -495,7 +517,7 @@ public class MapView extends FrameLayout { if (destroyed) { return; } - if (nativeMapView == null) { + if (!isMapInitialized()) { mapboxMapOptions.styleUrl(url); return; } @@ -512,7 +534,7 @@ public class MapView extends FrameLayout { return; } - if (!isInEditMode() && nativeMapView != null) { + if (!isInEditMode() && isMapInitialized()) { nativeMapView.resizeView(width, height); } } @@ -526,7 +548,9 @@ public class MapView extends FrameLayout { @CallSuper protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - mapZoomButtonController.setVisible(false); + if (isZoomButtonControllerInitialized()) { + mapZoomButtonController.setVisible(false); + } } // Called when view is hidden and shown @@ -536,7 +560,7 @@ public class MapView extends FrameLayout { return; } - if (mapZoomButtonController != null) { + if (isZoomButtonControllerInitialized()) { mapZoomButtonController.setVisible(visibility == View.VISIBLE); } } @@ -598,6 +622,14 @@ public class MapView extends FrameLayout { } } + private boolean isMapInitialized() { + return nativeMapView != null; + } + + private boolean isZoomButtonControllerInitialized() { + return mapZoomButtonController != null; + } + MapboxMap getMapboxMap() { return mapboxMap; } @@ -883,16 +915,23 @@ public class MapView extends FrameLayout { } } - private class MapZoomControllerListener implements ZoomButtonsController.OnZoomListener { + private static class MapZoomControllerListener implements ZoomButtonsController.OnZoomListener { private final MapGestureDetector mapGestureDetector; private final UiSettings uiSettings; private final Transform transform; + private final CameraChangeDispatcher cameraChangeDispatcher; + private final float mapWidth; + private final float mapHeight; - MapZoomControllerListener(MapGestureDetector detector, UiSettings uiSettings, Transform transform) { + MapZoomControllerListener(MapGestureDetector detector, UiSettings uiSettings, Transform transform, + CameraChangeDispatcher dispatcher, float mapWidth, float mapHeight) { this.mapGestureDetector = detector; this.uiSettings = uiSettings; this.transform = transform; + this.cameraChangeDispatcher = dispatcher; + this.mapWidth = mapWidth; + this.mapHeight = mapHeight; } // Not used @@ -905,6 +944,7 @@ public class MapView extends FrameLayout { @Override public void onZoom(boolean zoomIn) { if (uiSettings.isZoomGesturesEnabled()) { + cameraChangeDispatcher.onCameraMoveStarted(CameraChangeDispatcher.REASON_API_ANIMATION); onZoom(zoomIn, mapGestureDetector.getFocalPoint()); } } @@ -913,7 +953,7 @@ public class MapView extends FrameLayout { if (focalPoint != null) { transform.zoom(zoomIn, focalPoint); } else { - PointF centerPoint = new PointF(getMeasuredWidth() / 2, getMeasuredHeight() / 2); + PointF centerPoint = new PointF(mapWidth / 2, mapHeight / 2); transform.zoom(zoomIn, centerPoint); } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java index 83b4dc4e84..6bf8342efb 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java @@ -47,6 +47,7 @@ import com.mapbox.services.commons.geojson.Feature; import com.mapbox.services.commons.geojson.Geometry; import java.lang.reflect.ParameterizedType; +import java.util.HashMap; import java.util.List; import timber.log.Timber; @@ -463,6 +464,13 @@ public final class MapboxMap { } /** + * Adds an images to be used in the map's style + */ + public void addImages(@NonNull HashMap<String, Bitmap> images) { + nativeMapView.addImages(images); + } + + /** * Removes an image from the map's style * * @param name the name of the image to remove @@ -709,6 +717,10 @@ public final class MapboxMap { // MapChange.REGION_DID_CHANGE_ANIMATED is not called for `jumpTo` // invalidate camera position to provide OnCameraChange event. invalidateCameraPosition(); + + if (callback != null) { + callback.onFinish(); + } } }); } @@ -1753,39 +1765,123 @@ public final class MapboxMap { * Sets a callback that is invoked when camera movement has ended. * * @param listener the listener to notify + * @deprecated use {@link #addOnCameraIdleListener(OnCameraIdleListener)} + * and {@link #removeOnCameraIdleListener(OnCameraIdleListener)} instead */ + @Deprecated public void setOnCameraIdleListener(@Nullable OnCameraIdleListener listener) { cameraChangeDispatcher.setOnCameraIdleListener(listener); } /** + * Adds a callback that is invoked when camera movement has ended. + * + * @param listener the listener to notify + */ + public void addOnCameraIdleListener(@Nullable 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(@Nullable OnCameraIdleListener listener) { + cameraChangeDispatcher.removeOnCameraIdleListener(listener); + } + + /** * Sets a callback that is invoked when camera movement was cancelled. * * @param listener the listener to notify + * @deprecated use {@link #addOnCameraMoveCancelListener(OnCameraMoveCanceledListener)} and + * {@link #removeOnCameraMoveCancelListener(OnCameraMoveCanceledListener)} instead */ + @Deprecated public void setOnCameraMoveCancelListener(@Nullable OnCameraMoveCanceledListener listener) { cameraChangeDispatcher.setOnCameraMoveCanceledListener(listener); } /** + * Adds a callback that is invoked when camera movement was cancelled. + * + * @param listener the listener to notify + */ + public void addOnCameraMoveCancelListener(@Nullable 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(@Nullable OnCameraMoveCanceledListener listener) { + cameraChangeDispatcher.removeOnCameraMoveCancelListener(listener); + } + + /** * Sets a callback that is invoked when camera movement has started. * * @param listener the listener to notify + * @deprecated use {@link #addOnCameraMoveStartedListener(OnCameraMoveStartedListener)} and + * {@link #removeOnCameraMoveStartedListener(OnCameraMoveStartedListener)} instead */ + @Deprecated public void setOnCameraMoveStartedListener(@Nullable OnCameraMoveStartedListener listener) { cameraChangeDispatcher.setOnCameraMoveStartedListener(listener); } /** + * Adds a callback that is invoked when camera movement has started. + * + * @param listener the listener to notify + */ + public void addOnCameraMoveStartedListener(@Nullable 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(@Nullable OnCameraMoveStartedListener listener) { + cameraChangeDispatcher.removeOnCameraMoveStartedListener(listener); + } + + /** * Sets a callback that is invoked when camera position changes. * * @param listener the listener to notify + * @deprecated use {@link #addOnCameraMoveListener(OnCameraMoveListener)} and + * {@link #removeOnCameraMoveListener(OnCameraMoveListener)}instead */ + @Deprecated public void setOnCameraMoveListener(@Nullable OnCameraMoveListener listener) { cameraChangeDispatcher.setOnCameraMoveListener(listener); } /** + * Adds a callback that is invoked when camera position changes. + * + * @param listener the listener to notify + */ + public void addOnCameraMoveListener(@Nullable OnCameraMoveListener listener) { + cameraChangeDispatcher.addOnCameraMoveListener(listener); + } + + /** + * Removes a callback that is invoked when camera position changes. + * + * @param listener the listener to remove + */ + public void removeOnCameraMoveListener(@Nullable 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. diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MarkerContainer.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MarkerContainer.java index 072382ce07..2c2f07a112 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MarkerContainer.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MarkerContainer.java @@ -98,15 +98,8 @@ class MarkerContainer implements Markers { @NonNull @Override public List<Marker> obtainAllIn(@NonNull RectF rectangle) { - // convert Rectangle to be density depedent - float pixelRatio = nativeMapView.getPixelRatio(); - RectF rect = new RectF(rectangle.left / pixelRatio, - rectangle.top / pixelRatio, - rectangle.right / pixelRatio, - rectangle.bottom / pixelRatio); - + RectF rect = nativeMapView.getDensityDependantRectangle(rectangle); long[] ids = nativeMapView.queryPointAnnotations(rect); - List<Long> idsList = new ArrayList<>(ids.length); for (long id : ids) { idsList.add(id); diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java index bd8a54783e..8b6bce69e2 100755 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java @@ -4,6 +4,7 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.PointF; import android.graphics.RectF; +import android.os.AsyncTask; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -34,7 +35,9 @@ import com.mapbox.services.commons.geojson.Geometry; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import timber.log.Timber; @@ -468,6 +471,13 @@ final class NativeMapView { return nativeQueryPointAnnotations(rect); } + public long[] queryShapeAnnotations(RectF rectF) { + if (isDestroyedOn("queryShapeAnnotations")) { + return new long[] {}; + } + return nativeQueryShapeAnnotations(rectF); + } + public void addAnnotationIcon(String symbol, int width, int height, float scale, byte[] pixels) { if (isDestroyedOn("addAnnotationIcon")) { return; @@ -746,6 +756,7 @@ final class NativeMapView { if (isDestroyedOn("addImage")) { return; } + // Check/correct config if (image.getConfig() != Bitmap.Config.ARGB_8888) { image = image.copy(Bitmap.Config.ARGB_8888, false); @@ -762,6 +773,14 @@ final class NativeMapView { nativeAddImage(name, image.getWidth(), image.getHeight(), pixelRatio, buffer.array()); } + public void addImages(@NonNull HashMap<String, Bitmap> bitmapHashMap) { + if (isDestroyedOn("addImages")) { + return; + } + //noinspection unchecked + new BitmapImageConversionTask(this).execute(bitmapHashMap); + } + public void removeImage(String name) { if (isDestroyedOn("removeImage")) { return; @@ -825,6 +844,15 @@ final class NativeMapView { return pixelRatio; } + RectF getDensityDependantRectangle(final RectF rectangle) { + return new RectF( + rectangle.left / pixelRatio, + rectangle.top / pixelRatio, + rectangle.right / pixelRatio, + rectangle.bottom / pixelRatio + ); + } + // // Callbacks // @@ -927,6 +955,8 @@ final class NativeMapView { private native long[] nativeQueryPointAnnotations(RectF rect); + private native long[] nativeQueryShapeAnnotations(RectF rect); + private native void nativeAddAnnotationIcon(String symbol, int width, int height, float scale, byte[] pixels); private native void nativeRemoveAnnotationIcon(String symbol); @@ -1006,6 +1036,8 @@ final class NativeMapView { private native void nativeAddImage(String name, int width, int height, float pixelRatio, byte[] array); + private native void nativeAddImages(Image[] images); + private native void nativeRemoveImage(String name); private native Bitmap nativeGetImage(String name); @@ -1093,4 +1125,55 @@ final class NativeMapView { }); } + + + // + // Image conversion + // + + private static class BitmapImageConversionTask extends AsyncTask<HashMap<String, Bitmap>, Void, List<Image>> { + + private NativeMapView nativeMapView; + + BitmapImageConversionTask(NativeMapView nativeMapView) { + this.nativeMapView = nativeMapView; + } + + @Override + protected List<Image> doInBackground(HashMap<String, Bitmap>... params) { + HashMap<String, Bitmap> bitmapHashMap = params[0]; + + List<Image> images = new ArrayList<>(); + ByteBuffer buffer; + String name; + Bitmap bitmap; + + for (Map.Entry<String, Bitmap> stringBitmapEntry : bitmapHashMap.entrySet()) { + name = stringBitmapEntry.getKey(); + bitmap = stringBitmapEntry.getValue(); + + if (bitmap.getConfig() != Bitmap.Config.ARGB_8888) { + bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false); + } + + buffer = ByteBuffer.allocate(bitmap.getByteCount()); + bitmap.copyPixelsToBuffer(buffer); + + float density = bitmap.getDensity() == Bitmap.DENSITY_NONE ? Bitmap.DENSITY_NONE : bitmap.getDensity(); + float pixelRatio = density / DisplayMetrics.DENSITY_DEFAULT; + + images.add(new Image(buffer.array(), pixelRatio, name, bitmap.getWidth(), bitmap.getHeight())); + } + + return images; + } + + @Override + protected void onPostExecute(List<Image> images) { + super.onPostExecute(images); + if (nativeMapView != null && !nativeMapView.isDestroyedOn("nativeAddImages")) { + nativeMapView.nativeAddImages(images.toArray(new Image[images.size()])); + } + } + } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotationContainer.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotationContainer.java new file mode 100644 index 0000000000..6ded2f32fb --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotationContainer.java @@ -0,0 +1,38 @@ +package com.mapbox.mapboxsdk.maps; + +import android.graphics.RectF; +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 NativeMapView nativeMapView; + private final LongSparseArray<Annotation> annotations; + + ShapeAnnotationContainer(NativeMapView nativeMapView, LongSparseArray<Annotation> annotations) { + this.nativeMapView = nativeMapView; + this.annotations = annotations; + } + + @Override + public List<Annotation> obtainAllIn(RectF rectangle) { + RectF rect = nativeMapView.getDensityDependantRectangle(rectangle); + long[] annotationIds = nativeMapView.queryShapeAnnotations(rect); + return getAnnotationsFromIds(annotationIds); + } + + private List<Annotation> getAnnotationsFromIds(long[] annotationIds) { + List<Annotation> shapeAnnotations = new ArrayList<>(); + for (long annotationId : annotationIds) { + Annotation annotation = annotations.get(annotationId); + if (annotation != null) { + shapeAnnotations.add(annotation); + } + } + return shapeAnnotations; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotations.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/ShapeAnnotations.java new file mode 100644 index 0000000000..f9b2ca10db --- /dev/null +++ b/platform/android/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<Annotation> obtainAllIn(RectF rectF); + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java index 6f63c2eba8..0366e50627 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/Transform.java @@ -98,9 +98,6 @@ final class Transform implements MapView.OnMapChangedListener { cancelTransitions(); cameraChangeDispatcher.onCameraMoveStarted(OnCameraMoveStartedListener.REASON_API_ANIMATION); mapView.jumpTo(cameraPosition.bearing, cameraPosition.target, cameraPosition.tilt, cameraPosition.zoom); - if (callback != null) { - callback.onFinish(); - } cameraChangeDispatcher.onCameraIdle(); } } @@ -210,6 +207,10 @@ final class Transform implements MapView.OnMapChangedListener { return cameraPosition.zoom; } + double getRawZoom() { + return mapView.getZoom(); + } + void zoom(boolean zoomIn, @NonNull PointF focalPoint) { CameraPosition cameraPosition = invalidateCameraPosition(); if (cameraPosition != null) { @@ -221,6 +222,17 @@ final class Transform implements MapView.OnMapChangedListener { } } + void zoom(double zoomAddition, @NonNull PointF focalPoint, long duration) { + CameraPosition cameraPosition = invalidateCameraPosition(); + if (cameraPosition != null) { + int newZoom = (int) Math.round(cameraPosition.zoom + zoomAddition); + setZoom(newZoom, focalPoint, duration); + } else { + // we are not transforming, notify about being idle + cameraChangeDispatcher.onCameraIdle(); + } + } + void setZoom(double zoom, @NonNull PointF focalPoint) { setZoom(zoom, focalPoint, 0); } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/widgets/CompassView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/widgets/CompassView.java index 45f72af1c5..1e604c9bef 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/widgets/CompassView.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/widgets/CompassView.java @@ -6,10 +6,10 @@ import android.support.annotation.NonNull; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewPropertyAnimatorCompat; import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; -import android.support.v7.widget.AppCompatImageView; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import com.mapbox.mapboxsdk.maps.MapboxMap; @@ -22,7 +22,7 @@ import com.mapbox.mapboxsdk.maps.MapboxMap; * use {@link com.mapbox.mapboxsdk.maps.UiSettings}. * </p> */ -public final class CompassView extends AppCompatImageView implements Runnable { +public final class CompassView extends ImageView implements Runnable { public static final long TIME_WAIT_IDLE = 500; public static final long TIME_MAP_NORTH_ANIMATION = 150; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java index f210729037..1b9a156352 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java @@ -300,10 +300,19 @@ public class OfflineRegion { /** * Pause or resume downloading of regional resources. + * <p> + * After a download has been completed, you are required to reset the state of the region to STATE_INACTIVE. + * </p> * * @param state the download state */ public void setDownloadState(@DownloadState int state) { + if (state == STATE_ACTIVE) { + fileSource.activate(); + } else { + fileSource.deactivate(); + } + this.state = state; setOfflineRegionDownloadState(state); } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshotter.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshotter.java index 5206606c83..5deedc3e63 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshotter.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/snapshotter/MapSnapshotter.java @@ -4,6 +4,7 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; +import android.graphics.Matrix; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; @@ -262,35 +263,59 @@ public class MapSnapshotter { nativeCancel(); } - protected void addOverlay(Bitmap original) { + /** + * Draw an overlay on the map snapshot. + * + * @param mapSnapshot the map snapshot to draw the overlay on + */ + protected void addOverlay(MapSnapshot mapSnapshot) { + Bitmap original = mapSnapshot.getBitmap(); + Canvas canvas = new Canvas(original); + addLogo(canvas, original); + } + + /** + * Draw a logo on the canvas created from the map snapshot. + * + * @param canvas the canvas to draw the bitmap on + * @param original the map snapshot image + */ + private void addLogo(Canvas canvas, Bitmap original) { DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); float margin = displayMetrics.density * LOGO_MARGIN_DP; - Canvas canvas = new Canvas(original); + Bitmap logo = createScaledLogo(original); + canvas.drawBitmap(logo, margin, original.getHeight() - logo.getHeight() - margin, null); + } + + /** + * Create a scaled logo for a map snapshot. + * + * @param snapshot the map snapshot where the logo should be placed on + * @return the scaled bitmap logo + */ + private Bitmap createScaledLogo(Bitmap snapshot) { + Bitmap logo = BitmapFactory.decodeResource(context.getResources(), R.drawable.mapbox_logo_icon, null); + float scale = calculateLogoScale(snapshot, logo); + Matrix matrix = new Matrix(); + matrix.postScale(scale, scale); + return Bitmap.createBitmap(logo, 0, 0, logo.getWidth(), logo.getHeight(), matrix, true); + } - // Decode just the boundaries - BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); - bitmapOptions.inJustDecodeBounds = true; - BitmapFactory.decodeResource(context.getResources(), R.drawable.mapbox_logo_icon, bitmapOptions); - int srcWidth = bitmapOptions.outWidth; - int srcHeight = bitmapOptions.outHeight; - - // Ratio, preferred dimensions and resulting sample size - float widthRatio = displayMetrics.widthPixels / original.getWidth(); - float heightRatio = displayMetrics.heightPixels / original.getHeight(); - float prefWidth = srcWidth / widthRatio; - float prefHeight = srcHeight / heightRatio; - int sampleSize = MapSnaphotUtil.calculateInSampleSize(bitmapOptions, (int) prefWidth, (int) prefHeight); - - // Scale bitmap - bitmapOptions.inJustDecodeBounds = false; - bitmapOptions.inScaled = true; - bitmapOptions.inSampleSize = sampleSize; - bitmapOptions.inDensity = srcWidth; - bitmapOptions.inTargetDensity = (int) prefWidth * bitmapOptions.inSampleSize; - Bitmap logo = BitmapFactory.decodeResource(context.getResources(), R.drawable.mapbox_logo_icon, bitmapOptions); - - float scaledHeight = bitmapOptions.outHeight * heightRatio / bitmapOptions.inSampleSize; - canvas.drawBitmap(logo, margin, original.getHeight() - scaledHeight - margin, null); + /** + * Calculates the scale of the logo, only allow downscaling. + * + * @param snapshot the bitmap of the map snapshot + * @param logo the bitmap of the mapbox logo + * @return the scale value + */ + private float calculateLogoScale(Bitmap snapshot, Bitmap logo) { + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + float widthRatio = displayMetrics.widthPixels / snapshot.getWidth(); + float heightRatio = displayMetrics.heightPixels / snapshot.getHeight(); + float prefWidth = logo.getWidth() / widthRatio; + float prefHeight = logo.getHeight() / heightRatio; + float calculatedScale = Math.min(prefWidth / logo.getWidth(), prefHeight / logo.getHeight()) * 2; + return calculatedScale < 1 ? calculatedScale : 1.0f; } /** @@ -302,7 +327,7 @@ public class MapSnapshotter { protected void onSnapshotReady(MapSnapshot snapshot) { if (callback != null) { if (snapshot.isShowLogo()) { - addOverlay(snapshot.getBitmap()); + addOverlay(snapshot); } callback.onSnapshotReady(snapshot); reset(); diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java index a968cdf192..41dc449b50 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java @@ -72,7 +72,7 @@ public class FileSource { MapboxConstants.KEY_META_DATA_SET_STORAGE_EXTERNAL, MapboxConstants.DEFAULT_SET_STORAGE_EXTERNAL); } catch (PackageManager.NameNotFoundException exception) { - Timber.e(exception,"Failed to read the package metadata: "); + Timber.e(exception, "Failed to read the package metadata: "); } catch (Exception exception) { Timber.e(exception, "Failed to read the storage key: "); } @@ -119,17 +119,39 @@ public class FileSource { } private long nativePtr; + private long activeCounter; + private boolean wasPaused; private FileSource(String cachePath, AssetManager assetManager) { initialize(Mapbox.getAccessToken(), cachePath, assetManager); } + public void activate() { + activeCounter++; + if (activeCounter == 1 && wasPaused) { + wasPaused = false; + resume(); + } + } + + public void deactivate() { + activeCounter--; + if (activeCounter == 0) { + wasPaused = true; + pause(); + } + } + public native void setAccessToken(@NonNull String accessToken); public native String getAccessToken(); public native void setApiBaseUrl(String baseUrl); + private native void resume(); + + private native void pause(); + /** * Sets a callback for transforming URLs requested from the internet * <p> diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ColorUtils.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ColorUtils.java index 14b18b00dc..1c0e439afc 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ColorUtils.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/ColorUtils.java @@ -5,6 +5,7 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.Drawable; +import android.os.Build; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.v4.graphics.drawable.DrawableCompat; @@ -30,10 +31,15 @@ public class ColorUtils { */ @ColorInt public static int getPrimaryColor(@NonNull Context context) { - TypedValue typedValue = new TypedValue(); - Resources.Theme theme = context.getTheme(); - theme.resolveAttribute(R.attr.colorPrimary, typedValue, true); - return typedValue.data; + try { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + int id = context.getResources().getIdentifier("colorPrimary", "attrs", context.getPackageName()); + theme.resolveAttribute(id, typedValue, true); + return typedValue.data; + } catch (Exception exception) { + return getColorCompat(context, R.color.mapbox_blue); + } } /** @@ -44,10 +50,15 @@ public class ColorUtils { */ @ColorInt public static int getPrimaryDarkColor(@NonNull Context context) { - TypedValue typedValue = new TypedValue(); - Resources.Theme theme = context.getTheme(); - theme.resolveAttribute(R.attr.colorPrimaryDark, typedValue, true); - return typedValue.data; + try { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + int id = context.getResources().getIdentifier("colorPrimaryDark", "attrs", context.getPackageName()); + theme.resolveAttribute(id, typedValue, true); + return typedValue.data; + } catch (Exception exception) { + return getColorCompat(context, R.color.mapbox_blue); + } } /** @@ -58,10 +69,15 @@ public class ColorUtils { */ @ColorInt public static int getAccentColor(@NonNull Context context) { - TypedValue typedValue = new TypedValue(); - Resources.Theme theme = context.getTheme(); - theme.resolveAttribute(R.attr.colorAccent, typedValue, true); - return typedValue.data; + try { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + int id = context.getResources().getIdentifier("colorAccent", "attrs", context.getPackageName()); + theme.resolveAttribute(id, typedValue, true); + return typedValue.data; + } catch (Exception exception) { + return getColorCompat(context, R.color.mapbox_gray); + } } /** @@ -122,4 +138,12 @@ public class ColorUtils { throw new ConversionException("Not a valid rgb/rgba value"); } } + + private static int getColorCompat(Context context, int id) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return context.getResources().getColor(id, context.getTheme()); + } else { + return context.getResources().getColor(id); + } + } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/res/values-bg/strings.xml b/platform/android/MapboxGLAndroidSDK/src/main/res/values-bg/strings.xml new file mode 100644 index 0000000000..262e94f368 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/res/values-bg/strings.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mapbox_compassContentDescription">Компас на картата. Активирай, за да насочиш картата на север.</string> + <string name="mapbox_attributionsIconContentDescription">Иконка функции. Активирай, за да покажеш диалог функции.</string> + <string name="mapbox_myLocationViewContentDescription">Изглед локация. Това показва местоположението ти на картата.</string> + <string name="mapbox_mapActionDescription">Показва карта създадена с Mapbox. Скролни с два пръста. Мащабирай с два пръста.</string> + <string name="mapbox_attributionsDialogTitle">Mapbox Android SDK</string> + <string name="mapbox_attributionTelemetryTitle">Направи Mapbox картите по-добри.</string> + <string name="mapbox_attributionTelemetryMessage">Помагаш OpenStreetMap и Mapbox картите да станат по-добри, като предоставяш анонимни данни за потребление.</string> + <string name="mapbox_attributionTelemetryPositive">Съгласявам се</string> + <string name="mapbox_attributionTelemetryNegative">Не се съгласявам</string> + <string name="mapbox_attributionTelemetryNeutral">Повече инфо</string> + <string name="mapbox_offline_error_region_definition_invalid">Предоставените OfflineRegionDefinition не пасват в границите на света: %s</string> + + </resources> diff --git a/platform/android/MapboxGLAndroidSDK/src/main/res/values-hu/strings.xml b/platform/android/MapboxGLAndroidSDK/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000000..520edb2733 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/res/values-hu/strings.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="mapbox_myLocationViewContentDescription">Hely nézet. Megmutatja, hol vagy a térképen.</string> + <string name="mapbox_mapActionDescription">Egy Mapbox-szal készült térkép megjelenítése. Húzd két ujjadat a görgetéshez. Csippentsd össze a nagyításhoz.</string> + <string name="mapbox_attributionsDialogTitle">Mapbox Android SDK</string> + <string name="mapbox_attributionTelemetryTitle">Tedd jobbá a Mapbox térképeket</string> + <string name="mapbox_attributionTelemetryMessage">Segítesz az OpenStreetMap és Mapbox térképek jobbá tételében névtelen felhasználási adatok elküldésével.</string> + <string name="mapbox_attributionTelemetryPositive">Egyetértek</string> + <string name="mapbox_attributionTelemetryNegative">Nem értek egyet</string> + <string name="mapbox_attributionTelemetryNeutral">Több infó</string> + <string name="mapbox_offline_error_region_definition_invalid">A megadott OfflineRegionDefinition nem fér bele a világ kereteibe: %s</string> + + </resources> diff --git a/platform/android/MapboxGLAndroidSDK/src/main/res/values/styles.xml b/platform/android/MapboxGLAndroidSDK/src/main/res/values/styles.xml deleted file mode 100644 index eba1fb3a7d..0000000000 --- a/platform/android/MapboxGLAndroidSDK/src/main/res/values/styles.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - - <style name="mapbox_AlertDialogStyle" parent="Theme.AppCompat.Light.Dialog.Alert"/> - -</resources> diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java index 8d9a360714..bb96c9939d 100644 --- a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java +++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java @@ -74,15 +74,6 @@ public class LatLngBoundsTest { } @Test - public void emptySpan() { - latLngBounds = new LatLngBounds.Builder() - .include(LAT_LNG_NOT_NULL_ISLAND) - .include(LAT_LNG_NOT_NULL_ISLAND) - .build(); - assertTrue("Should be empty", latLngBounds.isEmptySpan()); - } - - @Test public void notEmptySpan() { latLngBounds = new LatLngBounds.Builder() .include(LAT_LNG_NOT_NULL_ISLAND) diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/AnnotationManagerTest.java b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/AnnotationManagerTest.java index 0d592f9bb3..239ad7392b 100644 --- a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/AnnotationManagerTest.java +++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/AnnotationManagerTest.java @@ -10,6 +10,7 @@ import com.mapbox.mapboxsdk.annotations.MarkerViewManager; import com.mapbox.mapboxsdk.geometry.LatLng; import org.junit.Test; +import org.mockito.ArgumentMatchers; import java.util.ArrayList; import java.util.List; @@ -32,8 +33,9 @@ public class AnnotationManagerTest { Markers markers = new MarkerContainer(aNativeMapView, aMapView, annotationsArray, aIconManager, aMarkerViewManager); Polygons polygons = new PolygonContainer(aNativeMapView, annotationsArray); Polylines polylines = new PolylineContainer(aNativeMapView, annotationsArray); + ShapeAnnotations shapeAnnotations = new ShapeAnnotationContainer(aNativeMapView, annotationsArray); AnnotationManager annotationManager = new AnnotationManager(aNativeMapView, aMapView, annotationsArray, - aMarkerViewManager, aIconManager, annotations, markers, polygons, polylines); + aMarkerViewManager, aIconManager, annotations, markers, polygons, polylines, shapeAnnotations); Marker aMarker = mock(Marker.class); long aId = 5L; when(aNativeMapView.addMarker(aMarker)).thenReturn(aId); @@ -58,18 +60,23 @@ public class AnnotationManagerTest { Markers markers = new MarkerContainer(aNativeMapView, aMapView, annotationsArray, aIconManager, aMarkerViewManager); Polygons polygons = new PolygonContainer(aNativeMapView, annotationsArray); Polylines polylines = new PolylineContainer(aNativeMapView, annotationsArray); + ShapeAnnotations shapeAnnotations = new ShapeAnnotationContainer(aNativeMapView, annotationsArray); AnnotationManager annotationManager = new AnnotationManager(aNativeMapView, aMapView, annotationsArray, - aMarkerViewManager, aIconManager, annotations, markers, polygons, polylines); + aMarkerViewManager, aIconManager, annotations, markers, polygons, polylines, shapeAnnotations); long firstId = 1L; long secondId = 2L; List<BaseMarkerOptions> 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.<Marker>anyList())) + .thenReturn(new long[]{firstId, secondId}); + annotationManager.addMarkers(markerList, aMapboxMap); assertEquals(2, annotationManager.getAnnotations().size()); diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/CameraChangeDispatcherTest.java b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/CameraChangeDispatcherTest.java new file mode 100644 index 0000000000..090d274fe7 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/CameraChangeDispatcherTest.java @@ -0,0 +1,87 @@ +package com.mapbox.mapboxsdk.maps; + +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class CameraChangeDispatcherTest { + + @Test + public void testSetCameraIdleListener() { + CameraChangeDispatcher dispatcher = new CameraChangeDispatcher(); + MapboxMap.OnCameraIdleListener listener = mock(MapboxMap.OnCameraIdleListener.class); + dispatcher.setOnCameraIdleListener(listener); + dispatcher.onCameraMoveStarted(MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE); + dispatcher.onCameraIdle(); + verify(listener).onCameraIdle(); + } + + @Test + public void testSetCameraMoveStartedListener() { + CameraChangeDispatcher dispatcher = new CameraChangeDispatcher(); + MapboxMap.OnCameraMoveStartedListener listener = mock(MapboxMap.OnCameraMoveStartedListener.class); + dispatcher.setOnCameraMoveStartedListener(listener); + dispatcher.onCameraMoveStarted(MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE); + verify(listener).onCameraMoveStarted(MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE); + } + + @Test + public void testSetCameraMoveCancelListener() { + CameraChangeDispatcher dispatcher = new CameraChangeDispatcher(); + MapboxMap.OnCameraMoveCanceledListener listener = mock(MapboxMap.OnCameraMoveCanceledListener.class); + dispatcher.setOnCameraMoveCanceledListener(listener); + dispatcher.onCameraMoveStarted(MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE); + dispatcher.onCameraMoveCanceled(); + verify(listener).onCameraMoveCanceled(); + } + + @Test + public void testSetCameraMoveListener() { + CameraChangeDispatcher dispatcher = new CameraChangeDispatcher(); + MapboxMap.OnCameraMoveListener listener = mock(MapboxMap.OnCameraMoveListener.class); + dispatcher.setOnCameraMoveListener(listener); + dispatcher.onCameraMoveStarted(MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE); + dispatcher.onCameraMove(); + verify(listener).onCameraMove(); + } + + @Test + public void testAddCameraIdleListener() { + CameraChangeDispatcher dispatcher = new CameraChangeDispatcher(); + MapboxMap.OnCameraIdleListener listener = mock(MapboxMap.OnCameraIdleListener.class); + dispatcher.addOnCameraIdleListener(listener); + dispatcher.onCameraMoveStarted(MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE); + dispatcher.onCameraIdle(); + verify(listener).onCameraIdle(); + } + + @Test + public void testAddCameraMoveStartedListener() { + CameraChangeDispatcher dispatcher = new CameraChangeDispatcher(); + MapboxMap.OnCameraMoveStartedListener listener = mock(MapboxMap.OnCameraMoveStartedListener.class); + dispatcher.addOnCameraMoveStartedListener(listener); + dispatcher.onCameraMoveStarted(MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE); + verify(listener).onCameraMoveStarted(MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE); + } + + @Test + public void testAddCameraMoveCancelListener() { + CameraChangeDispatcher dispatcher = new CameraChangeDispatcher(); + MapboxMap.OnCameraMoveCanceledListener listener = mock(MapboxMap.OnCameraMoveCanceledListener.class); + dispatcher.addOnCameraMoveCancelListener(listener); + dispatcher.onCameraMoveStarted(MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE); + dispatcher.onCameraMoveCanceled(); + verify(listener).onCameraMoveCanceled(); + } + + @Test + public void testAddCameraMoveListener() { + CameraChangeDispatcher dispatcher = new CameraChangeDispatcher(); + MapboxMap.OnCameraMoveListener listener = mock(MapboxMap.OnCameraMoveListener.class); + dispatcher.addOnCameraMoveListener(listener); + dispatcher.onCameraMoveStarted(MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE); + dispatcher.onCameraMove(); + verify(listener).onCameraMove(); + } +} diff --git a/platform/android/MapboxGLAndroidSDKTestApp/build.gradle b/platform/android/MapboxGLAndroidSDKTestApp/build.gradle index 81ee635cf1..f45ad3dc3a 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/build.gradle +++ b/platform/android/MapboxGLAndroidSDKTestApp/build.gradle @@ -59,6 +59,7 @@ dependencies { // Support libraries compile rootProject.ext.dep.supportAppcompatV7 compile rootProject.ext.dep.supportRecyclerView + compile rootProject.ext.dep.supportDesign // Leak Canary debugCompile rootProject.ext.dep.leakCanaryDebug @@ -71,7 +72,6 @@ dependencies { compile rootProject.ext.dep.lost // Testing dependencies - androidTestCompile rootProject.ext.dep.testSpoonRunner androidTestCompile rootProject.ext.dep.supportAnnotations androidTestCompile rootProject.ext.dep.testRunner androidTestCompile rootProject.ext.dep.testRules @@ -81,8 +81,6 @@ dependencies { apply from: 'gradle-make.gradle' apply from: 'gradle-config.gradle' -apply from: 'gradle-device-farm.gradle' -apply from: 'gradle-spoon.gradle' apply from: 'gradle-checkstyle.gradle' apply from: '../gradle-lint.gradle' diff --git a/platform/android/MapboxGLAndroidSDKTestApp/gradle-device-farm.gradle b/platform/android/MapboxGLAndroidSDKTestApp/gradle-device-farm.gradle deleted file mode 100644 index 5cb5d75bbb..0000000000 --- a/platform/android/MapboxGLAndroidSDKTestApp/gradle-device-farm.gradle +++ /dev/null @@ -1,43 +0,0 @@ -apply plugin: 'devicefarm' - -def getAccessKeyDeviceFarm() { - if (project.hasProperty('AWS_ACCESS_KEY_ID_DEVICE_FARM')) { - return AWS_ACCESS_KEY_ID_DEVICE_FARM - } else { - println("Could not locate AWS_ACCESS_KEY_ID_DEVICE_FARM in gradle.properties") - return "" - } -} - -def getSecretAccessKeyDeviceFarm() { - if (project.hasProperty('AWS_SECRET_ACCESS_KEY_DEVICE_FARM')) { - return AWS_SECRET_ACCESS_KEY_DEVICE_FARM - } else { - println("Could not locate AWS_SECRET_ACCESS_KEY_DEVICE_FARM in gradle.properties") - return "" - } -} - -devicefarm { - - projectName "Mapbox GL Android" // required: Must already exists. - devicePool "sanity" // optional: Defaults to "Top Devices" - - authentication { - accessKey getAccessKeyDeviceFarm() - secretKey getSecretAccessKeyDeviceFarm() - } - - devicestate { - wifi "on" - bluetooth "off" - gps "on" - nfc "on" - latitude 47.6204 // default - longitude - 122.3491 // default - } - - instrumentation { - - } -} diff --git a/platform/android/MapboxGLAndroidSDKTestApp/gradle-spoon.gradle b/platform/android/MapboxGLAndroidSDKTestApp/gradle-spoon.gradle deleted file mode 100644 index 7a5b966443..0000000000 --- a/platform/android/MapboxGLAndroidSDKTestApp/gradle-spoon.gradle +++ /dev/null @@ -1,8 +0,0 @@ -println "configuring spoon" -apply plugin: 'spoon' -spoon { - // Spoon: distributes instrumentation tests to all your Androids - // for more options see https://github.com/stanfy/spoon-gradle-plugin - grantAllPermissions = true - debug = true -} diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxMapTest.java b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxMapTest.java index 294d57bce1..3dbbaceb1a 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxMapTest.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/maps/MapboxMapTest.java @@ -14,6 +14,7 @@ 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.CameraUpdateFactory; import com.mapbox.mapboxsdk.constants.MapboxConstants; import com.mapbox.mapboxsdk.exceptions.InvalidMarkerPositionException; import com.mapbox.mapboxsdk.geometry.LatLng; @@ -31,9 +32,12 @@ import org.junit.Test; import java.util.ArrayList; import java.util.List; +import timber.log.Timber; + import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static com.mapbox.mapboxsdk.testapp.utils.TestConstants.LAT_LNG_DELTA; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertNotNull; import static org.junit.Assert.assertEquals; @@ -90,9 +94,34 @@ public class MapboxMapTest extends BaseActivityTest { } // - // CameraForLatLngBounds + // Camera tests // @Test + public void testCameraPositionOnFinish() { + ViewUtils.checkViewIsDisplayed(R.id.mapView); + onView(withId(R.id.mapView)).perform(new MapboxMapAction(new InvokeViewAction() { + @Override + public void onViewAction(UiController uiController, View view) { + + final LatLng latLng = new LatLng(30.0, 30.0); + mapboxMap.moveCamera(CameraUpdateFactory.newLatLng(latLng), new MapboxMap.CancelableCallback() { + @Override + public void onCancel() { + } + + @Override + public void onFinish() { + LatLng cameraPositionLatLng = mapboxMap.getCameraPosition().target; + Timber.d(cameraPositionLatLng.toString()); + assertEquals(cameraPositionLatLng.getLatitude(), latLng.getLatitude(), LAT_LNG_DELTA); + assertEquals(cameraPositionLatLng.getLongitude(), latLng.getLongitude(), LAT_LNG_DELTA); + } + }); + } + })); + } + + @Test public void testCameraForLatLngBounds() { ViewUtils.checkViewIsDisplayed(R.id.mapView); onView(withId(R.id.mapView)).perform(new MapboxMapAction(new InvokeViewAction() { diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/BaseActivityTest.java b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/BaseActivityTest.java index c029bc09c4..61bff1f113 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/BaseActivityTest.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/activity/BaseActivityTest.java @@ -49,8 +49,9 @@ public abstract class BaseActivityTest { throw new RuntimeException("Could not start test for " + getActivityClass().getSimpleName() + ".\n" + "The ViewHierarchy doesn't contain a view with resource id = R.id.mapView or \n" + "the Activity doesn't contain an instance variable with a name equal to mapboxMap.\n" - + "You can resolve this issue be implementing the requirements above or\n add " - + getActivityClass().getSimpleName() + " to the excludeActivities array in `generate-test-code.js`.\n"); + + "You can resolve this issue by adding the requirements above or\n add " + + getActivityClass().getSimpleName() + " to the platform/android/scripts/exclude-activity-gen.json to blacklist" + + " the Activity from being generated.\n"); } } @@ -67,8 +68,7 @@ public abstract class BaseActivityTest { protected abstract Class getActivityClass(); protected void checkViewIsDisplayed(int id) { - onView(withId(id)) - .check(matches(isDisplayed())); + onView(withId(id)).check(matches(isDisplayed())); } protected void waitLoop() { diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/widgets/AttributionTest.java b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/widgets/AttributionTest.java index d37c6db2d5..a20426c29c 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/widgets/AttributionTest.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/maps/widgets/AttributionTest.java @@ -21,6 +21,7 @@ import com.mapbox.mapboxsdk.testapp.activity.espresso.EspressoTestActivity; import org.hamcrest.Matcher; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import static android.support.test.espresso.Espresso.onData; @@ -67,6 +68,7 @@ public class AttributionTest extends BaseActivityTest { } @Test + @Ignore public void testMapboxStreetsMapboxAttributionLink() { validateTestSetup(); if (urlSpans == null) { @@ -87,6 +89,7 @@ public class AttributionTest extends BaseActivityTest { } @Test + @Ignore public void testMapboxStreetsOpenStreetMapAttributionLink() { validateTestSetup(); if (urlSpans == null) { @@ -107,6 +110,7 @@ public class AttributionTest extends BaseActivityTest { } @Test + @Ignore public void testImproveMapLink() { validateTestSetup(); if (urlSpans == null) { diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/utils/OnMapReadyIdlingResource.java b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/utils/OnMapReadyIdlingResource.java index 1c4981ca5e..0e2e4587ee 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/utils/OnMapReadyIdlingResource.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/utils/OnMapReadyIdlingResource.java @@ -13,10 +13,8 @@ public class OnMapReadyIdlingResource implements IdlingResource, OnMapReadyCallb private MapboxMap mapboxMap; private IdlingResource.ResourceCallback resourceCallback; - private Activity activity; public OnMapReadyIdlingResource(Activity activity) { - this.activity = activity; try { Field field = activity.getClass().getDeclaredField("mapView"); field.setAccessible(true); @@ -24,7 +22,6 @@ public class OnMapReadyIdlingResource implements IdlingResource, OnMapReadyCallb } catch (Exception err) { throw new RuntimeException(err); } - } @Override @@ -53,4 +50,4 @@ public class OnMapReadyIdlingResource implements IdlingResource, OnMapReadyCallb resourceCallback.onTransitionToIdle(); } } -} +}
\ No newline at end of file diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/CameraAnimatorActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/CameraAnimatorActivity.java index cc44ac9715..c8c5c6bd37 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/CameraAnimatorActivity.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/CameraAnimatorActivity.java @@ -5,13 +5,20 @@ import android.animation.AnimatorSet; import android.animation.TypeEvaluator; import android.animation.ValueAnimator; import android.os.Bundle; +import android.support.v4.util.LongSparseArray; import android.support.v4.view.animation.FastOutLinearInInterpolator; import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.support.v4.view.animation.PathInterpolatorCompat; import android.support.v7.app.AppCompatActivity; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.animation.AnticipateOvershootInterpolator; +import android.view.animation.BounceInterpolator; +import android.view.animation.Interpolator; import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.maps.MapView; import com.mapbox.mapboxsdk.maps.MapboxMap; @@ -24,6 +31,51 @@ import com.mapbox.mapboxsdk.testapp.R; public class CameraAnimatorActivity extends AppCompatActivity implements OnMapReadyCallback { private static final double ANIMATION_DELAY_FACTOR = 1.5; + private static final LatLng START_LAT_LNG = new LatLng(37.787947, -122.407432); + + private final LongSparseArray<AnimatorBuilder> animators = new LongSparseArray<AnimatorBuilder>() { + { + put(R.id.menu_action_accelerate_decelerate_interpolator, new AnimatorBuilder() { + @Override + public Animator build() { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + createLatLngAnimator(START_LAT_LNG, new LatLng(37.826715, -122.422795)), + obtainExampleInterpolator(new FastOutSlowInInterpolator(), 2500) + ); + return animatorSet; + } + }); + + put(R.id.menu_action_bounce_interpolator, new AnimatorBuilder() { + @Override + public Animator build() { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + createLatLngAnimator(START_LAT_LNG, new LatLng(37.787947, -122.407432)), + obtainExampleInterpolator(new BounceInterpolator(), 3750) + ); + return animatorSet; + } + }); + + put(R.id.menu_action_anticipate_overshoot_interpolator, new AnimatorBuilder() { + @Override + public Animator build() { + return obtainExampleInterpolator(new AnticipateOvershootInterpolator(), 2500); + } + }); + + put(R.id.menu_action_path_interpolator, new AnimatorBuilder() { + @Override + public Animator build() { + return obtainExampleInterpolator( + PathInterpolatorCompat.create(.22f, .68f, 0, 1.71f), 2500); + } + }); + } + }; + private MapView mapView; private MapboxMap mapboxMap; @@ -32,7 +84,6 @@ public class CameraAnimatorActivity extends AppCompatActivity implements OnMapRe protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_camera_animator); - mapView = (MapView) findViewById(R.id.mapView); if (mapView != null) { mapView.onCreate(savedInstanceState); @@ -43,27 +94,42 @@ public class CameraAnimatorActivity extends AppCompatActivity implements OnMapRe @Override public void onMapReady(final MapboxMap map) { mapboxMap = map; + initFab(); + } + + private void initFab() { findViewById(R.id.fab).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { view.setVisibility(View.GONE); - createAnimator(mapboxMap.getCameraPosition()).start(); + + CameraPosition animatedPosition = new CameraPosition.Builder() + .target(new LatLng(37.789992, -122.402214)) + .tilt(60) + .zoom(14.5f) + .bearing(135) + .build(); + + createExampleAnimator(mapboxMap.getCameraPosition(), animatedPosition).start(); } }); } - private Animator createAnimator(CameraPosition currentPosition) { + // + // Animator API used for the animation on the FAB + // + + private Animator createExampleAnimator(CameraPosition currentPosition, CameraPosition targetPosition) { AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.play(createLatLngAnimator(currentPosition.target)); - animatorSet.play(createZoomAnimator(currentPosition.zoom)); - animatorSet.play(createBearingAnimator(currentPosition.bearing)); - animatorSet.play(createTiltAnimator(currentPosition.tilt)); + animatorSet.play(createLatLngAnimator(currentPosition.target, targetPosition.target)); + animatorSet.play(createZoomAnimator(currentPosition.zoom, targetPosition.zoom)); + animatorSet.play(createBearingAnimator(currentPosition.bearing, targetPosition.bearing)); + animatorSet.play(createTiltAnimator(currentPosition.tilt, targetPosition.tilt)); return animatorSet; } - private Animator createLatLngAnimator(LatLng currentPosition) { - LatLng target = new LatLng(37.789992, -122.402214); - ValueAnimator latLngAnimator = ValueAnimator.ofObject(new LatLngEvaluator(), currentPosition, target); + private Animator createLatLngAnimator(LatLng currentPosition, LatLng targetPosition) { + ValueAnimator latLngAnimator = ValueAnimator.ofObject(new LatLngEvaluator(), currentPosition, targetPosition); latLngAnimator.setDuration((long) (1000 * ANIMATION_DELAY_FACTOR)); latLngAnimator.setInterpolator(new FastOutSlowInInterpolator()); latLngAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @@ -75,8 +141,8 @@ public class CameraAnimatorActivity extends AppCompatActivity implements OnMapRe return latLngAnimator; } - private Animator createZoomAnimator(double currentZoom) { - ValueAnimator zoomAnimator = ValueAnimator.ofFloat((float) currentZoom, 14.5f); + private Animator createZoomAnimator(double currentZoom, double targetZoom) { + ValueAnimator zoomAnimator = ValueAnimator.ofFloat((float) currentZoom, (float) targetZoom); zoomAnimator.setDuration((long) (2200 * ANIMATION_DELAY_FACTOR)); zoomAnimator.setStartDelay((long) (600 * ANIMATION_DELAY_FACTOR)); zoomAnimator.setInterpolator(new AnticipateOvershootInterpolator()); @@ -89,8 +155,8 @@ public class CameraAnimatorActivity extends AppCompatActivity implements OnMapRe return zoomAnimator; } - private Animator createBearingAnimator(double currentBearing) { - ValueAnimator bearingAnimator = ValueAnimator.ofFloat((float) currentBearing, 135); + private Animator createBearingAnimator(double currentBearing, double targetBearing) { + ValueAnimator bearingAnimator = ValueAnimator.ofFloat((float) currentBearing, (float) targetBearing); bearingAnimator.setDuration((long) (1000 * ANIMATION_DELAY_FACTOR)); bearingAnimator.setStartDelay((long) (1000 * ANIMATION_DELAY_FACTOR)); bearingAnimator.setInterpolator(new FastOutLinearInInterpolator()); @@ -103,8 +169,8 @@ public class CameraAnimatorActivity extends AppCompatActivity implements OnMapRe return bearingAnimator; } - private Animator createTiltAnimator(double currentTilt) { - ValueAnimator tiltAnimator = ValueAnimator.ofFloat((float) currentTilt, 60); + private Animator createTiltAnimator(double currentTilt, double targetTilt) { + ValueAnimator tiltAnimator = ValueAnimator.ofFloat((float) currentTilt, (float) targetTilt); tiltAnimator.setDuration((long) (1000 * ANIMATION_DELAY_FACTOR)); tiltAnimator.setStartDelay((long) (1500 * ANIMATION_DELAY_FACTOR)); tiltAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @@ -116,20 +182,66 @@ public class CameraAnimatorActivity extends AppCompatActivity implements OnMapRe return tiltAnimator; } - private static class LatLngEvaluator implements TypeEvaluator<LatLng> { + // + // Interpolator examples + // - private final LatLng latLng = new LatLng(); + private Animator obtainExampleInterpolator(int menuItemId) { + return animators.get(menuItemId).build(); + } - @Override - public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) { - latLng.setLatitude(startValue.getLatitude() - + ((endValue.getLatitude() - startValue.getLatitude()) * fraction)); - latLng.setLongitude(startValue.getLongitude() - + ((endValue.getLongitude() - startValue.getLongitude()) * fraction)); - return latLng; + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_animator, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (mapboxMap == null) { + return false; } + findViewById(R.id.fab).setVisibility(View.GONE); + resetCameraPosition(); + playAnimation(item.getItemId()); + return super.onOptionsItemSelected(item); } + private void resetCameraPosition() { + mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition( + new CameraPosition.Builder() + .target(START_LAT_LNG) + .zoom(11) + .bearing(0) + .tilt(0) + .build() + )); + } + + private void playAnimation(int itemId) { + Animator animator = obtainExampleInterpolator(itemId); + if (animator != null) { + animator.start(); + } + } + + private Animator obtainExampleInterpolator(Interpolator interpolator, long duration) { + ValueAnimator zoomAnimator = ValueAnimator.ofFloat(11.0f, 16.0f); + zoomAnimator.setDuration((long) (duration * ANIMATION_DELAY_FACTOR)); + zoomAnimator.setInterpolator(interpolator); + zoomAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mapboxMap.setZoom((Float) animation.getAnimatedValue()); + } + }); + return zoomAnimator; + } + + // + // MapView lifecycle + // + @Override protected void onStart() { super.onStart(); @@ -171,4 +283,25 @@ public class CameraAnimatorActivity extends AppCompatActivity implements OnMapRe super.onLowMemory(); mapView.onLowMemory(); } + + /** + * Helper class to evaluate LatLng objects with a ValueAnimator + */ + private static class LatLngEvaluator implements TypeEvaluator<LatLng> { + + private final LatLng latLng = new LatLng(); + + @Override + public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) { + latLng.setLatitude(startValue.getLatitude() + + ((endValue.getLatitude() - startValue.getLatitude()) * fraction)); + latLng.setLongitude(startValue.getLongitude() + + ((endValue.getLongitude() - startValue.getLongitude()) * fraction)); + return latLng; + } + } + + interface AnimatorBuilder { + Animator build(); + } } diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/CameraPositionActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/CameraPositionActivity.java index 2820bdbd53..e5c6a3f584 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/CameraPositionActivity.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/camera/CameraPositionActivity.java @@ -1,6 +1,5 @@ package com.mapbox.mapboxsdk.testapp.activity.camera; -import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; @@ -28,11 +27,13 @@ import timber.log.Timber; /** * Test activity showcasing how to listen to camera change events. */ -public class CameraPositionActivity extends AppCompatActivity implements OnMapReadyCallback { +public class CameraPositionActivity extends AppCompatActivity implements OnMapReadyCallback, View.OnClickListener, + MapboxMap.OnMapLongClickListener { private MapView mapView; private MapboxMap mapboxMap; private FloatingActionButton fab; + private boolean logCameraChanges; @Override protected void onCreate(Bundle savedInstanceState) { @@ -47,96 +48,48 @@ public class CameraPositionActivity extends AppCompatActivity implements OnMapRe @Override public void onMapReady(@NonNull final MapboxMap map) { mapboxMap = map; - - mapboxMap.setOnCameraIdleListener(new MapboxMap.OnCameraIdleListener() { - @Override - public void onCameraIdle() { - Timber.e("OnCameraIdle"); - fab.setColorFilter(ContextCompat.getColor(CameraPositionActivity.this, android.R.color.holo_green_dark)); - } - }); - - mapboxMap.setOnCameraMoveCancelListener(new MapboxMap.OnCameraMoveCanceledListener() { - @Override - public void onCameraMoveCanceled() { - Timber.e("OnCameraMoveCanceled"); - } - }); - - mapboxMap.setOnCameraMoveListener(new MapboxMap.OnCameraMoveListener() { - @Override - public void onCameraMove() { - Timber.e("OnCameraMove"); - fab.setColorFilter(ContextCompat.getColor(CameraPositionActivity.this, android.R.color.holo_orange_dark)); - } - }); - - mapboxMap.setOnCameraMoveStartedListener(new MapboxMap.OnCameraMoveStartedListener() { - - private final String[] REASONS = {"REASON_API_GESTURE", "REASON_DEVELOPER_ANIMATION", "REASON_API_ANIMATION"}; - - @Override - public void onCameraMoveStarted(int reason) { - // reason ranges from 1 <-> 3 - fab.setColorFilter(ContextCompat.getColor(CameraPositionActivity.this, android.R.color.holo_red_dark)); - Timber.e("OnCameraMoveStarted: %s", REASONS[reason - 1]); - } - }); + toggleLogCameraChanges(); // add a listener to FAB fab = (FloatingActionButton) findViewById(R.id.fab); fab.setColorFilter(ContextCompat.getColor(CameraPositionActivity.this, R.color.primary)); - fab.setOnClickListener(new View.OnClickListener() { - @SuppressLint("InflateParams") - @Override - public void onClick(View view) { - Context context = view.getContext(); - final View dialogContent = LayoutInflater.from(context).inflate(R.layout.dialog_camera_position, null); - AlertDialog.Builder builder = new AlertDialog.Builder( - context, com.mapbox.mapboxsdk.R.style.mapbox_AlertDialogStyle); - builder.setTitle(R.string.dialog_camera_position); - builder.setView(onInflateDialogContent(dialogContent)); - builder.setPositiveButton("Animate", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - double latitude = Double.parseDouble( - ((TextView) dialogContent.findViewById(R.id.value_lat)).getText().toString()); - double longitude = Double.parseDouble( - ((TextView) dialogContent.findViewById(R.id.value_lon)).getText().toString()); - double zoom = Double.parseDouble( - ((TextView) dialogContent.findViewById(R.id.value_zoom)).getText().toString()); - double bearing = Double.parseDouble( - ((TextView) dialogContent.findViewById(R.id.value_bearing)).getText().toString()); - double tilt = Double.parseDouble( - ((TextView) dialogContent.findViewById(R.id.value_tilt)).getText().toString()); - - CameraPosition cameraPosition = new CameraPosition.Builder() - .target(new LatLng(latitude, longitude)) - .zoom(zoom) - .bearing(bearing) - .tilt(tilt) - .build(); - - mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition), 5000, - new MapboxMap.CancelableCallback() { - @Override - public void onCancel() { - Timber.v("OnCancel called"); - } - - @Override - public void onFinish() { - Timber.v("OnFinish called"); - } - }); - Timber.v(cameraPosition.toString()); - } - }); - builder.setNegativeButton("Cancel", null); - builder.setCancelable(false); - builder.show(); - } - }); + fab.setOnClickListener(this); + + // listen to long click events to toggle logging camera changes + mapboxMap.setOnMapLongClickListener(this); + } + + @Override + public void onMapLongClick(@NonNull LatLng point) { + toggleLogCameraChanges(); + } + + @Override + public void onClick(View view) { + Context context = view.getContext(); + final View dialogContent = LayoutInflater.from(context).inflate(R.layout.dialog_camera_position, null); + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.dialog_camera_position); + builder.setView(onInflateDialogContent(dialogContent)); + builder.setPositiveButton("Animate", new DialogClickListener(mapboxMap, dialogContent)); + builder.setNegativeButton("Cancel", null); + builder.setCancelable(false); + builder.show(); + } + + private void toggleLogCameraChanges() { + logCameraChanges = !logCameraChanges; + if (logCameraChanges) { + mapboxMap.addOnCameraIdleListener(idleListener); + mapboxMap.addOnCameraMoveCancelListener(moveCanceledListener); + mapboxMap.addOnCameraMoveListener(moveListener); + mapboxMap.addOnCameraMoveStartedListener(moveStartedListener); + } else { + mapboxMap.removeOnCameraIdleListener(idleListener); + mapboxMap.removeOnCameraMoveCancelListener(moveCanceledListener); + mapboxMap.removeOnCameraMoveListener(moveListener); + mapboxMap.removeOnCameraMoveStartedListener(moveStartedListener); + } } @Override @@ -193,6 +146,42 @@ public class CameraPositionActivity extends AppCompatActivity implements OnMapRe seekBar.setProgress(defaultValue); } + private MapboxMap.OnCameraIdleListener idleListener = new MapboxMap.OnCameraIdleListener() { + @Override + public void onCameraIdle() { + Timber.e("OnCameraIdle"); + fab.setColorFilter(ContextCompat.getColor(CameraPositionActivity.this, android.R.color.holo_green_dark)); + } + }; + + private MapboxMap.OnCameraMoveListener moveListener = new MapboxMap.OnCameraMoveListener() { + @Override + public void onCameraMove() { + Timber.e("OnCameraMove"); + fab.setColorFilter(ContextCompat.getColor(CameraPositionActivity.this, android.R.color.holo_orange_dark)); + } + }; + + private MapboxMap.OnCameraMoveCanceledListener moveCanceledListener = new MapboxMap.OnCameraMoveCanceledListener() { + @Override + public void onCameraMoveCanceled() { + Timber.e("OnCameraMoveCanceled"); + + } + }; + + private MapboxMap.OnCameraMoveStartedListener moveStartedListener = new MapboxMap.OnCameraMoveStartedListener() { + + private final String[] REASONS = {"REASON_API_GESTURE", "REASON_DEVELOPER_ANIMATION", "REASON_API_ANIMATION"}; + + @Override + public void onCameraMoveStarted(int reason) { + // reason ranges from 1 <-> 3 + fab.setColorFilter(ContextCompat.getColor(CameraPositionActivity.this, android.R.color.holo_red_dark)); + Timber.e("OnCameraMoveStarted: %s", REASONS[reason - 1]); + } + }; + private class ValueChangeListener implements SeekBar.OnSeekBarChangeListener { protected TextView textView; @@ -224,4 +213,50 @@ public class CameraPositionActivity extends AppCompatActivity implements OnMapRe super.onProgressChanged(seekBar, progress - 180, fromUser); } } + + private static class DialogClickListener implements DialogInterface.OnClickListener { + + private MapboxMap mapboxMap; + private View dialogContent; + + public DialogClickListener(MapboxMap mapboxMap, View view) { + this.mapboxMap = mapboxMap; + this.dialogContent = view; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + double latitude = Double.parseDouble( + ((TextView) dialogContent.findViewById(R.id.value_lat)).getText().toString()); + double longitude = Double.parseDouble( + ((TextView) dialogContent.findViewById(R.id.value_lon)).getText().toString()); + double zoom = Double.parseDouble( + ((TextView) dialogContent.findViewById(R.id.value_zoom)).getText().toString()); + double bearing = Double.parseDouble( + ((TextView) dialogContent.findViewById(R.id.value_bearing)).getText().toString()); + double tilt = Double.parseDouble( + ((TextView) dialogContent.findViewById(R.id.value_tilt)).getText().toString()); + + CameraPosition cameraPosition = new CameraPosition.Builder() + .target(new LatLng(latitude, longitude)) + .zoom(zoom) + .bearing(bearing) + .tilt(tilt) + .build(); + + mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition), 5000, + new MapboxMap.CancelableCallback() { + @Override + public void onCancel() { + Timber.v("OnCancel called"); + } + + @Override + public void onFinish() { + Timber.v("OnFinish called"); + } + }); + Timber.v(cameraPosition.toString()); + } + } } diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/OfflineActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/OfflineActivity.java index 5bffd4d930..3a59e0628d 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/OfflineActivity.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/OfflineActivity.java @@ -264,6 +264,7 @@ public class OfflineActivity extends AppCompatActivity if (status.isComplete()) { // Download complete endProgress("Region downloaded successfully."); + offlineRegion.setDownloadState(OfflineRegion.STATE_INACTIVE); offlineRegion.setObserver(null); return; } else if (status.isRequiredResourceCountPrecise()) { @@ -281,11 +282,13 @@ public class OfflineActivity extends AppCompatActivity @Override public void onError(OfflineRegionError error) { Timber.e("onError: %s, %s", error.getReason(), error.getMessage()); + offlineRegion.setDownloadState(OfflineRegion.STATE_INACTIVE); } @Override public void mapboxTileCountLimitExceeded(long limit) { Timber.e("Mapbox tile count limit exceeded: %s", limit); + offlineRegion.setDownloadState(OfflineRegion.STATE_INACTIVE); } }); diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolGeneratorActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolGeneratorActivity.java index 0eaccfef0c..6e9e45cfaa 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolGeneratorActivity.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolGeneratorActivity.java @@ -1,13 +1,14 @@ package com.mapbox.mapboxsdk.testapp.activity.style; +import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.support.v7.app.AppCompatActivity; import android.graphics.Color; import android.graphics.PointF; +import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.NonNull; - +import android.support.v7.app.AppCompatActivity; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -32,7 +33,7 @@ import com.mapbox.services.commons.geojson.custom.PositionDeserializer; import com.mapbox.services.commons.models.Position; import java.io.IOException; - +import java.util.HashMap; import java.util.List; import timber.log.Timber; @@ -64,49 +65,10 @@ public class SymbolGeneratorActivity extends AppCompatActivity implements OnMapR } @Override - public void onMapReady(MapboxMap map) { + public void onMapReady(final MapboxMap map) { mapboxMap = map; - try { - // read local geojson from raw folder - String tinyCountriesJson = ResourceUtils.readRawResource(this, R.raw.tiny_countries); - - // convert geojson to a model - FeatureCollection featureCollection = new GsonBuilder() - .registerTypeAdapter(Geometry.class, new GeometryDeserializer()) - .registerTypeAdapter(Position.class, new PositionDeserializer()) - .create().fromJson(tinyCountriesJson, FeatureCollection.class); - - // add a geojson to the map - Source source = new GeoJsonSource(SOURCE_ID, featureCollection); - mapboxMap.addSource(source); - - // for each feature add a symbolLayer - for (Feature feature : featureCollection.getFeatures()) { - String countryName = feature.getStringProperty(FEATURE_ID); - - // create View - TextView textView = new TextView(this); - textView.setBackgroundColor(getResources().getColor(R.color.blueAccent)); - textView.setPadding(10, 5, 10, 5); - textView.setTextColor(Color.WHITE); - textView.setText(countryName); - - // create bitmap from view - mapboxMap.addImage(countryName, SymbolGenerator.generate(textView)); - } - - // create layer use - mapboxMap.addLayer(new SymbolLayer(LAYER_ID, SOURCE_ID) - .withProperties( - iconImage("{" + FEATURE_ID + "}"), // { } is a token notation - iconAllowOverlap(false) - ) - ); - - addSymbolClickListener(); - } catch (IOException exception) { - Timber.e(exception); - } + addSymbolClickListener(); + new LoadDataTask(map, SymbolGeneratorActivity.this).execute(); } private void addSymbolClickListener() { @@ -213,4 +175,91 @@ public class SymbolGeneratorActivity extends AppCompatActivity implements OnMapR return bitmap; } } -} + + private static class LoadDataTask extends AsyncTask<Void, Void, FeatureCollection> { + + private final MapboxMap mapboxMap; + private final Context context; + + LoadDataTask(MapboxMap mapboxMap, Context context) { + this.mapboxMap = mapboxMap; + this.context = context; + } + + @Override + protected FeatureCollection doInBackground(Void... params) { + try { + // read local geojson from raw folder + String tinyCountriesJson = ResourceUtils.readRawResource(context, R.raw.tiny_countries); + + // convert geojson to a model + FeatureCollection featureCollection = new GsonBuilder() + .registerTypeAdapter(Geometry.class, new GeometryDeserializer()) + .registerTypeAdapter(Position.class, new PositionDeserializer()) + .create().fromJson(tinyCountriesJson, FeatureCollection.class); + + return featureCollection; + } catch (IOException exception) { + return null; + } + } + + + @Override + protected void onPostExecute(FeatureCollection featureCollection) { + super.onPostExecute(featureCollection); + if (featureCollection == null) { + return; + } + + // add a geojson to the map + Source source = new GeoJsonSource(SOURCE_ID, featureCollection); + mapboxMap.addSource(source); + + // create layer use + mapboxMap.addLayer(new SymbolLayer(LAYER_ID, SOURCE_ID) + .withProperties( + iconImage("{" + FEATURE_ID + "}"), // { } is a token notation + iconAllowOverlap(false) + ) + ); + + new GenerateSymbolTask(mapboxMap, context).execute(featureCollection); + } + } + + private static class GenerateSymbolTask extends AsyncTask<FeatureCollection, Void, HashMap<String, Bitmap>> { + + private MapboxMap mapboxMap; + private Context context; + + GenerateSymbolTask(MapboxMap mapboxMap, Context context) { + this.mapboxMap = mapboxMap; + this.context = context; + } + + @SuppressWarnings("WrongThread") + @Override + protected HashMap<String, Bitmap> doInBackground(FeatureCollection... params) { + FeatureCollection featureCollection = params[0]; + + HashMap<String, Bitmap> imagesMap = new HashMap<>(); + for (Feature feature : featureCollection.getFeatures()) { + String countryName = feature.getStringProperty(FEATURE_ID); + TextView textView = new TextView(context); + textView.setBackgroundColor(context.getResources().getColor(R.color.blueAccent)); + textView.setPadding(10, 5, 10, 5); + textView.setTextColor(Color.WHITE); + textView.setText(countryName); + imagesMap.put(countryName, SymbolGenerator.generate(textView)); + } + return imagesMap; + } + + @Override + protected void onPostExecute(HashMap<String, Bitmap> bitmapHashMap) { + super.onPostExecute(bitmapHashMap); + mapboxMap.addImages(bitmapHashMap); + } + } +}
\ No newline at end of file diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/ZoomFunctionSymbolLayerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/ZoomFunctionSymbolLayerActivity.java index 95c3929c1d..abfd7ae529 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/ZoomFunctionSymbolLayerActivity.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/ZoomFunctionSymbolLayerActivity.java @@ -12,6 +12,7 @@ import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.maps.MapView; import com.mapbox.mapboxsdk.maps.MapboxMap; import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; +import com.mapbox.mapboxsdk.style.layers.Property; import com.mapbox.mapboxsdk.style.layers.SymbolLayer; import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; import com.mapbox.mapboxsdk.testapp.R; @@ -32,6 +33,7 @@ import static com.mapbox.mapboxsdk.style.functions.stops.Stops.interval; import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap; import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage; import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility; /** * Test activity showcasing changing the icon with a zoom function and adding selection state to a SymbolLayer. @@ -49,9 +51,11 @@ public class ZoomFunctionSymbolLayerActivity extends AppCompatActivity { private MapView mapView; private MapboxMap mapboxMap; private GeoJsonSource source; + private SymbolLayer layer; private boolean isInitialPosition = true; private boolean isSelected = false; + private boolean isShowingSymbolLayer = true; @Override public void onCreate(Bundle savedInstanceState) { @@ -81,6 +85,13 @@ public class ZoomFunctionSymbolLayerActivity extends AppCompatActivity { } } + private void toggleSymbolLayerVisibility() { + layer.setProperties( + visibility(isShowingSymbolLayer ? Property.NONE : Property.VISIBLE) + ); + isShowingSymbolLayer = !isShowingSymbolLayer; + } + private FeatureCollection createFeatureCollection() { Position position = isInitialPosition ? Position.fromCoordinates(-74.01618140, 40.701745) @@ -95,7 +106,7 @@ public class ZoomFunctionSymbolLayerActivity extends AppCompatActivity { } private void addLayer() { - SymbolLayer layer = new SymbolLayer(LAYER_ID, SOURCE_ID); + layer = new SymbolLayer(LAYER_ID, SOURCE_ID); layer.setProperties( iconImage( zoom( @@ -145,9 +156,13 @@ public class ZoomFunctionSymbolLayerActivity extends AppCompatActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { - if (mapboxMap != null && item.getItemId() == R.id.menu_action_change_location) { - isInitialPosition = !isInitialPosition; - updateSource(); + if (mapboxMap != null) { + if (item.getItemId() == R.id.menu_action_change_location) { + isInitialPosition = !isInitialPosition; + updateSource(); + } else if (item.getItemId() == R.id.menu_action_toggle_source) { + toggleSymbolLayerVisibility(); + } } return super.onOptionsItemSelected(item); } diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_camera_animator.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_camera_animator.xml index d4933bfb9a..cb14aab91f 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_camera_animator.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_camera_animator.xml @@ -10,8 +10,8 @@ android:id="@id/mapView" android:layout_width="match_parent" android:layout_height="match_parent" - app:mapbox_cameraTargetLat="37.774913" - app:mapbox_cameraTargetLng="-122.419368" + app:mapbox_cameraTargetLat="37.787947" + app:mapbox_cameraTargetLng="-122.407432" app:mapbox_cameraZoom="11" app:mapbox_styleUrl="@string/mapbox_style_mapbox_streets"/> diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_animator.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_animator.xml new file mode 100644 index 0000000000..db5a62d2cb --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_animator.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/menu_action_accelerate_decelerate_interpolator" + android:title="@string/menuitem_title_accelerate_decelerate" + app:showAsAction="never"/> + <item + android:id="@+id/menu_action_bounce_interpolator" + android:title="@string/menuitem_title_bounce" + app:showAsAction="never"/> + <item + android:id="@+id/menu_action_anticipate_overshoot_interpolator" + android:title="@string/menuitem_title_anticipate_overshoot" + app:showAsAction="never"/> + <item + android:id="@+id/menu_action_path_interpolator" + android:title="@string/menuitem_title_path" + app:showAsAction="never"/> +</menu> diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_symbols.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_symbols.xml index 3e5c8ab14c..7f3c44262d 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_symbols.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_symbols.xml @@ -5,4 +5,8 @@ android:id="@+id/menu_action_change_location" android:title="@string/menuitem_change_location" app:showAsAction="never"/> + <item + android:id="@+id/menu_action_toggle_source" + android:title="@string/menuitem_toggle_symbol_layer_visibility" + app:showAsAction="never"/> </menu> diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/actions.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/actions.xml index 4ca19def71..5c0828ab74 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/actions.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/actions.xml @@ -12,6 +12,11 @@ <string name="menuitem_title_change_location_source_null">Reset location source to null</string> <string name="menuitem_change_icon_overlap">Toggle icon overlap</string> <string name="menuitem_change_location">Change location</string> + <string name="menuitem_title_accelerate_decelerate">Accelerate/Decelerate interpolator</string> + <string name="menuitem_title_bounce">Bounce interpolator</string> + <string name="menuitem_title_anticipate_overshoot">Anticipate/Overshoot interpolator</string> + <string name="menuitem_title_path">PathInterpolator interpolator</string> + <string name="menuitem_toggle_symbol_layer_visibility">Toggle Symbol Layer Visibility</string> <string name="button_camera_move">Move</string> <string name="button_camera_ease">Ease</string> <string name="button_camera_animate">Animate</string> diff --git a/platform/android/build.gradle b/platform/android/build.gradle index e298b84da8..f3301d0b5a 100644 --- a/platform/android/build.gradle +++ b/platform/android/build.gradle @@ -4,8 +4,6 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:2.3.1' - classpath 'com.amazonaws:aws-devicefarm-gradle-plugin:1.2' - classpath 'com.stanfy.spoon:spoon-gradle-plugin:1.2.1' } } diff --git a/platform/android/config.cmake b/platform/android/config.cmake index 4af7a4293d..b3888a9418 100644 --- a/platform/android/config.cmake +++ b/platform/android/config.cmake @@ -142,6 +142,8 @@ add_library(mbgl-android STATIC platform/android/src/style/conversion/types_string_values.hpp platform/android/src/map/camera_position.cpp platform/android/src/map/camera_position.hpp + platform/android/src/map/image.cpp + platform/android/src/map/image.hpp # Style conversion Java -> C++ platform/android/src/style/android_conversion.hpp diff --git a/platform/android/dependencies.gradle b/platform/android/dependencies.gradle index 650cf38952..8d4c32045c 100644 --- a/platform/android/dependencies.gradle +++ b/platform/android/dependencies.gradle @@ -27,7 +27,6 @@ ext { mockito : 'org.mockito:mockito-core:2.10.0', // instrumentation test - testSpoonRunner : 'com.squareup.spoon:spoon-client:1.7.1', testRunner : "com.android.support.test:runner:${testRunnerVersion}", testRules : "com.android.support.test:rules:${testRunnerVersion}", testEspressoCore : "com.android.support.test.espresso:espresso-core:${espressoVersion}", @@ -36,7 +35,7 @@ ext { // support supportAnnotations : "com.android.support:support-annotations:${supportLibVersion}", supportAppcompatV7 : "com.android.support:appcompat-v7:${supportLibVersion}", - supportV4 : "com.android.support:support-v4:${supportLibVersion}", + supportFragmentV4 : "com.android.support:support-fragment:${supportLibVersion}", supportDesign : "com.android.support:design:${supportLibVersion}", supportRecyclerView : "com.android.support:recyclerview-v7:${supportLibVersion}", diff --git a/platform/android/scripts/exclude-activity-gen.json b/platform/android/scripts/exclude-activity-gen.json new file mode 100644 index 0000000000..f05001c6ae --- /dev/null +++ b/platform/android/scripts/exclude-activity-gen.json @@ -0,0 +1,31 @@ +[ + "BaseLocationActivity", + "MapSnapshotterMarkerActivity", + "MapSnapshotterReuseActivity", + "LatLngBoundsActivity", + "BottomSheetActivity", + "MapSnapshotterActivity", + "MockLocationEngine", + "DeleteRegionActivity", + "RealTimeGeoJsonActivity", + "UpdateMetadataActivity", + "CarDrivingActivity", + "MyLocationTrackingModeActivity", + "MyLocationToggleActivity", + "MyLocationTintActivity", + "MyLocationDrawableActivity", + "DoubleMapActivity", + "LocationPickerActivity", + "GeoJsonClusteringActivity", + "RuntimeStyleTestActivity", + "AnimatedMarkerActivity", + "ViewPagerActivity", + "MapFragmentActivity", + "SupportMapFragmentActivity", + "SnapshotActivity", + "NavigationDrawerActivity", + "QueryRenderedFeaturesBoxHighlightActivity", + "MultiMapActivity", + "MapInDialogActivity", + "SimpleMapActivity" +]
\ No newline at end of file diff --git a/platform/android/scripts/generate-test-code.js b/platform/android/scripts/generate-test-code.js index 01a294a301..e27de7e40b 100644 --- a/platform/android/scripts/generate-test-code.js +++ b/platform/android/scripts/generate-test-code.js @@ -13,8 +13,7 @@ global.camelize = function (str) { }); } - -const excludeActivities = ["BaseLocationActivity","MockLocationEngine","DeleteRegionActivity","RealTimeGeoJsonActivity","UpdateMetadataActivity","CarDrivingActivity","MyLocationTrackingModeActivity","MyLocationToggleActivity","MyLocationTintActivity","MyLocationDrawableActivity","DoubleMapActivity", "LocationPickerActivity","GeoJsonClusteringActivity","RuntimeStyleTestActivity", "AnimatedMarkerActivity", "ViewPagerActivity","MapFragmentActivity","SupportMapFragmentActivity","SnapshotActivity","NavigationDrawerActivity", "QueryRenderedFeaturesBoxHighlightActivity", "MultiMapActivity", "MapInDialogActivity", "SimpleMapActivity"]; +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); @@ -24,7 +23,9 @@ if (!fs.existsSync(testBasePath)){ fs.mkdirSync(testBasePath); } -console.log("Generating test activities:"); +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); @@ -45,18 +46,22 @@ for(const subPackage of subPackages) { try { fs.accessSync(filePath, fs.F_OK); fs.unlinkSync(filePath); - console.log("Removed file: "+filePath); } catch (e) { - console.log("No file found: "+filePath); } - // only generate test file if not part of exclude list - if (!(excludeActivities.indexOf(activityName) > -1)) { - console.log("Created file: "+filePath); + // 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{ - console.log("Excluding file: "+filePath); + 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/platform/android/src/android_renderer_frontend.cpp b/platform/android/src/android_renderer_frontend.cpp index b80e23e21f..afdb08a10e 100644 --- a/platform/android/src/android_renderer_frontend.cpp +++ b/platform/android/src/android_renderer_frontend.cpp @@ -112,6 +112,11 @@ AnnotationIDs AndroidRendererFrontend::queryPointAnnotations(const ScreenBox& bo 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(); +} + } // namespace android } // namespace mbgl diff --git a/platform/android/src/android_renderer_frontend.hpp b/platform/android/src/android_renderer_frontend.hpp index 94508fd816..178870c452 100644 --- a/platform/android/src/android_renderer_frontend.hpp +++ b/platform/android/src/android_renderer_frontend.hpp @@ -36,6 +36,7 @@ public: std::vector<Feature> queryRenderedFeatures(const ScreenBox&, const RenderedQueryOptions&) const; std::vector<Feature> querySourceFeatures(const std::string& sourceID, const SourceQueryOptions&) const; AnnotationIDs queryPointAnnotations(const ScreenBox& box) const; + AnnotationIDs queryShapeAnnotations(const ScreenBox& box) const; // Memory void onLowMemory(); diff --git a/platform/android/src/file_source.cpp b/platform/android/src/file_source.cpp index 262e3d3c6a..a576661a4f 100644 --- a/platform/android/src/file_source.cpp +++ b/platform/android/src/file_source.cpp @@ -8,8 +8,6 @@ #include "asset_manager_file_source.hpp" #include "jni/generic_global_ref_deleter.hpp" -#include <string> - namespace mbgl { namespace android { @@ -64,6 +62,14 @@ void FileSource::setResourceTransform(jni::JNIEnv& env, jni::Object<FileSource:: } } +void FileSource::resume(jni::JNIEnv&) { + fileSource->resume(); +} + +void FileSource::pause(jni::JNIEnv&) { + fileSource->pause(); +} + jni::Class<FileSource> FileSource::javaClass; FileSource* FileSource::getNativePeer(jni::JNIEnv& env, jni::Object<FileSource> jFileSource) { @@ -93,7 +99,9 @@ void FileSource::registerNative(jni::JNIEnv& env) { METHOD(&FileSource::getAccessToken, "getAccessToken"), METHOD(&FileSource::setAccessToken, "setAccessToken"), METHOD(&FileSource::setAPIBaseUrl, "setApiBaseUrl"), - METHOD(&FileSource::setResourceTransform, "setResourceTransform") + METHOD(&FileSource::setResourceTransform, "setResourceTransform"), + METHOD(&FileSource::resume, "resume"), + METHOD(&FileSource::pause, "pause") ); } diff --git a/platform/android/src/file_source.hpp b/platform/android/src/file_source.hpp index 4abe352bff..2933aedf86 100644 --- a/platform/android/src/file_source.hpp +++ b/platform/android/src/file_source.hpp @@ -41,6 +41,10 @@ public: void setResourceTransform(jni::JNIEnv&, jni::Object<FileSource::ResourceTransformCallback>); + void resume(jni::JNIEnv&); + + void pause(jni::JNIEnv&); + static jni::Class<FileSource> javaClass; static FileSource* getNativePeer(jni::JNIEnv&, jni::Object<FileSource>); diff --git a/platform/android/src/jni.cpp b/platform/android/src/jni.cpp index f39aeb6374..f4e5734861 100755 --- a/platform/android/src/jni.cpp +++ b/platform/android/src/jni.cpp @@ -172,6 +172,7 @@ void registerNatives(JavaVM *vm) { // Map CameraPosition::registerNative(env); + Image::registerNative(env); // Connectivity ConnectivityListener::registerNative(env); diff --git a/platform/android/src/map/image.cpp b/platform/android/src/map/image.cpp new file mode 100644 index 0000000000..5f5c90eddd --- /dev/null +++ b/platform/android/src/map/image.cpp @@ -0,0 +1,44 @@ +#include <mbgl/style/image.hpp> +#include <mbgl/util/exception.hpp> +#include "image.hpp" + +namespace mbgl { +namespace android { + +mbgl::style::Image Image::getImage(jni::JNIEnv& env, jni::Object<Image> image) { + static auto widthField = Image::javaClass.GetField<jni::jint>(env, "width"); + static auto heightField = Image::javaClass.GetField<jni::jint>(env, "height"); + static auto pixelRatioField = Image::javaClass.GetField<jni::jfloat>(env, "pixelRatio"); + static auto bufferField = Image::javaClass.GetField<jni::Array<jbyte>>(env, "buffer"); + static auto nameField = Image::javaClass.GetField<jni::String>(env, "name"); + + 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<std::string>(env, image.Get(env, nameField)); + + jni::NullCheck(env, &pixels); + std::size_t size = pixels.Length(env); + + mbgl::PremultipliedImage premultipliedImage({ static_cast<uint32_t>(width), static_cast<uint32_t>(height) }); + if (premultipliedImage.bytes() != uint32_t(size)) { + throw mbgl::util::SpriteImageException("Sprite image pixel count mismatch"); + } + + jni::GetArrayRegion(env, *pixels, 0, size, reinterpret_cast<jbyte*>(premultipliedImage.data.get())); + + return mbgl::style::Image {name, std::move(premultipliedImage), pixelRatio}; +} + +void Image::registerNative(jni::JNIEnv &env) { + // Lookup the class + Image::javaClass = *jni::Class<Image>::Find(env).NewGlobalRef(env).release(); +} + +jni::Class<Image> Image::javaClass; + + +} // namespace android +} // namespace mb + diff --git a/platform/android/src/map/image.hpp b/platform/android/src/map/image.hpp new file mode 100644 index 0000000000..1513e13ee7 --- /dev/null +++ b/platform/android/src/map/image.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include <mbgl/util/noncopyable.hpp> + +#include <jni/jni.hpp> +#include <mbgl/style/image.hpp> + +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&, jni::Object<Image>); + + static jni::Class<Image> javaClass; + + static void registerNative(jni::JNIEnv&); + +}; + + +} // namespace android +} // namespace mbgl
\ No newline at end of file diff --git a/platform/android/src/native_map_view.cpp b/platform/android/src/native_map_view.cpp index 24a35a7068..04cbb21927 100755 --- a/platform/android/src/native_map_view.cpp +++ b/platform/android/src/native_map_view.cpp @@ -49,6 +49,7 @@ #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" @@ -633,6 +634,26 @@ jni::Array<jlong> NativeMapView::queryPointAnnotations(JNIEnv& env, jni::Object< return result; } +jni::Array<jlong> NativeMapView::queryShapeAnnotations(JNIEnv &env, jni::Object<RectF> 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<jlong> longIds(ids.begin(), ids.end()); + auto result = jni::Array<jni::jlong>::New(env, ids.size()); + result.SetRegion<std::vector<jni::jlong>>(env, 0, longIds); + + return result; +} + jni::Array<jni::Object<geojson::Feature>> NativeMapView::queryRenderedFeaturesForPoint(JNIEnv& env, jni::jfloat x, jni::jfloat y, jni::Array<jni::String> layerIds, jni::Array<jni::Object<>> jfilter) { @@ -904,6 +925,18 @@ void NativeMapView::addImage(JNIEnv& env, jni::String name, jni::jint w, jni::ji float(scale))); } +void NativeMapView::addImages(JNIEnv& env, jni::Array<jni::Object<mbgl::android::Image>> jimages) { + jni::NullCheck(env, &jimages); + std::size_t len = jimages.Length(env); + + for (std::size_t i = 0; i < len; i++) { + jni::Object<mbgl::android::Image> jimage = jimages.Get(env, i); + auto image = mbgl::android::Image::getImage(env, jimage); + map->getStyle().addImage(std::make_unique<mbgl::style::Image>(image)); + jni::DeleteLocalRef(env, jimage); + } +} + void NativeMapView::removeImage(JNIEnv& env, jni::String name) { map->getStyle().removeImage(jni::Make<std::string>(env, name)); } @@ -1000,6 +1033,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) { METHOD(&NativeMapView::getTransitionDelay, "nativeGetTransitionDelay"), METHOD(&NativeMapView::setTransitionDelay, "nativeSetTransitionDelay"), METHOD(&NativeMapView::queryPointAnnotations, "nativeQueryPointAnnotations"), + METHOD(&NativeMapView::queryShapeAnnotations, "nativeQueryShapeAnnotations"), METHOD(&NativeMapView::queryRenderedFeaturesForPoint, "nativeQueryRenderedFeaturesForPoint"), METHOD(&NativeMapView::queryRenderedFeaturesForBox, "nativeQueryRenderedFeaturesForBox"), METHOD(&NativeMapView::getLight, "nativeGetLight"), @@ -1017,6 +1051,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) { METHOD(&NativeMapView::removeSourceById, "nativeRemoveSourceById"), METHOD(&NativeMapView::removeSource, "nativeRemoveSource"), METHOD(&NativeMapView::addImage, "nativeAddImage"), + METHOD(&NativeMapView::addImages, "nativeAddImages"), METHOD(&NativeMapView::removeImage, "nativeRemoveImage"), METHOD(&NativeMapView::getImage, "nativeGetImage"), METHOD(&NativeMapView::setLatLngBounds, "nativeSetLatLngBounds"), diff --git a/platform/android/src/native_map_view.hpp b/platform/android/src/native_map_view.hpp index 4d226d0fa9..d7e3b17b99 100755 --- a/platform/android/src/native_map_view.hpp +++ b/platform/android/src/native_map_view.hpp @@ -22,6 +22,7 @@ #include "style/sources/sources.hpp" #include "geometry/lat_lng_bounds.hpp" #include "map/camera_position.hpp" +#include "map/image.hpp" #include "style/light.hpp" #include "bitmap.hpp" @@ -197,6 +198,8 @@ public: jni::Array<jlong> queryPointAnnotations(JNIEnv&, jni::Object<RectF>); + jni::Array<jlong> queryShapeAnnotations(JNIEnv&, jni::Object<RectF>); + jni::Array<jni::Object<geojson::Feature>> queryRenderedFeaturesForPoint(JNIEnv&, jni::jfloat, jni::jfloat, jni::Array<jni::String>, jni::Array<jni::Object<>> jfilter); @@ -235,6 +238,8 @@ public: void addImage(JNIEnv&, jni::String, jni::jint, jni::jint, jni::jfloat, jni::Array<jbyte>); + void addImages(JNIEnv&, jni::Array<jni::Object<mbgl::android::Image>>); + void removeImage(JNIEnv&, jni::String); jni::Object<Bitmap> getImage(JNIEnv&, jni::String); diff --git a/platform/darwin/resources/bg.lproj/Foundation.strings b/platform/darwin/resources/bg.lproj/Foundation.strings new file mode 100644 index 0000000000..f2a9c6eae0 --- /dev/null +++ b/platform/darwin/resources/bg.lproj/Foundation.strings @@ -0,0 +1,291 @@ +/* Clock position format, long: {hours} o’clock */ +"CLOCK_FMT_LONG" = "%@ часа"; + +/* Clock position format, medium: {hours} o’clock */ +"CLOCK_FMT_MEDIUM" = "%@ часа"; + +/* Clock position format, short: {hours}:00 */ +"CLOCK_FMT_SHORT" = "%@:00"; + +/* East, long */ +"COMPASS_E_LONG" = "изток"; + +/* East, short */ +"COMPASS_E_SHORT" = "И"; + +/* East by north, long */ +"COMPASS_EbN_LONG" = "ийст-тен-уест"; + +/* East by north, short */ +"COMPASS_EbN_SHORT" = "EbN"; + +/* East by south, long */ +"COMPASS_EbS_LONG" = "ийст-тен-саут"; + +/* East by south, short */ +"COMPASS_EbS_SHORT" = "EbS"; + +/* East-northeast, long */ +"COMPASS_ENE_LONG" = "север-североизток"; + +/* East-northeast, short */ +"COMPASS_ENE_SHORT" = "ССИ"; + +/* East-southeast, long */ +"COMPASS_ESE_LONG" = "изток-югоизток"; + +/* East-southeast, short */ +"COMPASS_ESE_SHORT" = "ИЮИ"; + +/* North, long */ +"COMPASS_N_LONG" = "север"; + +/* North, short */ +"COMPASS_N_SHORT" = "С"; + +/* North by east, long */ +"COMPASS_NbE_LONG" = "норд-тен-ийст"; + +/* North by east, short */ +"COMPASS_NbE_SHORT" = "NbE"; + +/* North by west, long */ +"COMPASS_NbW_LONG" = "норд-тен-уест"; + +/* North by west, short */ +"COMPASS_NbW_SHORT" = "NbW"; + +/* Northeast, long */ +"COMPASS_NE_LONG" = "североизток"; + +/* Northeast, short */ +"COMPASS_NE_SHORT" = "СИ"; + +/* Northeast by east, long */ +"COMPASS_NEbE_LONG" = "нордийст-тен-ийст"; + +/* Northeast by east, short */ +"COMPASS_NEbE_SHORT" = "NEbE"; + +/* Northeast by north, long */ +"COMPASS_NEbN_LONG" = "нордийст-тен-норд"; + +/* Northeast by north, short */ +"COMPASS_NEbN_SHORT" = "NEnN"; + +/* North-northeast, long */ +"COMPASS_NNE_LONG" = "север-североизток"; + +/* North-northeast, short */ +"COMPASS_NNE_SHORT" = "ССИ"; + +/* North-northwest, long */ +"COMPASS_NNW_LONG" = "север-северозапад"; + +/* North-northwest, short */ +"COMPASS_NNW_SHORT" = "ССЗ"; + +/* Northwest, long */ +"COMPASS_NW_LONG" = "северозапад"; + +/* Northwest, short */ +"COMPASS_NW_SHORT" = "СЗ"; + +/* Northwest by north, long */ +"COMPASS_NWbN_LONG" = "нордуест-тен-норд"; + +/* Northwest by north, short */ +"COMPASS_NWbN_SHORT" = "NWbN"; + +/* Northwest by west, long */ +"COMPASS_NWbW_LONG" = "нордуест-тен-уест"; + +/* Northwest by west, short */ +"COMPASS_NWbW_SHORT" = "NWbW"; + +/* South, long */ +"COMPASS_S_LONG" = "юг"; + +/* South, short */ +"COMPASS_S_SHORT" = "Ю"; + +/* South by east, long */ +"COMPASS_SbE_LONG" = "саут-тен-ийст"; + +/* South by east, short */ +"COMPASS_SbE_SHORT" = "SbE"; + +/* South by west, long */ +"COMPASS_SbW_LONG" = "саут-тен-уест"; + +/* South by west, short */ +"COMPASS_SbW_SHORT" = "SbW"; + +/* Southeast, long */ +"COMPASS_SE_LONG" = "югоизток"; + +/* Southeast, short */ +"COMPASS_SE_SHORT" = "ЮИ"; + +/* Southeast by east, long */ +"COMPASS_SEbE_LONG" = "саутийст-тен-ийст"; + +/* Southeast by east, short */ +"COMPASS_SEbE_SHORT" = "SEbE"; + +/* Southeast by south, long */ +"COMPASS_SEbS_LONG" = "саутийст-тен-саут"; + +/* Southeast by south, short */ +"COMPASS_SEbS_SHORT" = "SEbS"; + +/* South-southeast, long */ +"COMPASS_SSE_LONG" = "юг-югоизток"; + +/* South-southeast, short */ +"COMPASS_SSE_SHORT" = "ЮЮИ"; + +/* South-southwest, long */ +"COMPASS_SSW_LONG" = "юг-югозапад"; + +/* South-southwest, short */ +"COMPASS_SSW_SHORT" = "ЮЮЗ"; + +/* Southwest, long */ +"COMPASS_SW_LONG" = "югозапад"; + +/* Southwest, short */ +"COMPASS_SW_SHORT" = "ЮЗ"; + +/* Southwest by south, long */ +"COMPASS_SWbS_LONG" = "саутуест-тен-саут"; + +/* Southwest by south, short */ +"COMPASS_SWbS_SHORT" = "SWbS"; + +/* Southwest by west, long */ +"COMPASS_SWbW_LONG" = "саутуест-тен-уест"; + +/* Southwest by west, short */ +"COMPASS_SWbW_SHORT" = "SWbW"; + +/* West, long */ +"COMPASS_W_LONG" = "запад"; + +/* West, short */ +"COMPASS_W_SHORT" = "З"; + +/* West by north, long */ +"COMPASS_WbN_LONG" = "уест-тен-норд"; + +/* West by north, short */ +"COMPASS_WbN_SHORT" = "WbN"; + +/* West by south, long */ +"COMPASS_WbS_LONG" = "уест-тен-саут"; + +/* West by south, short */ +"COMPASS_WbS_SHORT" = "WbS"; + +/* West-northwest, long */ +"COMPASS_WNW_LONG" = "запад-северозапад"; + +/* West-northwest, short */ +"COMPASS_WNW_SHORT" = "ЗСЗ"; + +/* West-southwest, long */ +"COMPASS_WSW_LONG" = "запад-югозапад"; + +/* West-southwest, short */ +"COMPASS_WSW_SHORT" = "ЗСЗ"; + +/* Degrees format, long */ +"COORD_DEG_LONG" = "%d градус(а)"; + +/* Degrees format, medium: {degrees} */ +"COORD_DEG_MEDIUM" = "%d°"; + +/* Degrees format, short: {degrees} */ +"COORD_DEG_SHORT" = "%d°"; + +/* Coordinate format, long: {degrees}{minutes} */ +"COORD_DM_LONG" = "%1$@ и %2$@"; + +/* Coordinate format, medium: {degrees}{minutes} */ +"COORD_DM_MEDIUM" = "%1$@%2$@"; + +/* Coordinate format, short: {degrees}{minutes} */ +"COORD_DM_SHORT" = "%1$@%2$@"; + +/* Coordinate format, long: {degrees}{minutes}{seconds} */ +"COORD_DMS_LONG" = "%1$@, %2$@ и %3$@"; + +/* Coordinate format, medium: {degrees}{minutes}{seconds} */ +"COORD_DMS_MEDIUM" = "%1$@%2$@%3$@"; + +/* Coordinate format, short: {degrees}{minutes}{seconds} */ +"COORD_DMS_SHORT" = "%1$@%2$@%3$@"; + +/* East longitude format, long: {longitude} */ +"COORD_E_LONG" = "%@ изток"; + +/* East longitude format, medium: {longitude} */ +"COORD_E_MEDIUM" = "%@ изток"; + +/* East longitude format, short: {longitude} */ +"COORD_E_SHORT" = "%@И"; + +/* Coordinate pair format, long: {latitude}, {longitude} */ +"COORD_FMT_LONG" = "%1$@ на %2$@"; + +/* Coordinate pair format, medium: {latitude}, {longitude} */ +"COORD_FMT_MEDIUM" = "%1$@, %2$@"; + +/* Coordinate pair format, short: {latitude}, {longitude} */ +"COORD_FMT_SHORT" = "%1$@, %2$@"; + +/* Minutes format, long */ +"COORD_MIN_LONG" = "%d минута(и)"; + +/* Minutes format, medium: {minutes} */ +"COORD_MIN_MEDIUM" = "%d′"; + +/* Minutes format, short: {minutes} */ +"COORD_MIN_SHORT" = "%d′"; + +/* North latitude format, long: {latitude} */ +"COORD_N_LONG" = "%@ север"; + +/* North latitude format, medium: {latitude} */ +"COORD_N_MEDIUM" = "%@ север"; + +/* North latitude format, short: {latitude} */ +"COORD_N_SHORT" = "%@С"; + +/* South latitude format, long: {latitude} */ +"COORD_S_LONG" = "%@ юг"; + +/* South latitude format, medium: {latitude} */ +"COORD_S_MEDIUM" = "%@ юг"; + +/* South latitude format, short: {latitude} */ +"COORD_S_SHORT" = "%@Ю"; + +/* Seconds format, long */ +"COORD_SEC_LONG" = "%d секунда(и)"; + +/* Seconds format, medium: {seconds} */ +"COORD_SEC_MEDIUM" = "%d″"; + +/* Seconds format, short: {seconds} */ +"COORD_SEC_SHORT" = "%d″"; + +/* West longitude format, long: {longitude} */ +"COORD_W_LONG" = "%@ запад"; + +/* West longitude format, medium: {longitude} */ +"COORD_W_MEDIUM" = "%@ запад"; + +/* West longitude format, short: {longitude} */ +"COORD_W_SHORT" = "%@З"; + diff --git a/platform/darwin/resources/bg.lproj/Foundation.stringsdict b/platform/darwin/resources/bg.lproj/Foundation.stringsdict new file mode 100644 index 0000000000..fcaf0a48c1 --- /dev/null +++ b/platform/darwin/resources/bg.lproj/Foundation.stringsdict @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>COORD_DEG_LONG</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@degrees@</string> + <key>degrees</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>%d градус</string> + <key>other</key> + <string>%d градуса</string> + </dict> + </dict> + <key>COORD_MIN_LONG</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@minutes@</string> + <key>minutes</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>%d минута</string> + <key>other</key> + <string>%d минути</string> + </dict> + </dict> + <key>COORD_SEC_LONG</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@seconds@</string> + <key>seconds</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>%d секунда</string> + <key>other</key> + <string>%d секунди</string> + </dict> + </dict> +</dict> +</plist> diff --git a/platform/darwin/resources/hu.lproj/Foundation.stringsdict b/platform/darwin/resources/hu.lproj/Foundation.stringsdict new file mode 100644 index 0000000000..a594100046 --- /dev/null +++ b/platform/darwin/resources/hu.lproj/Foundation.stringsdict @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>COORD_DEG_LONG</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@degrees@</string> + <key>degrees</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>%d fok</string> + <key>other</key> + <string>%d fok</string> + </dict> + </dict> + <key>COORD_MIN_LONG</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@minutes@</string> + <key>minutes</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>%d perc</string> + <key>other</key> + <string>%d perc</string> + </dict> + </dict> + <key>COORD_SEC_LONG</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@seconds@</string> + <key>seconds</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>%d másodperc</string> + <key>other</key> + <string>%d másodperc</string> + </dict> + </dict> +</dict> +</plist> diff --git a/platform/darwin/resources/pt-BR.lproj/Foundation.strings b/platform/darwin/resources/pt-BR.lproj/Foundation.strings new file mode 100644 index 0000000000..71a7f7e4a5 --- /dev/null +++ b/platform/darwin/resources/pt-BR.lproj/Foundation.strings @@ -0,0 +1,291 @@ +/* Clock position format, long: {hours} o’clock */ +"CLOCK_FMT_LONG" = "São %@ horas"; + +/* Clock position format, medium: {hours} o’clock */ +"CLOCK_FMT_MEDIUM" = "São %@ horas"; + +/* Clock position format, short: {hours}:00 */ +"CLOCK_FMT_SHORT" = "%@:00"; + +/* East, long */ +"COMPASS_E_LONG" = "leste"; + +/* East, short */ +"COMPASS_E_SHORT" = "L"; + +/* East by north, long */ +"COMPASS_EbN_LONG" = "lest pelo norte"; + +/* East by north, short */ +"COMPASS_EbN_SHORT" = "LpN"; + +/* East by south, long */ +"COMPASS_EbS_LONG" = "leste pelo sul"; + +/* East by south, short */ +"COMPASS_EbS_SHORT" = "LpS"; + +/* East-northeast, long */ +"COMPASS_ENE_LONG" = "lés-nordeste"; + +/* East-northeast, short */ +"COMPASS_ENE_SHORT" = "ENE"; + +/* East-southeast, long */ +"COMPASS_ESE_LONG" = "lés-sudeste"; + +/* East-southeast, short */ +"COMPASS_ESE_SHORT" = "ESE"; + +/* North, long */ +"COMPASS_N_LONG" = "norte"; + +/* North, short */ +"COMPASS_N_SHORT" = "N"; + +/* North by east, long */ +"COMPASS_NbE_LONG" = "norte pelo leste"; + +/* North by east, short */ +"COMPASS_NbE_SHORT" = "NpL"; + +/* North by west, long */ +"COMPASS_NbW_LONG" = "norte pelo oeste"; + +/* North by west, short */ +"COMPASS_NbW_SHORT" = "NpO"; + +/* Northeast, long */ +"COMPASS_NE_LONG" = "nordeste"; + +/* Northeast, short */ +"COMPASS_NE_SHORT" = "NE"; + +/* Northeast by east, long */ +"COMPASS_NEbE_LONG" = "nordeste pelo leste"; + +/* Northeast by east, short */ +"COMPASS_NEbE_SHORT" = "NEpL"; + +/* Northeast by north, long */ +"COMPASS_NEbN_LONG" = "nordeste pelo norte"; + +/* Northeast by north, short */ +"COMPASS_NEbN_SHORT" = "NEpN"; + +/* North-northeast, long */ +"COMPASS_NNE_LONG" = "nor-nordeste"; + +/* North-northeast, short */ +"COMPASS_NNE_SHORT" = "NNE"; + +/* North-northwest, long */ +"COMPASS_NNW_LONG" = "nor-noroeste"; + +/* North-northwest, short */ +"COMPASS_NNW_SHORT" = "NNO"; + +/* Northwest, long */ +"COMPASS_NW_LONG" = "noroeste"; + +/* Northwest, short */ +"COMPASS_NW_SHORT" = "NO"; + +/* Northwest by north, long */ +"COMPASS_NWbN_LONG" = "noroeste pelo norte"; + +/* Northwest by north, short */ +"COMPASS_NWbN_SHORT" = "NOpN"; + +/* Northwest by west, long */ +"COMPASS_NWbW_LONG" = "noroeste pelo oeste"; + +/* Northwest by west, short */ +"COMPASS_NWbW_SHORT" = "NOpO"; + +/* South, long */ +"COMPASS_S_LONG" = "sul"; + +/* South, short */ +"COMPASS_S_SHORT" = "S"; + +/* South by east, long */ +"COMPASS_SbE_LONG" = "sul pelo leste"; + +/* South by east, short */ +"COMPASS_SbE_SHORT" = "SpL"; + +/* South by west, long */ +"COMPASS_SbW_LONG" = "sul pelo oeste"; + +/* South by west, short */ +"COMPASS_SbW_SHORT" = "SbO"; + +/* Southeast, long */ +"COMPASS_SE_LONG" = "sudeste"; + +/* Southeast, short */ +"COMPASS_SE_SHORT" = "SE"; + +/* Southeast by east, long */ +"COMPASS_SEbE_LONG" = "sudeste pelo leste"; + +/* Southeast by east, short */ +"COMPASS_SEbE_SHORT" = "SEpL"; + +/* Southeast by south, long */ +"COMPASS_SEbS_LONG" = "sudeste pelo sul"; + +/* Southeast by south, short */ +"COMPASS_SEbS_SHORT" = "SEpS"; + +/* South-southeast, long */ +"COMPASS_SSE_LONG" = "sul-sudeste"; + +/* South-southeast, short */ +"COMPASS_SSE_SHORT" = "SSE"; + +/* South-southwest, long */ +"COMPASS_SSW_LONG" = "su-sudoeste"; + +/* South-southwest, short */ +"COMPASS_SSW_SHORT" = "SSO"; + +/* Southwest, long */ +"COMPASS_SW_LONG" = "sudoeste"; + +/* Southwest, short */ +"COMPASS_SW_SHORT" = "SO"; + +/* Southwest by south, long */ +"COMPASS_SWbS_LONG" = "sudoeste pelo sul"; + +/* Southwest by south, short */ +"COMPASS_SWbS_SHORT" = "SOpS"; + +/* Southwest by west, long */ +"COMPASS_SWbW_LONG" = "sudoeste pelo oeste"; + +/* Southwest by west, short */ +"COMPASS_SWbW_SHORT" = "SOpO"; + +/* West, long */ +"COMPASS_W_LONG" = "oeste"; + +/* West, short */ +"COMPASS_W_SHORT" = "O"; + +/* West by north, long */ +"COMPASS_WbN_LONG" = "oeste pelo norte"; + +/* West by north, short */ +"COMPASS_WbN_SHORT" = "OpN"; + +/* West by south, long */ +"COMPASS_WbS_LONG" = "oeste pelo sul"; + +/* West by south, short */ +"COMPASS_WbS_SHORT" = "OpS"; + +/* West-northwest, long */ +"COMPASS_WNW_LONG" = "oés-noroeste"; + +/* West-northwest, short */ +"COMPASS_WNW_SHORT" = "ONO"; + +/* West-southwest, long */ +"COMPASS_WSW_LONG" = "oés-sudoeste"; + +/* West-southwest, short */ +"COMPASS_WSW_SHORT" = "OSO"; + +/* Degrees format, long */ +"COORD_DEG_LONG" = "%d grau(s)"; + +/* Degrees format, medium: {degrees} */ +"COORD_DEG_MEDIUM" = "%d°"; + +/* Degrees format, short: {degrees} */ +"COORD_DEG_SHORT" = "%d°"; + +/* Coordinate format, long: {degrees}{minutes} */ +"COORD_DM_LONG" = "%1$@ e %2$@"; + +/* Coordinate format, medium: {degrees}{minutes} */ +"COORD_DM_MEDIUM" = "%1$@%2$@"; + +/* Coordinate format, short: {degrees}{minutes} */ +"COORD_DM_SHORT" = "%1$@%2$@"; + +/* Coordinate format, long: {degrees}{minutes}{seconds} */ +"COORD_DMS_LONG" = "%1$@, %2$@, e %3$@"; + +/* Coordinate format, medium: {degrees}{minutes}{seconds} */ +"COORD_DMS_MEDIUM" = "%1$@%2$@%3$@"; + +/* Coordinate format, short: {degrees}{minutes}{seconds} */ +"COORD_DMS_SHORT" = "%1$@%2$@%3$@"; + +/* East longitude format, long: {longitude} */ +"COORD_E_LONG" = "%@ leste"; + +/* East longitude format, medium: {longitude} */ +"COORD_E_MEDIUM" = "%@ leste"; + +/* East longitude format, short: {longitude} */ +"COORD_E_SHORT" = "%@L"; + +/* Coordinate pair format, long: {latitude}, {longitude} */ +"COORD_FMT_LONG" = "%1$@ por %2$@"; + +/* Coordinate pair format, medium: {latitude}, {longitude} */ +"COORD_FMT_MEDIUM" = "%1$@, %2$@"; + +/* Coordinate pair format, short: {latitude}, {longitude} */ +"COORD_FMT_SHORT" = "%1$@, %2$@"; + +/* Minutes format, long */ +"COORD_MIN_LONG" = "%d minuto(s)"; + +/* Minutes format, medium: {minutes} */ +"COORD_MIN_MEDIUM" = "%d′"; + +/* Minutes format, short: {minutes} */ +"COORD_MIN_SHORT" = "%d′"; + +/* North latitude format, long: {latitude} */ +"COORD_N_LONG" = "%@ norte"; + +/* North latitude format, medium: {latitude} */ +"COORD_N_MEDIUM" = "%@ norte"; + +/* North latitude format, short: {latitude} */ +"COORD_N_SHORT" = "%@N"; + +/* South latitude format, long: {latitude} */ +"COORD_S_LONG" = "%@ sul"; + +/* South latitude format, medium: {latitude} */ +"COORD_S_MEDIUM" = "%@ sul"; + +/* South latitude format, short: {latitude} */ +"COORD_S_SHORT" = "%@S"; + +/* Seconds format, long */ +"COORD_SEC_LONG" = "%d segundo(s)"; + +/* Seconds format, medium: {seconds} */ +"COORD_SEC_MEDIUM" = "%d″"; + +/* Seconds format, short: {seconds} */ +"COORD_SEC_SHORT" = "%d″"; + +/* West longitude format, long: {longitude} */ +"COORD_W_LONG" = "%@ oeste"; + +/* West longitude format, medium: {longitude} */ +"COORD_W_MEDIUM" = "%@ oeste"; + +/* West longitude format, short: {longitude} */ +"COORD_W_SHORT" = "%@O"; + diff --git a/platform/darwin/src/MGLGeometry.mm b/platform/darwin/src/MGLGeometry.mm index 1540a3a741..715a70f0b8 100644 --- a/platform/darwin/src/MGLGeometry.mm +++ b/platform/darwin/src/MGLGeometry.mm @@ -4,6 +4,10 @@ #import <mbgl/util/projection.hpp> +#if !TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR +#import <Cocoa/Cocoa.h> +#endif + /** Vertical field of view, measured in degrees, for determining the altitude of the viewpoint. @@ -57,3 +61,48 @@ double MGLZoomLevelForAltitude(CLLocationDistance altitude, CGFloat pitch, CLLoc CGFloat mapPixelWidthAtZoom = std::cos(MGLRadiansFromDegrees(latitude)) * mbgl::util::M2PI * mbgl::util::EARTH_RADIUS_M / metersPerPixel; return ::log2(mapPixelWidthAtZoom / mbgl::util::tileSize); } + +MGLRadianDistance MGLDistanceBetweenRadianCoordinates(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to) { + double a = pow(sin((to.latitude - from.latitude) / 2), 2) + + pow(sin((to.longitude - from.longitude) / 2), 2) * cos(from.latitude) * cos(to.latitude); + + return 2 * atan2(sqrt(a), sqrt(1 - a)); +} + +MGLRadianDirection MGLRadianCoordinatesDirection(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to) { + double a = sin(to.longitude - from.longitude) * cos(to.latitude); + double b = cos(from.latitude) * sin(to.latitude) + - sin(from.latitude) * cos(to.latitude) * cos(to.longitude - from.longitude); + return atan2(a, b); +} + +MGLRadianCoordinate2D MGLRadianCoordinateAtDistanceFacingDirection(MGLRadianCoordinate2D coordinate, + MGLRadianDistance distance, + MGLRadianDirection direction) { + double otherLatitude = asin(sin(coordinate.latitude) * cos(distance) + + cos(coordinate.latitude) * sin(distance) * cos(direction)); + double otherLongitude = coordinate.longitude + atan2(sin(direction) * sin(distance) * cos(coordinate.latitude), + cos(distance) - sin(coordinate.latitude) * sin(otherLatitude)); + return MGLRadianCoordinate2DMake(otherLatitude, otherLongitude); +} + +CLLocationDirection MGLDirectionBetweenCoordinates(CLLocationCoordinate2D firstCoordinate, CLLocationCoordinate2D secondCoordinate) { + // Ported from https://github.com/mapbox/turf-swift/blob/857e2e8060678ef4a7a9169d4971b0788fdffc37/Turf/Turf.swift#L23-L31 + MGLRadianCoordinate2D firstRadianCoordinate = MGLRadianCoordinateFromLocationCoordinate(firstCoordinate); + MGLRadianCoordinate2D secondRadianCoordinate = MGLRadianCoordinateFromLocationCoordinate(secondCoordinate); + + CGFloat a = sin(secondRadianCoordinate.longitude - firstRadianCoordinate.longitude) * cos(secondRadianCoordinate.latitude); + CGFloat b = (cos(firstRadianCoordinate.latitude) * sin(secondRadianCoordinate.latitude) + - sin(firstRadianCoordinate.latitude) * cos(secondRadianCoordinate.latitude) * cos(secondRadianCoordinate.longitude - firstRadianCoordinate.longitude)); + MGLRadianDirection radianDirection = atan2(a, b); + return radianDirection * 180 / M_PI; +} + +CGPoint MGLPointRounded(CGPoint point) { +#if TARGET_OS_IPHONE || TARGET_OS_SIMULATOR + CGFloat scaleFactor = [UIScreen instancesRespondToSelector:@selector(nativeScale)] ? [UIScreen mainScreen].nativeScale : [UIScreen mainScreen].scale; +#elif TARGET_OS_MAC + CGFloat scaleFactor = [NSScreen mainScreen].backingScaleFactor; +#endif + return CGPointMake(round(point.x * scaleFactor) / scaleFactor, round(point.y * scaleFactor) / scaleFactor); +} diff --git a/platform/darwin/src/MGLGeometry_Private.h b/platform/darwin/src/MGLGeometry_Private.h index 88fcf5b576..8b9c6c2327 100644 --- a/platform/darwin/src/MGLGeometry_Private.h +++ b/platform/darwin/src/MGLGeometry_Private.h @@ -105,36 +105,26 @@ NS_INLINE MGLRadianCoordinate2D MGLRadianCoordinateFromLocationCoordinate(CLLoca MGLRadiansFromDegrees(locationCoordinate.longitude)); } -/* +/** Returns the distance in radians given two coordinates. */ -NS_INLINE MGLRadianDistance MGLDistanceBetweenRadianCoordinates(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to) -{ - double a = pow(sin((to.latitude - from.latitude) / 2), 2) - + pow(sin((to.longitude - from.longitude) / 2), 2) * cos(from.latitude) * cos(to.latitude); - - return 2 * atan2(sqrt(a), sqrt(1 - a)); -} +MGLRadianDistance MGLDistanceBetweenRadianCoordinates(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to); -/* +/** Returns direction in radians given two coordinates. */ -NS_INLINE MGLRadianDirection MGLRadianCoordinatesDirection(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to) { - double a = sin(to.longitude - from.longitude) * cos(to.latitude); - double b = cos(from.latitude) * sin(to.latitude) - - sin(from.latitude) * cos(to.latitude) * cos(to.longitude - from.longitude); - return atan2(a, b); -} +MGLRadianDirection MGLRadianCoordinatesDirection(MGLRadianCoordinate2D from, MGLRadianCoordinate2D to); + +/** + Returns a coordinate at a given distance and direction away from coordinate. + */ +MGLRadianCoordinate2D MGLRadianCoordinateAtDistanceFacingDirection(MGLRadianCoordinate2D coordinate, + MGLRadianDistance distance, + MGLRadianDirection direction); -/* - Returns coordinate at a given distance and direction away from coordinate. +/** + Returns the direction from one coordinate to another. */ -NS_INLINE MGLRadianCoordinate2D MGLRadianCoordinateAtDistanceFacingDirection(MGLRadianCoordinate2D coordinate, - MGLRadianDistance distance, - MGLRadianDirection direction) { - double otherLatitude = asin(sin(coordinate.latitude) * cos(distance) - + cos(coordinate.latitude) * sin(distance) * cos(direction)); - double otherLongitude = coordinate.longitude + atan2(sin(direction) * sin(distance) * cos(coordinate.latitude), - cos(distance) - sin(coordinate.latitude) * sin(otherLatitude)); - return MGLRadianCoordinate2DMake(otherLatitude, otherLongitude); -} +CLLocationDirection MGLDirectionBetweenCoordinates(CLLocationCoordinate2D firstCoordinate, CLLocationCoordinate2D secondCoordinate); + +CGPoint MGLPointRounded(CGPoint point); diff --git a/platform/darwin/src/MGLImageSource.h b/platform/darwin/src/MGLImageSource.h index 21487d9739..5088f6bac0 100644 --- a/platform/darwin/src/MGLImageSource.h +++ b/platform/darwin/src/MGLImageSource.h @@ -38,8 +38,11 @@ MGL_EXPORT bottomLeft: CLLocationCoordinate2D(latitude: 37.936, longitude: -80.425), bottomRight: CLLocationCoordinate2D(latitude: 37.936, longitude: -71.516), topRight: CLLocationCoordinate2D(latitude: 46.437, longitude: -71.516)) - let source = MGLImageSource(identifier: "radar", coordinateQuad: coordinates, url: URL(string: "https://www.mapbox.com/mapbox-gl-js/assets/radar.gif")!) + let source = MGLImageSource(identifier: "radar-source", coordinateQuad: coordinates, url: URL(string: "https://www.mapbox.com/mapbox-gl-js/assets/radar.gif")!) mapView.style?.addSource(source) + + let layer = MGLRasterStyleLayer(identifier: "radar-layer", source: source) + style.addLayer(layer) ``` */ MGL_EXPORT diff --git a/platform/darwin/src/MGLMapSnapshotter.h b/platform/darwin/src/MGLMapSnapshotter.h index 9cf032f5d1..541bc68b93 100644 --- a/platform/darwin/src/MGLMapSnapshotter.h +++ b/platform/darwin/src/MGLMapSnapshotter.h @@ -5,10 +5,10 @@ NS_ASSUME_NONNULL_BEGIN -MGL_EXPORT /** - The options to use when creating images with the `MGLMapsnapshotter`. + The options to use when creating images with the `MGLMapSnapshotter`. */ +MGL_EXPORT @interface MGLMapSnapshotOptions : NSObject /** @@ -63,7 +63,8 @@ MGL_EXPORT /** The scale of the output image. Defaults to the main screen scale. - Minimum is 1. + + The minimum scale is 1. */ @property (nonatomic) CGFloat scale; @@ -196,7 +197,7 @@ MGL_EXPORT @property (nonatomic) CGSize size; /** - Indicates whether as snapshot is currently being generated. + Indicates whether a snapshot is currently being generated. */ @property (nonatomic, readonly, getter=isLoading) BOOL loading; diff --git a/platform/darwin/src/MGLMapSnapshotter.mm b/platform/darwin/src/MGLMapSnapshotter.mm index a00521a87a..dea93eba34 100644 --- a/platform/darwin/src/MGLMapSnapshotter.mm +++ b/platform/darwin/src/MGLMapSnapshotter.mm @@ -14,11 +14,14 @@ #import "MGLGeometry_Private.h" #import "NSBundle+MGLAdditions.h" #import "MGLStyle.h" +#import "MGLAttributionInfo_Private.h" #if TARGET_OS_IPHONE #import "UIImage+MGLAdditions.h" #else #import "NSImage+MGLAdditions.h" +#import <CoreGraphics/CoreGraphics.h> +#import <QuartzCore/QuartzCore.h> #endif const CGPoint MGLLogoImagePosition = CGPointMake(8, 8); @@ -146,6 +149,33 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; dispatch_async(queue, ^{ _snapshotCallback = std::make_unique<mbgl::Actor<mbgl::MapSnapshotter::Callback>>(*mbgl::Scheduler::GetCurrent(), [=](std::exception_ptr mbglError, mbgl::PremultipliedImage image, mbgl::MapSnapshotter::Attributions attributions, mbgl::MapSnapshotter::PointForFn pointForFn) { _loading = false; + + NSMutableArray *infos = [NSMutableArray array]; + +#if TARGET_OS_IPHONE + CGFloat fontSize = [UIFont smallSystemFontSize]; + UIColor *attributeFontColor = [UIColor blackColor]; +#else + CGFloat fontSize = [NSFont systemFontSizeForControlSize:NSMiniControlSize]; + NSColor *attributeFontColor = [NSColor blackColor]; +#endif + for (auto attribution = attributions.begin(); attribution != attributions.end(); ++attribution) { + NSString *attributionHTMLString = @(attribution->c_str()); + NSArray *tileSetInfos = [MGLAttributionInfo attributionInfosFromHTMLString:attributionHTMLString + fontSize:fontSize + linkColor:attributeFontColor]; + [infos growArrayByAddingAttributionInfosFromArray:tileSetInfos]; + } + + CGSize attributionBackgroundSize = CGSizeMake(10, 0); + for (MGLAttributionInfo *info in infos) { + if (info.isFeedbackLink) { + continue; + } + attributionBackgroundSize.width += [info.title size].width + 10; + attributionBackgroundSize.height = MAX([info.title size].height, attributionBackgroundSize.height); + } + if (mbglError) { NSString *description = @(mbgl::util::toString(mbglError).c_str()); NSDictionary *userInfo = @{NSLocalizedDescriptionKey: description}; @@ -170,10 +200,39 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; #if TARGET_OS_IPHONE UIImage *logoImage = [UIImage imageNamed:@"mapbox" inBundle:[NSBundle mgl_frameworkBundle] compatibleWithTraitCollection:nil]; + CGRect logoImageRect = CGRectMake(MGLLogoImagePosition.x, mglImage.size.height - (MGLLogoImagePosition.y + logoImage.size.height), logoImage.size.width, logoImage.size.height); + CGRect attributionBackgroundFrame = CGRectMake(mglImage.size.width - 10 - attributionBackgroundSize.width, + logoImageRect.origin.y + (logoImageRect.size.height / 2) - (attributionBackgroundSize.height / 2) + 1, + attributionBackgroundSize.width, + attributionBackgroundSize.height); + CGPoint attributionTextPosition = CGPointMake(attributionBackgroundFrame.origin.x + 10, + attributionBackgroundFrame.origin.y - 1); + + CGRect cropRect = CGRectMake(attributionBackgroundFrame.origin.x * mglImage.scale, + attributionBackgroundFrame.origin.y * mglImage.scale, + attributionBackgroundSize.width * mglImage.scale, + attributionBackgroundSize.height * mglImage.scale); + + UIGraphicsBeginImageContextWithOptions(mglImage.size, NO, self.options.scale); [mglImage drawInRect:CGRectMake(0, 0, mglImage.size.width, mglImage.size.height)]; - [logoImage drawInRect:CGRectMake(MGLLogoImagePosition.x, mglImage.size.height - (MGLLogoImagePosition.y + logoImage.size.height), logoImage.size.width,logoImage.size.height)]; + + [logoImage drawInRect:logoImageRect]; + + UIImage *currentImage = UIGraphicsGetImageFromCurrentImageContext(); + CGImageRef attributionImageRef = CGImageCreateWithImageInRect([currentImage CGImage], cropRect); + UIImage *attributionImage = [UIImage imageWithCGImage:attributionImageRef]; + CGImageRelease(attributionImageRef); + + CIImage *ciAttributionImage = [[CIImage alloc] initWithCGImage:attributionImage.CGImage]; + + UIImage *blurredAttributionBackground = [self blurredAttributionBackground:ciAttributionImage]; + + [blurredAttributionBackground drawInRect:attributionBackgroundFrame]; + + [self drawAttributionText:infos origin:attributionTextPosition]; + UIImage *compositedImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); @@ -183,6 +242,15 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; NSSize targetSize = NSMakeSize(self.options.size.width, self.options.size.height); NSRect targetFrame = NSMakeRect(0, 0, targetSize.width, targetSize.height); + CGRect logoImageRect = CGRectMake(MGLLogoImagePosition.x, MGLLogoImagePosition.y, logoImage.size.width, logoImage.size.height); + CGRect attributionBackgroundFrame = CGRectMake(targetFrame.size.width - 10 - attributionBackgroundSize.width, + MGLLogoImagePosition.y + 1, + attributionBackgroundSize.width, + attributionBackgroundSize.height); + CGPoint attributionTextPosition = CGPointMake(attributionBackgroundFrame.origin.x + 10, + logoImageRect.origin.y + (logoImageRect.size.height / 2) - (attributionBackgroundSize.height / 2)); + + NSImage *compositedImage = nil; NSImageRep *sourceImageRep = [sourceImage bestRepresentationForRect:targetFrame context:nil @@ -190,10 +258,24 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; compositedImage = [[NSImage alloc] initWithSize:targetSize]; [compositedImage lockFocus]; + [sourceImageRep drawInRect: targetFrame]; - [logoImage drawInRect:CGRectMake(MGLLogoImagePosition.x, MGLLogoImagePosition.y, logoImage.size.width,logoImage.size.height)]; + + [logoImage drawInRect:logoImageRect]; + + NSBitmapImageRep *attributionBackground = [[NSBitmapImageRep alloc] initWithFocusedViewRect:attributionBackgroundFrame]; + + CIImage *attributionBackgroundImage = [[CIImage alloc] initWithCGImage:[attributionBackground CGImage]]; + + NSImage *blurredAttributionBackground = [self blurredAttributionBackground:attributionBackgroundImage]; + + [blurredAttributionBackground drawInRect:attributionBackgroundFrame]; + + [self drawAttributionText:infos origin:attributionTextPosition]; + [compositedImage unlockFocus]; + #endif // Dispatch result to origin queue @@ -208,6 +290,47 @@ const CGFloat MGLSnapshotterMinimumPixelSize = 64; }); } +- (void)drawAttributionText:(NSArray *)attributionInfo origin:(CGPoint)origin +{ + for (MGLAttributionInfo *info in attributionInfo) { + if (info.isFeedbackLink) { + continue; + } + [info.title drawAtPoint:origin]; + + origin.x += [info.title size].width + 10; + } +} + +- (MGLImage *)blurredAttributionBackground:(CIImage *)backgroundImage +{ + CGAffineTransform transform = CGAffineTransformIdentity; + CIFilter *clamp = [CIFilter filterWithName:@"CIAffineClamp"]; + [clamp setValue:backgroundImage forKey:kCIInputImageKey]; + [clamp setValue:[NSValue valueWithBytes:&transform objCType:@encode(CGAffineTransform)] forKey:@"inputTransform"]; + + CIFilter *attributionBlurFilter = [CIFilter filterWithName:@"CIGaussianBlur"]; + [attributionBlurFilter setValue:[clamp outputImage] forKey:kCIInputImageKey]; + [attributionBlurFilter setValue:@10 forKey:kCIInputRadiusKey]; + + CIFilter *attributionColorFilter = [CIFilter filterWithName:@"CIColorControls"]; + [attributionColorFilter setValue:[attributionBlurFilter outputImage] forKey:kCIInputImageKey]; + [attributionColorFilter setValue:@(0.1) forKey:kCIInputBrightnessKey]; + + CIImage *blurredImage = attributionColorFilter.outputImage; + + CIContext *ctx = [CIContext contextWithOptions:nil]; + CGImageRef cgimg = [ctx createCGImage:blurredImage fromRect:[backgroundImage extent]]; + +#if TARGET_OS_IPHONE + + return [UIImage imageWithCGImage:cgimg]; +#else + + return [[NSImage alloc] initWithCGImage:cgimg size:[backgroundImage extent].size]; +#endif +} + - (void)cancel { _snapshotCallback.reset(); diff --git a/platform/darwin/src/MGLShapeSource.h b/platform/darwin/src/MGLShapeSource.h index d0097f748e..7460c83f50 100644 --- a/platform/darwin/src/MGLShapeSource.h +++ b/platform/darwin/src/MGLShapeSource.h @@ -85,7 +85,8 @@ extern MGL_EXPORT const MGLShapeSourceOption MGLShapeSourceOptionSimplificationT or they may be defined by local or external <a href="http://geojson.org/">GeoJSON</a> code. A shape source is added to an `MGLStyle` object along with an `MGLVectorStyleLayer` object. The vector style - layer defines the appearance of any content supplied by the shape source. + layer defines the appearance of any content supplied by the shape source. You + can update a shape source by setting its `shape` or `URL` property. Each <a href="https://www.mapbox.com/mapbox-gl-style-spec/#sources-geojson"><code>geojson</code></a> diff --git a/platform/darwin/src/MGLStyle.mm b/platform/darwin/src/MGLStyle.mm index 52efc7a85a..244fb94ef9 100644 --- a/platform/darwin/src/MGLStyle.mm +++ b/platform/darwin/src/MGLStyle.mm @@ -638,7 +638,7 @@ static NSURL *MGLStyleURL_trafficNight; self.URL ? [NSString stringWithFormat:@"\"%@\"", self.URL] : self.URL]; } -#pragma mark Style language preferences +#pragma mark Mapbox Streets source introspection - (void)setLocalizesLabels:(BOOL)localizesLabels { @@ -749,4 +749,29 @@ static NSURL *MGLStyleURL_trafficNight; } } +- (NS_SET_OF(MGLVectorSource *) *)mapboxStreetsSources { + return [self.sources objectsPassingTest:^BOOL (__kindof MGLVectorSource * _Nonnull source, BOOL * _Nonnull stop) { + return [source isKindOfClass:[MGLVectorSource class]] && source.mapboxStreets; + }]; +} + +- (NS_ARRAY_OF(MGLStyleLayer *) *)placeStyleLayers { + NSSet *streetsSourceIdentifiers = [self.mapboxStreetsSources valueForKey:@"identifier"]; + + NSSet *placeSourceLayerIdentifiers = [NSSet setWithObjects:@"marine_label", @"country_label", @"state_label", @"place_label", @"water_label", @"poi_label", @"rail_station_label", @"mountain_peak_label", nil]; + NSPredicate *isPlacePredicate = [NSPredicate predicateWithBlock:^BOOL (MGLVectorStyleLayer * _Nullable layer, NSDictionary<NSString *, id> * _Nullable bindings) { + return [layer isKindOfClass:[MGLVectorStyleLayer class]] && [streetsSourceIdentifiers containsObject:layer.sourceIdentifier] && [placeSourceLayerIdentifiers containsObject:layer.sourceLayerIdentifier]; + }]; + return [self.layers filteredArrayUsingPredicate:isPlacePredicate]; +} + +- (NS_ARRAY_OF(MGLStyleLayer *) *)roadStyleLayers { + NSSet *streetsSourceIdentifiers = [self.mapboxStreetsSources valueForKey:@"identifier"]; + + NSPredicate *isPlacePredicate = [NSPredicate predicateWithBlock:^BOOL (MGLVectorStyleLayer * _Nullable layer, NSDictionary<NSString *, id> * _Nullable bindings) { + return [layer isKindOfClass:[MGLVectorStyleLayer class]] && [streetsSourceIdentifiers containsObject:layer.sourceIdentifier] && [layer.sourceLayerIdentifier isEqualToString:@"road_label"]; + }]; + return [self.layers filteredArrayUsingPredicate:isPlacePredicate]; +} + @end diff --git a/platform/darwin/src/MGLStyle_Private.h b/platform/darwin/src/MGLStyle_Private.h index 92b08e844b..e5bd79dc02 100644 --- a/platform/darwin/src/MGLStyle_Private.h +++ b/platform/darwin/src/MGLStyle_Private.h @@ -14,6 +14,8 @@ namespace mbgl { @class MGLAttributionInfo; @class MGLMapView; @class MGLOpenGLStyleLayer; +@class MGLVectorSource; +@class MGLVectorStyleLayer; @interface MGLStyle (Private) @@ -30,4 +32,11 @@ namespace mbgl { @end +@interface MGLStyle (MGLStreetsAdditions) + +@property (nonatomic, readonly, copy) NS_ARRAY_OF(MGLVectorStyleLayer *) *placeStyleLayers; +@property (nonatomic, readonly, copy) NS_ARRAY_OF(MGLVectorStyleLayer *) *roadStyleLayers; + +@end + NS_ASSUME_NONNULL_END diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index 37a44549ba..404b4c7e2d 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -4,6 +4,12 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT ## master +### Annotations and user interaction + +* Increased the default maximum zoom level from 20 to 22. ([#9835](https://github.com/mapbox/mapbox-gl-native/pull/9835)) + +## 3.7.0 + ### Networking and storage * Added a new `MGLMapSnapshotter` class for capturing rendered map images from an `MGLMapView`’s camera. ([#9891](https://github.com/mapbox/mapbox-gl-native/pull/9891)) @@ -23,19 +29,29 @@ Mapbox welcomes participation and contributions from everyone. Please read [CONT * Fixed an issue that could cause antialiasing between polygons on the same layer to fail if the fill layers used data-driven styling for the fill color. ([#9699](https://github.com/mapbox/mapbox-gl-native/pull/9699)) * The previously deprecated support for style classes has been removed. For interface compatibility, the API methods remain, but they are now non-functional. -### Annotations and user interaction +### Annotations * Fixed several bugs and performance issues related to the use of annotations backed by `MGLAnnotationImage`. The limits on the number and size of images and glyphs has been effectively eliminated and should now depend on hardware constraints. These fixes also apply to images used to represent icons in `MGLSymbolStyleLayer`. ([#9213](https://github.com/mapbox/mapbox-gl-native/pull/9213)) -* Increased the default maximum zoom level from 20 to 22. ([#9835](https://github.com/mapbox/mapbox-gl-native/pull/9835)) * Added an `overlays` property to `MGLMapView`. ([#8617](https://github.com/mapbox/mapbox-gl-native/pull/8617)) * Selecting an annotation no longer sets the user tracking mode to `MGLUserTrackingModeNone`. ([#10094](https://github.com/mapbox/mapbox-gl-native/pull/10094)) * Added `-[MGLMapView cameraThatFitsShape:direction:edgePadding:]` 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)) +* Added support selection of shape and polyline annotations.([#9984](https://github.com/mapbox/mapbox-gl-native/pull/9984)) +* Fixed an issue where view annotations could be slightly misaligned. View annotation placement is now rounded to the nearest pixel. ([#10219](https://github.com/mapbox/mapbox-gl-native/pull/10219)) +* Fixed an issue where a shape annotation callout was not displayed if the centroid was not visible. ([#10255](https://github.com/mapbox/mapbox-gl-native/pull/10255)) + +### User interaction + +* Users of VoiceOver can now swipe left and right to navigate among visible places, points of interest, and roads. ([#9950](https://github.com/mapbox/mapbox-gl-native/pull/9950)) +* Increased the default maximum zoom level from 20 to 22. ([#9835](https://github.com/mapbox/mapbox-gl-native/pull/9835)) ### Other changes +* Added a Bulgarian localization. ([#10309](https://github.com/mapbox/mapbox-gl-native/pull/10309)) * Fixed an issue that could cause line label rendering glitches when the line geometry is projected to a point behind the plane of the camera. ([#9865](https://github.com/mapbox/mapbox-gl-native/pull/9865)) * Fixed an issue that could cause a crash when using `-[MGLMapView flyToCamera:completionHandler:]` and related methods with zoom levels at or near the maximum value. ([#9381](https://github.com/mapbox/mapbox-gl-native/pull/9381)) * Added `-[MGLMapView showAttribution:]` to allow custom attribution buttons to show the default attribution interface. ([#10085](https://github.com/mapbox/mapbox-gl-native/pull/10085)) +* Fixed a conflict between multiple copies of SMCalloutView in a project. ([#10183](https://github.com/mapbox/mapbox-gl-native/pull/10183)) +* Fixed a crash when enabling the scale bar in iOS 8. ([#10241](https://github.com/mapbox/mapbox-gl-native/pull/10241)) ## 3.6.4 - September 25, 2017 diff --git a/platform/ios/Mapbox-iOS-SDK-nightly-dynamic.podspec b/platform/ios/Mapbox-iOS-SDK-nightly-dynamic.podspec index 1abcaf467c..b8b6687f0c 100644 --- a/platform/ios/Mapbox-iOS-SDK-nightly-dynamic.podspec +++ b/platform/ios/Mapbox-iOS-SDK-nightly-dynamic.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |m| - version = '3.7.0-alpha.1' + version = '3.7.0-beta.4' m.name = 'Mapbox-iOS-SDK-nightly-dynamic' m.version = "#{version}-nightly" diff --git a/platform/ios/Mapbox-iOS-SDK-symbols.podspec b/platform/ios/Mapbox-iOS-SDK-symbols.podspec index ceb7f21c47..e84255715d 100644 --- a/platform/ios/Mapbox-iOS-SDK-symbols.podspec +++ b/platform/ios/Mapbox-iOS-SDK-symbols.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |m| - version = '3.7.0-alpha.1' + version = '3.7.0-beta.4' m.name = 'Mapbox-iOS-SDK-symbols' m.version = "#{version}-symbols" diff --git a/platform/ios/Mapbox-iOS-SDK.podspec b/platform/ios/Mapbox-iOS-SDK.podspec index bcd37d0d06..bb6c5e0123 100644 --- a/platform/ios/Mapbox-iOS-SDK.podspec +++ b/platform/ios/Mapbox-iOS-SDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |m| - version = '3.7.0-alpha.1' + version = '3.7.0-beta.4' m.name = 'Mapbox-iOS-SDK' m.version = version diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index 07838bc6bd..2c3d26b489 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -165,7 +165,9 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { self.mapView.scaleBar.hidden = NO; self.mapView.showsUserHeadingIndicator = YES; self.hudLabel.hidden = YES; - self.hudLabel.titleLabel.font = [UIFont monospacedDigitSystemFontOfSize:10 weight:UIFontWeightRegular]; + if ([UIFont respondsToSelector:@selector(monospacedDigitSystemFontOfSize:weight:)]) { + self.hudLabel.titleLabel.font = [UIFont monospacedDigitSystemFontOfSize:10 weight:UIFontWeightRegular]; + } if ([MGLAccountManager accessToken].length) { @@ -827,6 +829,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) { } MGLPolygon *polygon = [MGLPolygon polygonWithCoordinates:polygonCoordinates count:[stateCoordinatePairs count]]; + polygon.title = feature[@"properties"][@"NAME"]; [self.mapView addAnnotation:polygon]; diff --git a/platform/ios/app/bg.lproj/Localizable.strings b/platform/ios/app/bg.lproj/Localizable.strings new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/platform/ios/app/bg.lproj/Localizable.strings diff --git a/platform/ios/bitrise.yml b/platform/ios/bitrise.yml index 9ca44e1d47..24bd054dbc 100644 --- a/platform/ios/bitrise.yml +++ b/platform/ios/bitrise.yml @@ -4,6 +4,8 @@ default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git trigger_map: - pattern: nightly-release workflow: nightly-release +- pattern: release-from-tag + workflow: release-from-tag - pattern: "*" is_pull_request_allowed: true workflow: primary @@ -11,6 +13,12 @@ workflows: primary: steps: - script: + title: Skip Workflow + inputs: + - content: echo "This workflow is obsolete — see CircleCi." + nightly-release: + steps: + - script: title: Install Dependencies inputs: - content: |- @@ -19,41 +27,40 @@ workflows: brew install cmake - is_debug: 'yes' - script: - title: Generate Workspace + title: Configure AWS-CLI + inputs: + - content: |- + #!/bin/bash + pip install awscli + - script: + title: Build package inputs: - content: |- #!/bin/bash set -eu -o pipefail - export BUILDTYPE=Debug - make iproj + export BUILDTYPE=Release + export BUILD_DEVICE=true + export FORMAT=dynamic + make ipackage-strip + CLOUDWATCH=true platform/ios/scripts/metrics.sh + platform/ios/scripts/deploy-nightly.sh - is_debug: 'yes' - - xcode-test: - title: Run SDK Unit Tests - inputs: - - project_path: platform/ios/ios.xcworkspace - - scheme: CI - - deploy-to-bitrise-io: - title: Deploy to Bitrise.io - inputs: - - notify_user_groups: none - slack: title: Post to Slack inputs: - webhook_url: "$SLACK_HOOK_URL" - channel: "#gl-bots" - - from_username: 'Bitrise iOS' - - from_username_on_error: 'Bitrise iOS' + - from_username: 'Bitrise iOS Nightly 💤' + - from_username_on_error: 'Bitrise iOS Nightly 💤' - message: '<${BITRISE_BUILD_URL}|Build #${BITRISE_BUILD_NUMBER}> - for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}> - by ${GIT_CLONE_COMMIT_COMMITER_NAME} - passed' + for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}@%7B1day%7D...${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}> + completed successfully.' - message_on_error: '<${BITRISE_BUILD_URL}|Build #${BITRISE_BUILD_NUMBER}> - for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}> - by ${GIT_CLONE_COMMIT_COMMITER_NAME} - failed' + for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}@%7B1day%7D...${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}> + failed.' - icon_url: https://bitrise-public-content-production.s3.amazonaws.com/slack/bitrise-slack-icon-128.png - icon_url_on_error: https://bitrise-public-content-production.s3.amazonaws.com/slack/bitrise-slack-error-icon-128.png - nightly-release: + release-from-tag: steps: - script: title: Install Dependencies @@ -71,25 +78,21 @@ workflows: - content: |- #!/bin/bash set -eu -o pipefail - export BUILDTYPE=Release - export BUILD_DEVICE=true - export FORMAT=dynamic - make ipackage-strip - CLOUDWATCH=true platform/ios/scripts/metrics.sh - platform/ios/scripts/deploy-nightly.sh + export VERSION_TAG=${BITRISE_GIT_TAG} + platform/ios/scripts/deploy-packages.sh - is_debug: 'yes' - slack: title: Post to Slack inputs: - webhook_url: "$SLACK_HOOK_URL" - channel: "#gl-bots" - - from_username: 'Bitrise iOS Nightly \U0001F31D' - - from_username_on_error: 'Bitrise iOS Nightly \U0001F31D' + - from_username: 'Bitrise iOS Deploy' + - from_username_on_error: 'Bitrise iOS Deploy' - message: '<${BITRISE_BUILD_URL}|Build #${BITRISE_BUILD_NUMBER}> - for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}@%7B1day%7D...${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}> + for <https://github.com/mapbox/mapbox-gl-native/releases/tag/${BITRISE_GIT_TAG}|`${BITRISE_GIT_TAG}`> completed successfully.' - message_on_error: '<${BITRISE_BUILD_URL}|Build #${BITRISE_BUILD_NUMBER}> - for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}@%7B1day%7D...${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}> + for <https://github.com/mapbox/mapbox-gl-native/releases/tag/${BITRISE_GIT_TAG}|`${BITRISE_GIT_TAG}`> failed.' - icon_url: https://bitrise-public-content-production.s3.amazonaws.com/slack/bitrise-slack-icon-128.png - icon_url_on_error: https://bitrise-public-content-production.s3.amazonaws.com/slack/bitrise-slack-error-icon-128.png diff --git a/platform/ios/docs/guides/Adding Points to a Map.md b/platform/ios/docs/guides/Adding Points to a Map.md index 2698d5564f..2844075cc7 100644 --- a/platform/ios/docs/guides/Adding Points to a Map.md +++ b/platform/ios/docs/guides/Adding Points to a Map.md @@ -51,7 +51,7 @@ To use annotation views, implement `MGLMapViewDelegate` `-mapView:viewForAnnotat * No limit on style or image size * Full support for animations * Relative control over z-ordering using the `zPosition` property on `CALayer` -* [Familiar API for MapKit users](https://www.mapbox.com/help/switch-mapkit/#annotations-pins) +* Familiar API for MapKit users **Cons** @@ -63,7 +63,7 @@ To use annotation views, implement `MGLMapViewDelegate` `-mapView:viewForAnnotat For absolute full control of how points are displayed on a map, consider [runtime styling](runtime-styling.html). -You can use `MGLPointFeature` or any of the other [style feature subclasses](Style%20Features.html) to add points and shapes to an `MGLShapeSource`. +You can use `MGLPointFeature` or any other [style primitives](Style%20Primitives.html) to add points and shapes to an `MGLShapeSource`. From there, you can create one or many `MGLSymbolStyleLayer` or `MGLCircleStyleLayer` layers to filter and style points for display on the map ([example](https://www.mapbox.com/ios-sdk/examples/runtime-multiple-annotations)). diff --git a/platform/ios/framework/Settings.bundle/bg.lproj/Root.strings b/platform/ios/framework/Settings.bundle/bg.lproj/Root.strings new file mode 100644 index 0000000000..c86decde32 --- /dev/null +++ b/platform/ios/framework/Settings.bundle/bg.lproj/Root.strings @@ -0,0 +1,3 @@ +"TELEMETRY_GROUP_TITLE" = "Настройки за поверителност"; +"TELEMETRY_SWITCH_TITLE" = "Mapbox Телеметрия"; +"TELEMETRY_GROUP_FOOTER" = "Тази настройка позволява на приложението да споделя анонимни локации и данни за използване с Mapbox."; diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index a3ccc9337b..ad17e00673 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -292,10 +292,15 @@ DA35A2CB1CCAAAD200E826B2 /* NSValue+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = DA35A2C81CCAAAD200E826B2 /* NSValue+MGLAdditions.m */; }; DA35A2CC1CCAAAD200E826B2 /* NSValue+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = DA35A2C81CCAAAD200E826B2 /* NSValue+MGLAdditions.m */; }; DA35D0881E1A6309007DED41 /* one-liner.json in Resources */ = {isa = PBXBuildFile; fileRef = DA35D0871E1A6309007DED41 /* one-liner.json */; }; + DA5DB12A1FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DA5DB1291FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m */; }; DA6408DB1DA4E7D300908C90 /* MGLVectorStyleLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = DA6408D91DA4E7D300908C90 /* MGLVectorStyleLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; DA6408DC1DA4E7D300908C90 /* MGLVectorStyleLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = DA6408D91DA4E7D300908C90 /* MGLVectorStyleLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; DA6408DD1DA4E7D300908C90 /* MGLVectorStyleLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = DA6408DA1DA4E7D300908C90 /* MGLVectorStyleLayer.m */; }; DA6408DE1DA4E7D300908C90 /* MGLVectorStyleLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = DA6408DA1DA4E7D300908C90 /* MGLVectorStyleLayer.m */; }; + DA704CC21F65A475004B3F28 /* MGLMapAccessibilityElement.h in Headers */ = {isa = PBXBuildFile; fileRef = DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */; }; + DA704CC31F65A475004B3F28 /* MGLMapAccessibilityElement.h in Headers */ = {isa = PBXBuildFile; fileRef = DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */; }; + DA704CC41F65A475004B3F28 /* MGLMapAccessibilityElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.mm */; }; + DA704CC51F65A475004B3F28 /* MGLMapAccessibilityElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.mm */; }; DA72620B1DEEE3480043BB89 /* MGLOpenGLStyleLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = DA7262091DEEE3480043BB89 /* MGLOpenGLStyleLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; DA72620C1DEEE3480043BB89 /* MGLOpenGLStyleLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = DA7262091DEEE3480043BB89 /* MGLOpenGLStyleLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; DA72620D1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA72620A1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm */; }; @@ -774,6 +779,14 @@ DA2E885D1CC0382C00F24E7B /* MGLOfflinePackTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MGLOfflinePackTests.m; path = ../../darwin/test/MGLOfflinePackTests.m; sourceTree = "<group>"; }; DA2E885E1CC0382C00F24E7B /* MGLOfflineRegionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MGLOfflineRegionTests.m; path = ../../darwin/test/MGLOfflineRegionTests.m; sourceTree = "<group>"; }; DA2E88601CC0382C00F24E7B /* MGLStyleTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = MGLStyleTests.mm; path = ../../darwin/test/MGLStyleTests.mm; sourceTree = "<group>"; }; + DA33895F1FA3EAB7001EA329 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Foundation.strings"; sourceTree = "<group>"; }; + DA3389651FA3EE1B001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; }; + DA3389661FA3EE28001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Foundation.strings; sourceTree = "<group>"; }; + DA3389671FA3EE2F001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Foundation.stringsdict; sourceTree = "<group>"; }; + DA3389681FA3EE48001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; }; + DA3389691FA3EE50001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; + DA33896A1FA3EE58001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Root.strings; sourceTree = "<group>"; }; + DA33896B1FA3EF4A001EA329 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Foundation.stringsdict; sourceTree = "<group>"; }; DA35A29D1CC9E94C00E826B2 /* MGLCoordinateFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLCoordinateFormatter.h; sourceTree = "<group>"; }; DA35A2A01CC9E95F00E826B2 /* MGLCoordinateFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLCoordinateFormatter.m; sourceTree = "<group>"; }; DA35A2A91CCA058D00E826B2 /* MGLCoordinateFormatterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MGLCoordinateFormatterTests.m; path = ../../darwin/test/MGLCoordinateFormatterTests.m; sourceTree = "<group>"; }; @@ -794,6 +807,7 @@ DA57D4AC1EBA922A00793288 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = vi; path = vi.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; DA5C09BA1EFC48550056B178 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; }; DA5C09BB1EFC486C0056B178 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; }; + DA5DB1291FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MGLMapAccessibilityElementTests.m; sourceTree = "<group>"; }; DA6023F11E4CE94300DBFF23 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Foundation.strings; sourceTree = "<group>"; }; DA6023F21E4CE94800DBFF23 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sv; path = sv.lproj/Foundation.stringsdict; sourceTree = "<group>"; }; DA618B111E68823600CB7F44 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; @@ -812,6 +826,8 @@ DA704CBB1F637311004B3F28 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Foundation.strings; sourceTree = "<group>"; }; DA704CBC1F637405004B3F28 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; DA704CBD1F63746E004B3F28 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; + DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapAccessibilityElement.h; sourceTree = "<group>"; }; + DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLMapAccessibilityElement.mm; sourceTree = "<group>"; }; DA704CC71F6663A3004B3F28 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Foundation.strings; sourceTree = "<group>"; }; DA7262091DEEE3480043BB89 /* MGLOpenGLStyleLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLOpenGLStyleLayer.h; sourceTree = "<group>"; }; DA72620A1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLOpenGLStyleLayer.mm; sourceTree = "<group>"; }; @@ -1111,6 +1127,8 @@ 35599DB81D46AD7F0048254D /* Categories */ = { isa = PBXGroup; children = ( + 1FDD9D6D1F26936400252B09 /* MGLVectorSource+MGLAdditions.h */, + 1FDD9D6E1F26936400252B09 /* MGLVectorSource+MGLAdditions.m */, 350098DA1D484E60004B2AF0 /* NSValue+MGLStyleAttributeAdditions.h */, 350098DB1D484E60004B2AF0 /* NSValue+MGLStyleAttributeAdditions.mm */, ); @@ -1338,6 +1356,7 @@ 3598544C1E1D38AA00B29F84 /* MGLDistanceFormatterTests.m */, DA0CD58F1CF56F6A00A5F5A5 /* MGLFeatureTests.mm */, DA2E885C1CC0382C00F24E7B /* MGLGeometryTests.mm */, + DA5DB1291FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m */, 35E208A61D24210F00EC9A46 /* MGLNSDataAdditionsTests.m */, 1F95931C1E6DE2E900D5B294 /* MGLNSDateAdditionsTests.mm */, DAE7DEC11E245455007505A6 /* MGLNSStringAdditionsTests.m */, @@ -1415,6 +1434,8 @@ 35CE617F1D4165C2004F2359 /* Categories */, DAD165841CF4D06B001FF4B9 /* Annotations */, DAD165851CF4D08B001FF4B9 /* Telemetry */, + DA704CC01F65A475004B3F28 /* MGLMapAccessibilityElement.h */, + DA704CC11F65A475004B3F28 /* MGLMapAccessibilityElement.mm */, DA8848361CBAFB8500AB86E3 /* MGLMapView.h */, DA17BE2F1CC4BAC300402C41 /* MGLMapView_Private.h */, DA8848371CBAFB8500AB86E3 /* MGLMapView+IBAdditions.h */, @@ -1612,8 +1633,6 @@ DAD165831CF4CFED001FF4B9 /* Categories */ = { isa = PBXGroup; children = ( - 1FDD9D6D1F26936400252B09 /* MGLVectorSource+MGLAdditions.h */, - 1FDD9D6E1F26936400252B09 /* MGLVectorSource+MGLAdditions.m */, 7E016D821D9E890300A29A21 /* MGLPolygon+MGLAdditions.h */, 7E016D831D9E890300A29A21 /* MGLPolygon+MGLAdditions.m */, 7E016D7C1D9E86BE00A29A21 /* MGLPolyline+MGLAdditions.h */, @@ -1754,6 +1773,7 @@ DA35A2C91CCAAAD200E826B2 /* NSValue+MGLAdditions.h in Headers */, 3510FFEA1D6D9C7A00F413B2 /* NSComparisonPredicate+MGLAdditions.h in Headers */, DA6408DB1DA4E7D300908C90 /* MGLVectorStyleLayer.h in Headers */, + DA704CC21F65A475004B3F28 /* MGLMapAccessibilityElement.h in Headers */, DD0902AB1DB192A800C5BDCE /* MGLNetworkConfiguration.h in Headers */, DA8848571CBAFB9800AB86E3 /* MGLMapboxEvents.h in Headers */, 35D3A1E61E9BE7EB002B38EE /* MGLScaleBar.h in Headers */, @@ -1889,6 +1909,7 @@ 558DE7A11E5615E400C7916D /* MGLFoundation_Private.h in Headers */, 3538AA1E1D542239008EC33D /* MGLForegroundStyleLayer.h in Headers */, 30E578181DAA85520050F07E /* UIImage+MGLAdditions.h in Headers */, + DA704CC31F65A475004B3F28 /* MGLMapAccessibilityElement.h in Headers */, 40F887711D7A1E59008ECB67 /* MGLShapeSource_Private.h in Headers */, DABFB8631CBE99E500D62B32 /* MGLOfflineRegion.h in Headers */, DA35A2B21CCA141D00E826B2 /* MGLCompassDirectionFormatter.h in Headers */, @@ -2109,6 +2130,7 @@ fi, nl, hu, + bg, ); mainGroup = DA1DC9411CB6C1C2006E619F; productRefGroup = DA1DC94B1CB6C1C2006E619F /* Products */; @@ -2267,6 +2289,7 @@ DA2E88621CC0382C00F24E7B /* MGLOfflinePackTests.m in Sources */, 55E2AD131E5B125400E8C587 /* MGLOfflineStorageTests.mm in Sources */, 920A3E5D1E6F995200C16EFC /* MGLSourceQueryTests.m in Sources */, + DA5DB12A1FABF1EE001C2326 /* MGLMapAccessibilityElementTests.m in Sources */, FAE1CDCB1E9D79CB00C40B5B /* MGLFillExtrusionStyleLayerTests.mm in Sources */, DA35A2AA1CCA058D00E826B2 /* MGLCoordinateFormatterTests.m in Sources */, 357579831D502AE6000B822E /* MGLRasterStyleLayerTests.mm in Sources */, @@ -2355,6 +2378,7 @@ DA88482A1CBAFA6200AB86E3 /* MGLTilePyramidOfflineRegion.mm in Sources */, 4049C29F1DB6CD6C00B3F799 /* MGLPointCollection.mm in Sources */, 35136D3F1D42273000C20EFD /* MGLLineStyleLayer.mm in Sources */, + DA704CC41F65A475004B3F28 /* MGLMapAccessibilityElement.mm in Sources */, DA72620D1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm in Sources */, DA88481A1CBAFA6200AB86E3 /* MGLAccountManager.m in Sources */, 3510FFFB1D6DCC4700F413B2 /* NSCompoundPredicate+MGLAdditions.mm in Sources */, @@ -2442,6 +2466,7 @@ DAA4E4211CBB730400178DFB /* MGLOfflineStorage.mm in Sources */, 4049C2A01DB6CD6C00B3F799 /* MGLPointCollection.mm in Sources */, 35136D401D42273000C20EFD /* MGLLineStyleLayer.mm in Sources */, + DA704CC51F65A475004B3F28 /* MGLMapAccessibilityElement.mm in Sources */, DA72620E1DEEE3480043BB89 /* MGLOpenGLStyleLayer.mm in Sources */, DAA4E42F1CBB730400178DFB /* MGLCompactCalloutView.m in Sources */, 3510FFFC1D6DCC4700F413B2 /* NSCompoundPredicate+MGLAdditions.mm in Sources */, @@ -2520,6 +2545,7 @@ DA618B2B1E68932D00CB7F44 /* fi */, DAE8CCAD1E6E8C70009B5CB0 /* nl */, DA5C09BA1EFC48550056B178 /* hu */, + DA3389651FA3EE1B001EA329 /* bg */, ); name = Localizable.strings; sourceTree = "<group>"; @@ -2544,6 +2570,7 @@ DA618B2C1E68933600CB7F44 /* fi */, DAE8CCAE1E6E8C76009B5CB0 /* nl */, DACCD9C81F1F473700BB09A1 /* hu */, + DA33896A1FA3EE58001EA329 /* bg */, ); name = Root.strings; sourceTree = "<group>"; @@ -2566,6 +2593,7 @@ DA1AC01B1E5B8774006DF1D6 /* lt */, DA618B1B1E68884E00CB7F44 /* ca */, DA5C09BB1EFC486C0056B178 /* hu */, + DA3389681FA3EE48001EA329 /* bg */, ); name = Localizable.strings; sourceTree = "<group>"; @@ -2585,6 +2613,8 @@ DAE9E0F11EB7BF1B001E8E8B /* es */, DA704CBB1F637311004B3F28 /* ru */, DA704CC71F6663A3004B3F28 /* uk */, + DA33895F1FA3EAB7001EA329 /* pt-BR */, + DA3389661FA3EE28001EA329 /* bg */, ); name = Foundation.strings; sourceTree = "<group>"; @@ -2604,6 +2634,8 @@ DA618B1D1E6888F500CB7F44 /* ca */, DA618B261E68920D00CB7F44 /* lt */, DACFE7981F66EA2100630DA8 /* vi */, + DA3389671FA3EE2F001EA329 /* bg */, + DA33896B1FA3EF4A001EA329 /* hu */, ); name = Foundation.stringsdict; sourceTree = "<group>"; @@ -2631,6 +2663,7 @@ DA57D4AC1EBA922A00793288 /* vi */, DA704CBC1F637405004B3F28 /* uk */, DA704CBD1F63746E004B3F28 /* zh-Hant */, + DA3389691FA3EE50001EA329 /* bg */, ); name = Localizable.stringsdict; sourceTree = "<group>"; diff --git a/platform/ios/resources/Base.lproj/Localizable.strings b/platform/ios/resources/Base.lproj/Localizable.strings index 3f59262d71..039ef4c4b1 100644 --- a/platform/ios/resources/Base.lproj/Localizable.strings +++ b/platform/ios/resources/Base.lproj/Localizable.strings @@ -34,6 +34,9 @@ /* Accessibility label */ "INFO_A11Y_LABEL" = "About this map"; +/* List separator */ +"LIST_SEPARATOR" = ", "; + /* User-friendly error description */ "LOAD_MAP_FAILED_DESC" = "The map failed to load because an unknown error occurred."; @@ -46,12 +49,30 @@ /* Accessibility label */ "MAP_A11Y_LABEL" = "Map"; -/* Map accessibility value */ -"MAP_A11Y_VALUE" = "Zoom %1$dx\n%2$ld annotation(s) visible"; +/* Map accessibility value; {number of visible annotations} */ +"MAP_A11Y_VALUE_ANNOTATIONS" = "%ld annotation(s) visible."; + +/* Map accessibility value; {list of visible places} */ +"MAP_A11Y_VALUE_PLACES" = "Places visible: %@."; + +/* Map accessibility value; {number of visible roads} */ +"MAP_A11Y_VALUE_ROADS" = "%ld road(s) visible."; + +/* Map accessibility value; {zoom level} */ +"MAP_A11Y_VALUE_ZOOM" = "Zoom %dx."; /* User-friendly error description */ "PARSE_STYLE_FAILED_DESC" = "The map failed to load because the style is corrupted."; +/* Accessibility value indicating that a road is a divided road (dual carriageway) */ +"ROAD_DIVIDED_A11Y_VALUE" = "Divided road"; + +/* Accessibility value indicating that a road is a one-way road */ +"ROAD_ONEWAY_A11Y_VALUE" = "One way"; + +/* String format for accessibility value for road feature; {route number} */ +"ROAD_REF_A11Y_FMT" = "Route %@"; + /* Action sheet title */ "SDK_NAME" = "Mapbox iOS SDK"; diff --git a/platform/ios/resources/bg.lproj/Localizable.strings b/platform/ios/resources/bg.lproj/Localizable.strings new file mode 100644 index 0000000000..7098589b6b --- /dev/null +++ b/platform/ios/resources/bg.lproj/Localizable.strings @@ -0,0 +1,93 @@ +/* Accessibility hint */ +"ANNOTATION_A11Y_HINT" = "Показва повече инфо"; + +/* No comment provided by engineer. */ +"API_CLIENT_400_DESC" = "Неуспешна сесия за данни. Оригинална заявка: %@"; + +/* No comment provided by engineer. */ +"API_CLIENT_400_REASON" = "Статус кодът беше %ld"; + +/* No comment provided by engineer. */ +"CANCEL" = "Отказ"; + +/* Accessibility hint for closing the selected annotation’s callout view and returning to the map */ +"CLOSE_CALLOUT_A11Y_HINT" = "Връща към картата"; + +/* Accessibility hint */ +"COMPASS_A11Y_HINT" = "Завърта картата в посока север"; + +/* Accessibility label */ +"COMPASS_A11Y_LABEL" = "Компас"; + +/* Compass abbreviation for north */ +"COMPASS_NORTH" = "С"; + +/* Instructions in Interface Builder designable; {key}, {plist file name} */ +"DESIGNABLE" = "За да се показва Mapbox карта тук, добави %1$@ към твоя токен за достъп в %2$@\n\nЗа подробни инструкции, виж:"; + +/* Setup documentation URL display string; keep as short as possible */ +"FIRST_STEPS_URL" = "mapbox.com/help/first-steps-ios-sdk"; + +/* Accessibility hint */ +"INFO_A11Y_HINT" = "Показва кредитите, форма за връзка и още"; + +/* Accessibility label */ +"INFO_A11Y_LABEL" = "За тази карта"; + +/* User-friendly error description */ +"LOAD_MAP_FAILED_DESC" = "Картата не се зареди поради неизвестна грешка."; + +/* User-friendly error description */ +"LOAD_STYLE_FAILED_DESC" = "Картата не се зареди поради незареждане на стила."; + +/* Accessibility label */ +"LOGO_A11Y_LABEL" = "Mapbox"; + +/* Accessibility label */ +"MAP_A11Y_LABEL" = "Карта"; + +/* Map accessibility value */ +"MAP_A11Y_VALUE" = "Мащаб %1$dх\n@2$ld видима(и) анотация(и)"; + +/* User-friendly error description */ +"PARSE_STYLE_FAILED_DESC" = "Картата не се зареди поради повреден стил."; + +/* Action sheet title */ +"SDK_NAME" = "Mapbox iOS SDK"; + +/* Developer-only SDK update notification; {latest version, in format x.x.x} */ +"SDK_UPDATE_AVAILABLE" = "Сега е налична Mapbox iOS SDK версия %@:"; + +/* User-friendly error description */ +"STYLE_NOT_FOUND_DESC" = "Картата не се зареди поради неоткрит или несъвместим стил."; + +/* Telemetry prompt message */ +"TELEMETRY_DISABLED_MSG" = "Можеш да помогнеш OpenStreetMap и Mapbox да станат по-добри, като предоставиш анонимни данни за потребление."; + +/* Telemetry prompt button */ +"TELEMETRY_DISABLED_OFF" = "Не участвам"; + +/* Telemetry prompt button */ +"TELEMETRY_DISABLED_ON" = "Участвам"; + +/* Telemetry prompt message */ +"TELEMETRY_ENABLED_MSG" = "Помагаш OpenStreetMap и Mapbox да станат по-добри, като предоставяш анонимни данни за потребление."; + +/* Telemetry prompt button */ +"TELEMETRY_ENABLED_OFF" = "Спирам участие"; + +/* Telemetry prompt button */ +"TELEMETRY_ENABLED_ON" = "Продължавам да участвам"; + +/* Telemetry prompt button */ +"TELEMETRY_MORE" = "Искам още инфо"; + +/* Action in attribution sheet */ +"TELEMETRY_NAME" = "Mapbox Телеметрия"; + +/* Telemetry prompt title */ +"TELEMETRY_TITLE" = "Направи Mapbox картите по-добри"; + +/* Default user location annotation title */ +"USER_DOT_TITLE" = "Сега си тук"; + diff --git a/platform/ios/resources/bg.lproj/Localizable.stringsdict b/platform/ios/resources/bg.lproj/Localizable.stringsdict new file mode 100644 index 0000000000..f155a02acc --- /dev/null +++ b/platform/ios/resources/bg.lproj/Localizable.stringsdict @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>MAP_A11Y_VALUE</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@level@ +%#@count@</string> + <key>level</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>Мащаб %dx</string> + <key>other</key> + <string>Мащаб %dx</string> + </dict> + <key>count</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>ld</string> + <key>one</key> + <string>%d видима анотация</string> + <key>other</key> + <string>%d видими анотации</string> + </dict> + </dict> +</dict> +</plist> diff --git a/platform/ios/resources/en.lproj/Localizable.stringsdict b/platform/ios/resources/en.lproj/Localizable.stringsdict index e849318fe5..435b7bdfe8 100644 --- a/platform/ios/resources/en.lproj/Localizable.stringsdict +++ b/platform/ios/resources/en.lproj/Localizable.stringsdict @@ -2,22 +2,26 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> - <key>MAP_A11Y_VALUE</key> + <key>MAP_A11Y_VALUE_ANNOTATIONS</key> <dict> <key>NSStringLocalizedFormatKey</key> - <string>%#@level@ -%#@count@</string> - <key>level</key> + <string>%#@count@</string> + <key>count</key> <dict> <key>NSStringFormatSpecTypeKey</key> <string>NSStringPluralRuleType</string> <key>NSStringFormatValueTypeKey</key> - <string>d</string> + <string>ld</string> <key>one</key> - <string>Zoom %dx</string> + <string>%d annotation visible</string> <key>other</key> - <string>Zoom %dx</string> + <string>%d annotations visible</string> </dict> + </dict> + <key>MAP_A11Y_VALUE_ROADS</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@count@</string> <key>count</key> <dict> <key>NSStringFormatSpecTypeKey</key> @@ -25,9 +29,25 @@ <key>NSStringFormatValueTypeKey</key> <string>ld</string> <key>one</key> - <string>%d annotation visible</string> + <string>%d road visible</string> <key>other</key> - <string>%d annotations visible</string> + <string>%d roads visible</string> + </dict> + </dict> + <key>MAP_A11Y_VALUE_ZOOM</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@level@</string> + <key>level</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>Zoom %dx</string> + <key>other</key> + <string>Zoom %dx</string> </dict> </dict> </dict> diff --git a/platform/ios/resources/pt-BR.lproj/Localizable.strings b/platform/ios/resources/pt-BR.lproj/Localizable.strings index 56eaa7cf9f..53c3d3a12d 100644 --- a/platform/ios/resources/pt-BR.lproj/Localizable.strings +++ b/platform/ios/resources/pt-BR.lproj/Localizable.strings @@ -2,7 +2,7 @@ "ANNOTATION_A11Y_HINT" = "Mostrar mais informações"; /* No comment provided by engineer. */ -"API_CLIENT_400_DESC" = "The session data task failed. Original request was: %@"; +"API_CLIENT_400_DESC" = "Tarefa de dados da sessão falhou. Requisição original: %@"; /* No comment provided by engineer. */ "API_CLIENT_400_REASON" = "O código de status foi %ld"; @@ -10,6 +10,9 @@ /* No comment provided by engineer. */ "CANCEL" = "Cancelar"; +/* Accessibility hint for closing the selected annotation’s callout view and returning to the map */ +"CLOSE_CALLOUT_A11Y_HINT" = "Retornar ao mapa"; + /* Accessibility hint */ "COMPASS_A11Y_HINT" = "Rotaciona o mapa com face ao norte"; @@ -20,7 +23,7 @@ "COMPASS_NORTH" = "N"; /* Instructions in Interface Builder designable; {key}, {plist file name} */ -"DESIGNABLE" = "To display a Mapbox-hosted map here, set %1$@ to your access token in %2$@\n\nFor detailed instructions, see:"; +"DESIGNABLE" = "Para exibir um mapa hospedado no Mapbox aqui, insira %1$@ para seu token de acesso %2$@\n\nPara maiores detalhes, veja:"; /* Setup documentation URL display string; keep as short as possible */ "FIRST_STEPS_URL" = "mapbox.com/help/first-steps-ios-sdk"; @@ -31,6 +34,12 @@ /* Accessibility label */ "INFO_A11Y_LABEL" = "Sobre este mapa"; +/* User-friendly error description */ +"LOAD_MAP_FAILED_DESC" = "Falha ao carregar mapa devido a um erro desconhecido"; + +/* User-friendly error description */ +"LOAD_STYLE_FAILED_DESC" = "Falha ao carregar mapa porque o estilo não pode ser carregado"; + /* Accessibility label */ "LOGO_A11Y_LABEL" = "Mapbox"; @@ -40,11 +49,20 @@ /* Map accessibility value */ "MAP_A11Y_VALUE" = "Zoom %1$dx\n%2$ld anotações visíveis"; +/* User-friendly error description */ +"PARSE_STYLE_FAILED_DESC" = "Falha ao carregar mapa porque o estilo está corrompido."; + /* Action sheet title */ "SDK_NAME" = "Mapbox iOS SDK"; +/* Developer-only SDK update notification; {latest version, in format x.x.x} */ +"SDK_UPDATE_AVAILABLE" = "SDK Mapbox para iOS versão %@ está disponível:"; + +/* User-friendly error description */ +"STYLE_NOT_FOUND_DESC" = "Falha ao carregar mapa porque o estilo não pode ser encontrado ou é incompatível."; + /* Telemetry prompt message */ -"TELEMETRY_DISABLED_MSG" = "You can help make OpenStreetMap and Mapbox maps better by contributing anonymous usage data."; +"TELEMETRY_DISABLED_MSG" = "Você pode ajudar a tornar o OpenStreetMap e Mapbox ainda melhor contribuindo anonimamente com seus dados de uso."; /* Telemetry prompt button */ "TELEMETRY_DISABLED_OFF" = "Não Participar"; @@ -53,7 +71,7 @@ "TELEMETRY_DISABLED_ON" = "Participar"; /* Telemetry prompt message */ -"TELEMETRY_ENABLED_MSG" = "You are helping to make OpenStreetMap and Mapbox maps better by contributing anonymous usage data."; +"TELEMETRY_ENABLED_MSG" = "Você está ajudando a tornar o OpenStreetMap e Mapbox ainda melhor contribuindo anonimamente com seus dados de uso."; /* Telemetry prompt button */ "TELEMETRY_ENABLED_OFF" = "Parar de Participar"; diff --git a/platform/ios/scripts/deploy-packages.sh b/platform/ios/scripts/deploy-packages.sh index bdc946497d..103e53768c 100755 --- a/platform/ios/scripts/deploy-packages.sh +++ b/platform/ios/scripts/deploy-packages.sh @@ -58,17 +58,17 @@ BINARY_DIRECTORY=${BINARY_DIRECTORY:-build/ios/deploy} GITHUB_RELEASE=${GITHUB_RELEASE:-true} PUBLISH_PRE_FLAG='' +if [[ -z `which github-release` ]]; then + step "Installing github-release…" + brew install github-release + if [ -z `which github-release` ]; then + echo "Unable to install github-release. See: https://github.com/aktau/github-release" + exit 1 + fi +fi + if [[ ${GITHUB_RELEASE} = "true" ]]; then GITHUB_RELEASE=true # Assign bool, not just a string - - if [[ -z `which github-release` ]]; then - step "Installing github-release…" - brew install github-release - if [ -z `which github-release` ]; then - echo "Unable to install github-release. See: https://github.com/aktau/github-release" - exit 1 - fi - fi fi if [[ -z ${VERSION_TAG} ]]; then @@ -83,7 +83,7 @@ if [[ $( echo ${VERSION_TAG} | grep --invert-match ios-v ) ]]; then exit 1 fi -if [[ $( curl --head https://api.github.com/repos/${GITHUB_USER}/${GITHUB_REPO}/releases/tags/${VERSION_TAG} | head -n 1 | grep -c "404 Not Found") == 0 ]]; then +if github-release info --tag ${VERSION_TAG} | grep --quiet "draft: ✗"; then echo "Error: ${VERSION_TAG} has already been published on GitHub" echo "See: https://github.com/${GITHUB_USER}/${GITHUB_REPO}/releases/tag/${VERSION_TAG}" exit 1 diff --git a/platform/ios/scripts/document.sh b/platform/ios/scripts/document.sh index 170debb625..57b596a4b9 100755 --- a/platform/ios/scripts/document.sh +++ b/platform/ios/scripts/document.sh @@ -6,7 +6,7 @@ set -u if [ -z `which jazzy` ]; then echo "Installing jazzy…" - gem install jazzy + gem install jazzy --no-rdoc --no-ri if [ -z `which jazzy` ]; then echo "Unable to install jazzy. See https://github.com/mapbox/mapbox-gl-native/blob/master/platform/ios/INSTALL.md" exit 1 diff --git a/platform/ios/src/MGLCompactCalloutView.h b/platform/ios/src/MGLCompactCalloutView.h index 56c48a99e5..5cecf37ff6 100644 --- a/platform/ios/src/MGLCompactCalloutView.h +++ b/platform/ios/src/MGLCompactCalloutView.h @@ -7,7 +7,7 @@ callout view displays the represented annotation’s title, subtitle, and accessory views in a compact, two-line layout. */ -@interface MGLCompactCalloutView : SMCalloutView <MGLCalloutView> +@interface MGLCompactCalloutView : MGLSMCalloutView <MGLCalloutView> + (instancetype)platformCalloutView; diff --git a/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m index 21f6aaa540..1ed3d86ad1 100644 --- a/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m +++ b/platform/ios/src/MGLFaux3DUserLocationAnnotationView.m @@ -10,7 +10,7 @@ const CGFloat MGLUserLocationAnnotationDotSize = 22.0; const CGFloat MGLUserLocationAnnotationHaloSize = 115.0; const CGFloat MGLUserLocationAnnotationPuckSize = 45.0; -const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuckSize * 0.6; +const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuckSize * 0.5; const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01; @@ -175,12 +175,16 @@ const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01; _puckArrow = [CAShapeLayer layer]; _puckArrow.path = [[self puckArrow] CGPath]; _puckArrow.fillColor = [self.mapView.tintColor CGColor]; - _puckArrow.bounds = CGRectMake(0, 0, MGLUserLocationAnnotationArrowSize, MGLUserLocationAnnotationArrowSize); - _puckArrow.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0); + _puckArrow.bounds = CGRectMake(0, 0, round(MGLUserLocationAnnotationArrowSize), round(MGLUserLocationAnnotationArrowSize)); + _puckArrow.position = CGPointMake(CGRectGetMidX(super.bounds), CGRectGetMidY(super.bounds)); _puckArrow.shouldRasterize = YES; _puckArrow.rasterizationScale = [UIScreen mainScreen].scale; _puckArrow.drawsAsynchronously = YES; + _puckArrow.lineJoin = @"round"; + _puckArrow.lineWidth = 1.f; + _puckArrow.strokeColor = _puckArrow.fillColor; + [self.layer addSublayer:_puckArrow]; } if (self.userLocation.location.course >= 0) @@ -302,7 +306,7 @@ const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01; [CATransaction setDisableActions:shouldDisableActions]; _accuracyRingLayer.bounds = CGRectMake(0, 0, accuracyRingSize, accuracyRingSize); - _accuracyRingLayer.cornerRadius = accuracyRingSize / 2; + _accuracyRingLayer.cornerRadius = accuracyRingSize / 2.0; // match the halo to the accuracy ring _haloLayer.bounds = _accuracyRingLayer.bounds; @@ -431,9 +435,11 @@ const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01; - (CALayer *)circleLayerWithSize:(CGFloat)layerSize { + layerSize = round(layerSize); + CALayer *circleLayer = [CALayer layer]; circleLayer.bounds = CGRectMake(0, 0, layerSize, layerSize); - circleLayer.position = CGPointMake(super.bounds.size.width / 2.0, super.bounds.size.height / 2.0); + circleLayer.position = CGPointMake(CGRectGetMidX(super.bounds), CGRectGetMidY(super.bounds)); circleLayer.cornerRadius = layerSize / 2.0; circleLayer.shouldRasterize = YES; circleLayer.rasterizationScale = [UIScreen mainScreen].scale; @@ -456,7 +462,7 @@ const CGFloat MGLUserLocationHeadingUpdateThreshold = 0.01; - (CGFloat)calculateAccuracyRingSize { // diameter in screen points - return self.userLocation.location.horizontalAccuracy / [self.mapView metersPerPointAtLatitude:self.userLocation.coordinate.latitude] * 2.0; + return round(self.userLocation.location.horizontalAccuracy / [self.mapView metersPerPointAtLatitude:self.userLocation.coordinate.latitude] * 2.0); } @end diff --git a/platform/ios/src/MGLMapAccessibilityElement.h b/platform/ios/src/MGLMapAccessibilityElement.h new file mode 100644 index 0000000000..952f6cbf2f --- /dev/null +++ b/platform/ios/src/MGLMapAccessibilityElement.h @@ -0,0 +1,54 @@ +#import <UIKit/UIKit.h> + +#import "MGLFoundation.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol MGLFeature; + +/// Unique identifier representing a single annotation in mbgl. +typedef uint32_t MGLAnnotationTag; + +/** An accessibility element representing something that appears on the map. */ +MGL_EXPORT +@interface MGLMapAccessibilityElement : UIAccessibilityElement + +@end + +/** An accessibility element representing a map annotation. */ +@interface MGLAnnotationAccessibilityElement : MGLMapAccessibilityElement + +/** The tag of the annotation represented by this element. */ +@property (nonatomic) MGLAnnotationTag tag; + +- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)identifier NS_DESIGNATED_INITIALIZER; + +@end + +/** An accessibility element representing a map feature. */ +MGL_EXPORT +@interface MGLFeatureAccessibilityElement : MGLMapAccessibilityElement + +/** The feature represented by this element. */ +@property (nonatomic, strong) id <MGLFeature> feature; + +- (instancetype)initWithAccessibilityContainer:(id)container feature:(id <MGLFeature>)feature NS_DESIGNATED_INITIALIZER; + +@end + +/** An accessibility element representing a place feature. */ +MGL_EXPORT +@interface MGLPlaceFeatureAccessibilityElement : MGLFeatureAccessibilityElement +@end + +/** An accessibility element representing a road feature. */ +MGL_EXPORT +@interface MGLRoadFeatureAccessibilityElement : MGLFeatureAccessibilityElement +@end + +/** An accessibility element representing the MGLMapView at large. */ +MGL_EXPORT +@interface MGLMapViewProxyAccessibilityElement : UIAccessibilityElement +@end + +NS_ASSUME_NONNULL_END diff --git a/platform/ios/src/MGLMapAccessibilityElement.mm b/platform/ios/src/MGLMapAccessibilityElement.mm new file mode 100644 index 0000000000..4e5f165fbf --- /dev/null +++ b/platform/ios/src/MGLMapAccessibilityElement.mm @@ -0,0 +1,199 @@ +#import "MGLMapAccessibilityElement.h" +#import "MGLDistanceFormatter.h" +#import "MGLCompassDirectionFormatter.h" +#import "MGLFeature.h" +#import "MGLVectorSource+MGLAdditions.h" + +#import "NSBundle+MGLAdditions.h" +#import "MGLGeometry_Private.h" + +@implementation MGLMapAccessibilityElement + +- (UIAccessibilityTraits)accessibilityTraits { + return super.accessibilityTraits | UIAccessibilityTraitAdjustable; +} + +- (void)accessibilityIncrement { + [self.accessibilityContainer accessibilityIncrement]; +} + +- (void)accessibilityDecrement { + [self.accessibilityContainer accessibilityDecrement]; +} + +@end + +@implementation MGLAnnotationAccessibilityElement + +- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)tag { + if (self = [super initWithAccessibilityContainer:container]) { + _tag = tag; + self.accessibilityHint = NSLocalizedStringWithDefaultValue(@"ANNOTATION_A11Y_HINT", nil, nil, @"Shows more info", @"Accessibility hint"); + } + return self; +} + +- (UIAccessibilityTraits)accessibilityTraits { + return super.accessibilityTraits | UIAccessibilityTraitButton; +} + +@end + +@implementation MGLFeatureAccessibilityElement + +- (instancetype)initWithAccessibilityContainer:(id)container feature:(id<MGLFeature>)feature { + if (self = [super initWithAccessibilityContainer:container]) { + _feature = feature; + + NSString *languageCode = [MGLVectorSource preferredMapboxStreetsLanguage]; + NSString *nameAttribute = [NSString stringWithFormat:@"name_%@", languageCode]; + NSString *name = [feature attributeForKey:nameAttribute]; + + // If a feature hasn’t been translated into the preferred language, it + // may be in the local language, which may be written in another script. + // Romanize it. + NSLocale *locale = [NSLocale localeWithLocaleIdentifier:languageCode]; + NSOrthography *orthography; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + if ([NSOrthography respondsToSelector:@selector(defaultOrthographyForLanguage:)]) { + orthography = [NSOrthography defaultOrthographyForLanguage:locale.localeIdentifier]; + } +#pragma clang diagnostic pop +#endif + if ([orthography.dominantScript isEqualToString:@"Latn"]) { + name = [name stringByApplyingTransform:NSStringTransformToLatin reverse:NO]; + } + + self.accessibilityLabel = name; + } + return self; +} + +- (UIAccessibilityTraits)accessibilityTraits { + return super.accessibilityTraits | UIAccessibilityTraitStaticText; +} + +@end + +@implementation MGLPlaceFeatureAccessibilityElement + +- (instancetype)initWithAccessibilityContainer:(id)container feature:(id<MGLFeature>)feature { + if (self = [super initWithAccessibilityContainer:container feature:feature]) { + NSDictionary *attributes = feature.attributes; + NSMutableArray *facts = [NSMutableArray array]; + + // Announce the kind of place or POI. + if (attributes[@"type"]) { + // FIXME: Unfortunately, these types aren’t a closed set that can be + // localized, since they’re based on OpenStreetMap tags. + NSString *type = [attributes[@"type"] stringByReplacingOccurrencesOfString:@"_" + withString:@" "]; + [facts addObject:type]; + } + // Announce the kind of airport, rail station, or mountain based on its + // Maki image name. + else if (attributes[@"maki"]) { + // TODO: Localize Maki image names. + [facts addObject:attributes[@"maki"]]; + } + + // Announce the peak’s elevation in the preferred units. + if (attributes[@"elevation_m"] ?: attributes[@"elevation_ft"]) { + NSLengthFormatter *formatter = [[NSLengthFormatter alloc] init]; + formatter.unitStyle = NSFormattingUnitStyleLong; + + NSNumber *elevationValue; + NSLengthFormatterUnit unit; + BOOL usesMetricSystem = ![[formatter.numberFormatter.locale objectForKey:NSLocaleMeasurementSystem] + isEqualToString:@"U.S."]; + if (usesMetricSystem) { + elevationValue = attributes[@"elevation_m"]; + unit = NSLengthFormatterUnitMeter; + } else { + elevationValue = attributes[@"elevation_ft"]; + unit = NSLengthFormatterUnitFoot; + } + [facts addObject:[formatter stringFromValue:elevationValue.doubleValue unit:unit]]; + } + + if (facts.count) { + NSString *separator = NSLocalizedStringWithDefaultValue(@"LIST_SEPARATOR", nil, nil, @", ", @"List separator"); + self.accessibilityValue = [facts componentsJoinedByString:separator]; + } + } + return self; +} + +@end + +@implementation MGLRoadFeatureAccessibilityElement + +- (instancetype)initWithAccessibilityContainer:(id)container feature:(id<MGLFeature>)feature { + if (self = [super initWithAccessibilityContainer:container feature:feature]) { + NSDictionary *attributes = feature.attributes; + NSMutableArray *facts = [NSMutableArray array]; + + // Announce the route number. + if (attributes[@"ref"]) { + // TODO: Decorate the route number with the network name based on the shield attribute. + NSString *ref = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"ROAD_REF_A11Y_FMT", nil, nil, @"Route %@", @"String format for accessibility value for road feature; {route number}"), attributes[@"ref"]]; + [facts addObject:ref]; + } + + // Announce whether the road is a one-way road. + if ([attributes[@"oneway"] isEqualToString:@"true"]) { + [facts addObject:NSLocalizedStringWithDefaultValue(@"ROAD_ONEWAY_A11Y_VALUE", nil, nil, @"One way", @"Accessibility value indicating that a road is a one-way road")]; + } + + // Announce whether the road is a divided road. + MGLPolyline *polyline; + if ([feature isKindOfClass:[MGLMultiPolylineFeature class]]) { + [facts addObject:NSLocalizedStringWithDefaultValue(@"ROAD_DIVIDED_A11Y_VALUE", nil, nil, @"Divided road", @"Accessibility value indicating that a road is a divided road (dual carriageway)")]; + polyline = [(MGLMultiPolylineFeature *)feature polylines].firstObject; + } + + // Announce the road’s general direction. + if ([feature isKindOfClass:[MGLPolylineFeature class]]) { + polyline = (MGLPolylineFeature *)feature; + } + if (polyline) { + NSUInteger pointCount = polyline.pointCount; + if (pointCount) { + CLLocationCoordinate2D *coordinates = polyline.coordinates; + CLLocationDirection startDirection = MGLDirectionBetweenCoordinates(coordinates[pointCount - 1], coordinates[0]); + CLLocationDirection endDirection = MGLDirectionBetweenCoordinates(coordinates[0], coordinates[pointCount - 1]); + + MGLCompassDirectionFormatter *formatter = [[MGLCompassDirectionFormatter alloc] init]; + formatter.unitStyle = NSFormattingUnitStyleLong; + + NSString *startDirectionString = [formatter stringFromDirection:startDirection]; + NSString *endDirectionString = [formatter stringFromDirection:endDirection]; + NSString *directionString = [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"ROAD_DIRECTION_A11Y_FMT", nil, nil, @"%@ to %@", @"String format for accessibility value for road feature; {starting compass direction}, {ending compass direction}"), startDirectionString, endDirectionString]; + [facts addObject:directionString]; + } + } + + if (facts.count) { + NSString *separator = NSLocalizedStringWithDefaultValue(@"LIST_SEPARATOR", nil, nil, @", ", @"List separator"); + self.accessibilityValue = [facts componentsJoinedByString:separator]; + } + } + return self; +} + +@end + +@implementation MGLMapViewProxyAccessibilityElement + +- (instancetype)initWithAccessibilityContainer:(id)container { + if (self = [super initWithAccessibilityContainer:container]) { + self.accessibilityTraits = UIAccessibilityTraitButton; + self.accessibilityLabel = [self.accessibilityContainer accessibilityLabel]; + self.accessibilityHint = NSLocalizedStringWithDefaultValue(@"CLOSE_CALLOUT_A11Y_HINT", nil, nil, @"Returns to the map", @"Accessibility hint for closing the selected annotation’s callout view and returning to the map"); + } + return self; +} + +@end diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index d5bfdbbc9f..ccab1fad36 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -44,6 +44,7 @@ #import "MGLFoundation_Private.h" #import "MGLRendererFrontend.h" +#import "MGLVectorSource+MGLAdditions.h" #import "NSBundle+MGLAdditions.h" #import "NSDate+MGLAdditions.h" #import "NSException+MGLAdditions.h" @@ -69,6 +70,7 @@ #import "MGLAnnotationContainerView.h" #import "MGLAnnotationContainerView_Private.h" #import "MGLAttributionInfo_Private.h" +#import "MGLMapAccessibilityElement.h" #include <algorithm> #include <cstdlib> @@ -140,9 +142,6 @@ const CGFloat MGLAnnotationImagePaddingForCallout = 1; const CGSize MGLAnnotationAccessibilityElementMinimumSize = CGSizeMake(10, 10); -/// Unique identifier representing a single annotation in mbgl. -typedef uint32_t MGLAnnotationTag; - /// An indication that the requested annotation was not found or is nonexistent. enum { MGLAnnotationTagNotFound = UINT32_MAX }; @@ -165,38 +164,6 @@ mbgl::util::UnitBezier MGLUnitBezierForMediaTimingFunction(CAMediaTimingFunction return { p1[0], p1[1], p2[0], p2[1] }; } -@interface MGLAnnotationAccessibilityElement : UIAccessibilityElement - -@property (nonatomic) MGLAnnotationTag tag; - -- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)identifier NS_DESIGNATED_INITIALIZER; - -@end - -@implementation MGLAnnotationAccessibilityElement - -- (instancetype)initWithAccessibilityContainer:(id)container tag:(MGLAnnotationTag)tag -{ - if (self = [super initWithAccessibilityContainer:container]) - { - _tag = tag; - self.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitAdjustable; - } - return self; -} - -- (void)accessibilityIncrement -{ - [self.accessibilityContainer accessibilityIncrement]; -} - -- (void)accessibilityDecrement -{ - [self.accessibilityContainer accessibilityDecrement]; -} - -@end - /// Lightweight container for metadata about an annotation, including the annotation itself. class MGLAnnotationContext { public: @@ -208,32 +175,12 @@ public: NSString *viewReuseIdentifier; }; -/** An accessibility element representing the MGLMapView at large. */ -@interface MGLMapViewProxyAccessibilityElement : UIAccessibilityElement - -@end - -@implementation MGLMapViewProxyAccessibilityElement - -- (instancetype)initWithAccessibilityContainer:(id)container -{ - if (self = [super initWithAccessibilityContainer:container]) - { - self.accessibilityTraits = UIAccessibilityTraitButton; - self.accessibilityLabel = [self.accessibilityContainer accessibilityLabel]; - self.accessibilityHint = NSLocalizedStringWithDefaultValue(@"CLOSE_CALLOUT_A11Y_HINT", nil, nil, @"Returns to the map", @"Accessibility hint for closing the selected annotation’s callout view and returning to the map"); - } - return self; -} - -@end - #pragma mark - Private - @interface MGLMapView () <UIGestureRecognizerDelegate, GLKViewDelegate, CLLocationManagerDelegate, - SMCalloutViewDelegate, + MGLSMCalloutViewDelegate, MGLCalloutViewDelegate, MGLMultiPointDelegate, MGLAnnotationImageDelegate> @@ -328,6 +275,10 @@ public: BOOL _delegateHasLineWidthsForShapeAnnotations; MGLCompassDirectionFormatter *_accessibilityCompassFormatter; + NS_ARRAY_OF(id <MGLFeature>) *_visiblePlaceFeatures; + NS_ARRAY_OF(id <MGLFeature>) *_visibleRoadFeatures; + NS_MUTABLE_SET_OF(MGLFeatureAccessibilityElement *) *_featureAccessibilityElements; + BOOL _accessibilityValueAnnouncementIsPending; MGLReachability *_reachability; } @@ -437,10 +388,9 @@ public: self.accessibilityTraits = UIAccessibilityTraitAllowsDirectInteraction | UIAccessibilityTraitAdjustable; _accessibilityCompassFormatter = [[MGLCompassDirectionFormatter alloc] init]; _accessibilityCompassFormatter.unitStyle = NSFormattingUnitStyleLong; - self.backgroundColor = [UIColor clearColor]; self.clipsToBounds = YES; - + if (@available(iOS 11.0, *)) { self.accessibilityIgnoresInvertColors = YES; } // setup mbgl view _mbglView = new MBGLView(self); @@ -491,9 +441,6 @@ public: _logoView.accessibilityTraits = UIAccessibilityTraitStaticText; _logoView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"LOGO_A11Y_LABEL", nil, nil, @"Mapbox", @"Accessibility label"); _logoView.translatesAutoresizingMaskIntoConstraints = NO; -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 - if ([_logoView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _logoView.accessibilityIgnoresInvertColors = YES; } -#endif [self addSubview:_logoView]; _logoViewConstraints = [NSMutableArray array]; @@ -502,9 +449,6 @@ public: _attributionButton = [UIButton buttonWithType:UIButtonTypeInfoLight]; _attributionButton.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"INFO_A11Y_LABEL", nil, nil, @"About this map", @"Accessibility label"); _attributionButton.accessibilityHint = NSLocalizedStringWithDefaultValue(@"INFO_A11Y_HINT", nil, nil, @"Shows credits, a feedback form, and more", @"Accessibility hint"); -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 - if ([_attributionButton respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _attributionButton.accessibilityIgnoresInvertColors = YES; } -#endif [_attributionButton addTarget:self action:@selector(showAttribution:) forControlEvents:UIControlEventTouchUpInside]; _attributionButton.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_attributionButton]; @@ -521,9 +465,6 @@ public: _compassView.accessibilityLabel = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_LABEL", nil, nil, @"Compass", @"Accessibility label"); _compassView.accessibilityHint = NSLocalizedStringWithDefaultValue(@"COMPASS_A11Y_HINT", nil, nil, @"Rotates the map to face due north", @"Accessibility hint"); _compassView.translatesAutoresizingMaskIntoConstraints = NO; -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 - if ([_compassView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _compassView.accessibilityIgnoresInvertColors = YES; } -#endif [self addSubview:_compassView]; _compassViewConstraints = [NSMutableArray array]; @@ -531,9 +472,6 @@ public: // _scaleBar = [[MGLScaleBar alloc] init]; _scaleBar.translatesAutoresizingMaskIntoConstraints = NO; -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 - if ([_scaleBar respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _scaleBar.accessibilityIgnoresInvertColors = YES; } -#endif [self addSubview:_scaleBar]; _scaleBarConstraints = [NSMutableArray array]; @@ -648,9 +586,7 @@ public: _glView.contentScaleFactor = [UIScreen instancesRespondToSelector:@selector(nativeScale)] ? [[UIScreen mainScreen] nativeScale] : [[UIScreen mainScreen] scale]; _glView.layer.opaque = _opaque; _glView.delegate = self; -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 - if ([_glView respondsToSelector:@selector(accessibilityIgnoresInvertColors)]) { _glView.accessibilityIgnoresInvertColors = YES; } -#endif + [_glView bindDrawable]; [self insertSubview:_glView atIndex:0]; _glView.contentMode = UIViewContentModeCenter; @@ -1616,7 +1552,9 @@ public: id<MGLAnnotation>annotation = [self annotationForGestureRecognizer:singleTap persistingResults:YES]; if(annotation) { - [self selectAnnotation:annotation animated:YES]; + CGPoint calloutPoint = [singleTap locationInView:self]; + CGRect positionRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:calloutPoint]; + [self selectAnnotation:annotation animated:YES calloutPositioningRect:positionRect]; } else { @@ -1938,7 +1876,7 @@ public: return [self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)]; } -- (void)calloutViewClicked:(__unused SMCalloutView *)calloutView +- (void)calloutViewClicked:(__unused MGLSMCalloutView *)calloutView { if ([self.delegate respondsToSelector:@selector(mapView:tapOnCalloutForAnnotation:)]) { @@ -2008,14 +1946,14 @@ public: return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]); } - + - (CLLocationDegrees)angleBetweenPoints:(CGPoint)west east:(CGPoint)east { CGFloat slope = (west.y - east.y) / (west.x - east.x); - + CGFloat angle = atan(fabs(slope)); CLLocationDegrees degrees = MGLDegreesFromRadians(angle); - + return degrees; } @@ -2354,8 +2292,61 @@ public: - (NSString *)accessibilityValue { + NSMutableArray *facts = [NSMutableArray array]; + double zoomLevel = round(self.zoomLevel + 1); - return [NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE", nil, nil, @"Zoom %dx\n%ld annotation(s) visible", @"Map accessibility value"), (int)zoomLevel, (long)self.accessibilityAnnotationCount]; + [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_ZOOM", nil, nil, @"Zoom %dx.", @"Map accessibility value; {zoom level}"), (int)zoomLevel]]; + + NSInteger annotationCount = self.accessibilityAnnotationCount; + if (annotationCount) { + [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_ANNOTATIONS", nil, nil, @"%ld annotation(s) visible.", @"Map accessibility value; {number of visible annotations}"), (long)self.accessibilityAnnotationCount]]; + } + + NSArray *placeFeatures = self.visiblePlaceFeatures; + if (placeFeatures.count) { + NSMutableArray *placesArray = [NSMutableArray arrayWithCapacity:placeFeatures.count]; + NSMutableSet *placesSet = [NSMutableSet setWithCapacity:placeFeatures.count]; + for (id <MGLFeature> placeFeature in placeFeatures.reverseObjectEnumerator) { + NSString *name = [placeFeature attributeForKey:@"name"]; + if (![placesSet containsObject:name]) { + [placesArray addObject:name]; + [placesSet addObject:name]; + } + if (placesArray.count >= 3) { + break; + } + } + NSString *placesString = [placesArray componentsJoinedByString:NSLocalizedStringWithDefaultValue(@"LIST_SEPARATOR", nil, nil, @", ", @"List separator")]; + [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_PLACES", nil, nil, @"Places visible: %@.", @"Map accessibility value; {list of visible places}"), placesString]]; + } + + NSArray *roadFeatures = self.visibleRoadFeatures; + if (roadFeatures.count) { + [facts addObject:[NSString stringWithFormat:NSLocalizedStringWithDefaultValue(@"MAP_A11Y_VALUE_ROADS", nil, nil, @"%ld road(s) visible.", @"Map accessibility value; {number of visible roads}"), roadFeatures.count]]; + } + + NSString *value = [facts componentsJoinedByString:@" "]; + return value; +} + +- (NS_ARRAY_OF(id <MGLFeature>) *)visiblePlaceFeatures +{ + if (!_visiblePlaceFeatures) + { + NSArray *placeStyleLayerIdentifiers = [self.style.placeStyleLayers valueForKey:@"identifier"]; + _visiblePlaceFeatures = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:placeStyleLayerIdentifiers]]; + } + return _visiblePlaceFeatures; +} + +- (NS_ARRAY_OF(id <MGLFeature>) *)visibleRoadFeatures +{ + if (!_visibleRoadFeatures) + { + NSArray *roadStyleLayerIdentifiers = [self.style.roadStyleLayers valueForKey:@"identifier"]; + _visibleRoadFeatures = [self visibleFeaturesInRect:self.bounds inStyleLayersWithIdentifiers:[NSSet setWithArray:roadStyleLayerIdentifiers]]; + } + return _visibleRoadFeatures; } - (CGRect)accessibilityFrame @@ -2389,14 +2380,9 @@ public: { if (self.calloutViewForSelectedAnnotation) { - return 2 /* selectedAnnotationCalloutView, mapViewProxyAccessibilityElement */; - } - NSInteger count = self.accessibilityAnnotationCount + 2 /* compass, attributionButton */; - if (self.userLocationAnnotationView) - { - count++; + return 2 /* calloutViewForSelectedAnnotation, mapViewProxyAccessibilityElement */; } - return count; + return !!self.userLocationAnnotationView + self.accessibilityAnnotationCount + self.visiblePlaceFeatures.count + self.visibleRoadFeatures.count + 2 /* compass, attributionButton */; } - (NSInteger)accessibilityAnnotationCount @@ -2421,67 +2407,123 @@ public: } return nil; } - std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds]; - - // Ornaments - if (index == 0) + + // Compass + NSUInteger compassIndex = 0; + if (index == compassIndex) { return self.compassView; } - if ( ! self.userLocationAnnotationView) - { - index++; - } - else if (index == 1) + + // User location annotation + NSRange userLocationAnnotationRange = NSMakeRange(compassIndex + 1, !!self.userLocationAnnotationView); + if (NSLocationInRange(index, userLocationAnnotationRange)) { return self.userLocationAnnotationView; } - if (index > 0 && (NSUInteger)index == visibleAnnotations.size() + 2 /* compass, userLocationAnnotationView */) - { - return self.attributionButton; - } - - std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + CGPoint centerPoint = self.contentCenter; if (self.userTrackingMode != MGLUserTrackingModeNone) { centerPoint = self.userLocationAnnotationViewCenter; } - CLLocationCoordinate2D currentCoordinate = [self convertPoint:centerPoint toCoordinateFromView:self]; - std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { - CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; - CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; - CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude, - coordinateA.longitude - currentCoordinate.longitude); - CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude, - coordinateB.longitude - currentCoordinate.longitude); - return deltaA < deltaB; - }); - - NSUInteger annotationIndex = MGLAnnotationTagNotFound; - if (index >= 0 && (NSUInteger)(index - 2) < visibleAnnotations.size()) + + // Visible annotations + std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds]; + NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size()); + if (NSLocationInRange(index, visibleAnnotationRange)) + { + std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { + CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; + CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; + CGPoint pointA = [self convertCoordinate:coordinateA toPointToView:self]; + CGPoint pointB = [self convertCoordinate:coordinateB toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return deltaA < deltaB; + }); + + NSUInteger annotationIndex = index - visibleAnnotationRange.location; + MGLAnnotationTag annotationTag = visibleAnnotations[annotationIndex]; + NSAssert(annotationTag != MGLAnnotationTagNotFound, @"Can’t get accessibility element for nonexistent or invisible annotation at index %li.", (long)index); + return [self accessibilityElementForAnnotationWithTag:annotationTag]; + } + + // Visible place features + NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures; + NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count); + if (NSLocationInRange(index, visiblePlaceFeatureRange)) + { + visiblePlaceFeatures = [visiblePlaceFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) { + CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self]; + CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return [@(deltaA) compare:@(deltaB)]; + }]; + + id <MGLFeature> feature = visiblePlaceFeatures[index - visiblePlaceFeatureRange.location]; + return [self accessibilityElementForPlaceFeature:feature]; + } + + // Visible road features + NSArray *visibleRoadFeatures = self.visibleRoadFeatures; + NSRange visibleRoadFeatureRange = NSMakeRange(NSMaxRange(visiblePlaceFeatureRange), visibleRoadFeatures.count); + if (NSLocationInRange(index, visibleRoadFeatureRange)) + { + visibleRoadFeatures = [visibleRoadFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) { + CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self]; + CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return [@(deltaA) compare:@(deltaB)]; + }]; + + id <MGLFeature> feature = visibleRoadFeatures[index - visibleRoadFeatureRange.location]; + return [self accessibilityElementForRoadFeature:feature]; + } + + // Attribution button + NSUInteger attributionButtonIndex = NSMaxRange(visibleRoadFeatureRange); + if (index == attributionButtonIndex) { - annotationIndex = index - 2 /* compass, userLocationAnnotationView */; + return self.attributionButton; } - MGLAnnotationTag annotationTag = visibleAnnotations[annotationIndex]; - NSAssert(annotationTag != MGLAnnotationTagNotFound, @"Can’t get accessibility element for nonexistent or invisible annotation at index %li.", (long)index); + + NSAssert(NO, @"Index %ld not in recognized accessibility element ranges. " + @"User location annotation range: %@; visible annotation range: %@; " + @"visible place feature range: %@; visible road feature range: %@.", + (long)index, NSStringFromRange(userLocationAnnotationRange), + NSStringFromRange(visibleAnnotationRange), NSStringFromRange(visiblePlaceFeatureRange), + NSStringFromRange(visibleRoadFeatureRange)); + return nil; +} + +/** + Returns an accessibility element corresponding to a visible annotation with the given tag. + + @param annotationTag Tag of the annotation represented by the accessibility element to return. + */ +- (id)accessibilityElementForAnnotationWithTag:(MGLAnnotationTag)annotationTag +{ NSAssert(_annotationContextsByAnnotationTag.count(annotationTag), @"Missing annotation for tag %u.", annotationTag); MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); id <MGLAnnotation> annotation = annotationContext.annotation; - + // Let the annotation view serve as its own accessibility element. MGLAnnotationView *annotationView = annotationContext.annotationView; if (annotationView && annotationView.superview) { return annotationView; } - + // Lazily create an accessibility element for the found annotation. if ( ! annotationContext.accessibilityElement) { annotationContext.accessibilityElement = [[MGLAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self tag:annotationTag]; } - + // Update the accessibility element. MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag]; CGRect annotationFrame = [self frameOfImage:annotationImage.image centeredAtCoordinate:annotation.coordinate]; @@ -2492,8 +2534,7 @@ public: annotationFrame = CGRectUnion(annotationFrame, minimumFrame); CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self); annotationContext.accessibilityElement.accessibilityFrame = screenRect; - annotationContext.accessibilityElement.accessibilityHint = NSLocalizedStringWithDefaultValue(@"ANNOTATION_A11Y_HINT", nil, nil, @"Shows more info", @"Accessibility hint"); - + if ([annotation respondsToSelector:@selector(title)]) { annotationContext.accessibilityElement.accessibilityLabel = annotation.title; @@ -2502,10 +2543,114 @@ public: { annotationContext.accessibilityElement.accessibilityValue = annotation.subtitle; } - + return annotationContext.accessibilityElement; } +/** + Returns an accessibility element corresponding to the given place feature. + + @param feature The place feature represented by the accessibility element. + */ +- (id)accessibilityElementForPlaceFeature:(id <MGLFeature>)feature +{ + if (!_featureAccessibilityElements) + { + _featureAccessibilityElements = [NSMutableSet set]; + } + + MGLFeatureAccessibilityElement *element = [_featureAccessibilityElements objectsPassingTest:^BOOL(MGLFeatureAccessibilityElement * _Nonnull element, BOOL * _Nonnull stop) { + return element.feature.identifier && ![element.feature.identifier isEqual:@0] && [element.feature.identifier isEqual:feature.identifier]; + }].anyObject; + if (!element) + { + element = [[MGLPlaceFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; + } + CGPoint center = [self convertCoordinate:feature.coordinate toPointToView:self]; + CGRect annotationFrame = CGRectInset({center, CGSizeZero}, -MGLAnnotationAccessibilityElementMinimumSize.width / 2, -MGLAnnotationAccessibilityElementMinimumSize.width / 2); + CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self); + element.accessibilityFrame = screenRect; + + [_featureAccessibilityElements addObject:element]; + + return element; +} + +/** + Returns an accessibility element corresponding to the given road feature. + + @param feature The road feature represented by the accessibility element. + */ +- (id)accessibilityElementForRoadFeature:(id <MGLFeature>)feature +{ + if (!_featureAccessibilityElements) + { + _featureAccessibilityElements = [NSMutableSet set]; + } + + MGLFeatureAccessibilityElement *element = [_featureAccessibilityElements objectsPassingTest:^BOOL(MGLFeatureAccessibilityElement * _Nonnull element, BOOL * _Nonnull stop) { + return element.feature.identifier && ![element.feature.identifier isEqual:@0] && [element.feature.identifier isEqual:feature.identifier]; + }].anyObject; + if (!element) + { + element = [[MGLRoadFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; + } + + UIBezierPath *path; + if ([feature isKindOfClass:[MGLPointFeature class]]) + { + CGPoint center = [self convertCoordinate:feature.coordinate toPointToView:self]; + CGRect annotationFrame = CGRectInset({center, CGSizeZero}, -MGLAnnotationAccessibilityElementMinimumSize.width / 2, -MGLAnnotationAccessibilityElementMinimumSize.width / 2); + CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self); + element.accessibilityFrame = screenRect; + } + else if ([feature isKindOfClass:[MGLPolylineFeature class]]) + { + path = [self pathOfPolyline:(MGLPolyline *)feature]; + } + else if ([feature isKindOfClass:[MGLMultiPolylineFeature class]]) + { + path = [UIBezierPath bezierPath]; + for (MGLPolyline *polyline in [(MGLMultiPolylineFeature *)feature polylines]) + { + [path appendPath:[self pathOfPolyline:polyline]]; + } + } + + if (path) + { + CGPathRef strokedCGPath = CGPathCreateCopyByStrokingPath(path.CGPath, NULL, MGLAnnotationAccessibilityElementMinimumSize.width, kCGLineCapButt, kCGLineJoinMiter, 0); + UIBezierPath *strokedPath = [UIBezierPath bezierPathWithCGPath:strokedCGPath]; + CGPathRelease(strokedCGPath); + UIBezierPath *screenPath = UIAccessibilityConvertPathToScreenCoordinates(strokedPath, self); + element.accessibilityPath = screenPath; + } + + [_featureAccessibilityElements addObject:element]; + + return element; +} + +- (UIBezierPath *)pathOfPolyline:(MGLPolyline *)polyline +{ + CLLocationCoordinate2D *coordinates = polyline.coordinates; + NSUInteger pointCount = polyline.pointCount; + UIBezierPath *path = [UIBezierPath bezierPath]; + for (NSUInteger i = 0; i < pointCount; i++) + { + CGPoint point = [self convertCoordinate:coordinates[i] toPointToView:self]; + if (i) + { + [path addLineToPoint:point]; + } + else + { + [path moveToPoint:point]; + } + } + return path; +} + - (NSInteger)indexOfAccessibilityElement:(id)element { if (self.calloutViewForSelectedAnnotation) @@ -2513,17 +2658,30 @@ public: return [@[self.calloutViewForSelectedAnnotation, self.mapViewProxyAccessibilityElement] indexOfObject:element]; } + + // Compass + NSUInteger compassIndex = 0; if (element == self.compassView) { - return 0; + return compassIndex; } + + // User location annotation + NSRange userLocationAnnotationRange = NSMakeRange(compassIndex + 1, !!self.userLocationAnnotationView); if (element == self.userLocationAnnotationView) { - return 1; + return userLocationAnnotationRange.location; } - + + CGPoint centerPoint = self.contentCenter; + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + centerPoint = self.userLocationAnnotationViewCenter; + } + + // Visible annotations std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds]; - + NSRange visibleAnnotationRange = NSMakeRange(NSMaxRange(userLocationAnnotationRange), visibleAnnotations.size()); MGLAnnotationTag tag = MGLAnnotationTagNotFound; if ([element isKindOfClass:[MGLAnnotationView class]]) { @@ -2534,22 +2692,92 @@ public: { tag = [(MGLAnnotationAccessibilityElement *)element tag]; } - else if (element == self.attributionButton) - { - return !!self.userLocationAnnotationView + visibleAnnotations.size(); + + if (tag != MGLAnnotationTagNotFound) + { + std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { + CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; + CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; + CGPoint pointA = [self convertCoordinate:coordinateA toPointToView:self]; + CGPoint pointB = [self convertCoordinate:coordinateB toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return deltaA < deltaB; + }); + + auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), tag); + if (foundElement == visibleAnnotations.end()) + { + return NSNotFound; + } + return visibleAnnotationRange.location + std::distance(visibleAnnotations.begin(), foundElement); } - else - { - return NSNotFound; + + // Visible place features + NSArray *visiblePlaceFeatures = self.visiblePlaceFeatures; + NSRange visiblePlaceFeatureRange = NSMakeRange(NSMaxRange(visibleAnnotationRange), visiblePlaceFeatures.count); + if ([element isKindOfClass:[MGLPlaceFeatureAccessibilityElement class]]) + { + visiblePlaceFeatures = [visiblePlaceFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) { + CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self]; + CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return [@(deltaA) compare:@(deltaB)]; + }]; + + id <MGLFeature> feature = [(MGLPlaceFeatureAccessibilityElement *)element feature]; + NSUInteger featureIndex = [visiblePlaceFeatures indexOfObject:feature]; + if (featureIndex == NSNotFound) + { + featureIndex = [visiblePlaceFeatures indexOfObjectPassingTest:^BOOL (id <MGLFeature> _Nonnull visibleFeature, NSUInteger idx, BOOL * _Nonnull stop) { + return visibleFeature.identifier && ![visibleFeature.identifier isEqual:@0] && [visibleFeature.identifier isEqual:feature.identifier]; + }]; + } + if (featureIndex == NSNotFound) + { + return NSNotFound; + } + return visiblePlaceFeatureRange.location + featureIndex; } - - std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); - auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), tag); - if (foundElement == visibleAnnotations.end()) + + // Visible road features + NSArray *visibleRoadFeatures = self.visibleRoadFeatures; + NSRange visibleRoadFeatureRange = NSMakeRange(NSMaxRange(visiblePlaceFeatureRange), visibleRoadFeatures.count); + if ([element isKindOfClass:[MGLRoadFeatureAccessibilityElement class]]) + { + visibleRoadFeatures = [visibleRoadFeatures sortedArrayUsingComparator:^NSComparisonResult(id <MGLFeature> _Nonnull featureA, id <MGLFeature> _Nonnull featureB) { + CGPoint pointA = [self convertCoordinate:featureA.coordinate toPointToView:self]; + CGPoint pointB = [self convertCoordinate:featureB.coordinate toPointToView:self]; + CGFloat deltaA = hypot(pointA.x - centerPoint.x, pointA.y - centerPoint.y); + CGFloat deltaB = hypot(pointB.x - centerPoint.x, pointB.y - centerPoint.y); + return [@(deltaA) compare:@(deltaB)]; + }]; + + id <MGLFeature> feature = [(MGLRoadFeatureAccessibilityElement *)element feature]; + NSUInteger featureIndex = [visibleRoadFeatures indexOfObject:feature]; + if (featureIndex == NSNotFound) + { + featureIndex = [visibleRoadFeatures indexOfObjectPassingTest:^BOOL (id <MGLFeature> _Nonnull visibleFeature, NSUInteger idx, BOOL * _Nonnull stop) { + return visibleFeature.identifier && ![visibleFeature.identifier isEqual:@0] && [visibleFeature.identifier isEqual:feature.identifier]; + }]; + } + if (featureIndex == NSNotFound) + { + return NSNotFound; + } + return visibleRoadFeatureRange.location + featureIndex; + } + + // Attribution button + NSUInteger attributionButtonIndex = NSMaxRange(visibleRoadFeatureRange); + if (element == self.attributionButton) { - return NSNotFound; + return attributionButtonIndex; } - return !!self.userLocationAnnotationView + std::distance(visibleAnnotations.begin(), foundElement) + 1 /* compass */; + + return NSNotFound; } - (MGLMapViewProxyAccessibilityElement *)mapViewProxyAccessibilityElement @@ -2580,10 +2808,11 @@ public: { centerPoint = self.userLocationAnnotationViewCenter; } - _mbglMap->setZoom(_mbglMap->getZoom() + log2(scaleFactor), mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); + double newZoom = round(self.zoomLevel) + log2(scaleFactor); + _mbglMap->setZoom(newZoom, mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }); [self unrotateIfNeededForGesture]; - UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.accessibilityValue); + _accessibilityValueAnnouncementIsPending = YES; } #pragma mark - Geography - @@ -3266,6 +3495,12 @@ public: } std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:rect]; + std::vector<MGLAnnotationTag> shapeAnnotationTags = [self shapeAnnotationTagsInRect:rect]; + + if (shapeAnnotationTags.size()) { + annotationTags.insert(annotationTags.end(), shapeAnnotationTags.begin(), shapeAnnotationTags.end()); + } + if (annotationTags.size()) { NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:annotationTags.size()]; @@ -3382,7 +3617,7 @@ public: { annotationViewsForAnnotation[annotationValue] = annotationView; annotationView.annotation = annotation; - annotationView.center = [self convertCoordinate:annotation.coordinate toPointToView:self]; + annotationView.center = MGLPointRounded([self convertCoordinate:annotation.coordinate toPointToView:self]); [newAnnotationViews addObject:annotationView]; MGLAnnotationImage *annotationImage = self.invisibleAnnotationImage; @@ -3771,6 +4006,11 @@ public: queryRect = CGRectInset(queryRect, -MGLAnnotationImagePaddingForHitTest, -MGLAnnotationImagePaddingForHitTest); std::vector<MGLAnnotationTag> nearbyAnnotations = [self annotationTagsInRect:queryRect]; + std::vector<MGLAnnotationTag> nearbyShapeAnnotations = [self shapeAnnotationTagsInRect:queryRect]; + + if (nearbyShapeAnnotations.size()) { + nearbyAnnotations.insert(nearbyAnnotations.end(), nearbyShapeAnnotations.begin(), nearbyShapeAnnotations.end()); + } if (nearbyAnnotations.size()) { @@ -3778,54 +4018,59 @@ public: CGRect hitRect = CGRectInset({ point, CGSizeZero }, -MGLAnnotationImagePaddingForHitTest, -MGLAnnotationImagePaddingForHitTest); - + // Filter out any annotation whose image or view is unselectable or for which // hit testing fails. - auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(), - [&](const MGLAnnotationTag annotationTag) - { + auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag annotationTag) { id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag]; NSAssert(annotation, @"Unknown annotation found nearby tap"); if ( ! annotation) { return true; } - + MGLAnnotationContext annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); CGRect annotationRect; - + MGLAnnotationView *annotationView = annotationContext.annotationView; + if (annotationView) { if ( ! annotationView.enabled) { return true; } - - CGPoint calloutAnchorPoint = [self convertCoordinate:annotation.coordinate toPointToView:self]; + + CGPoint calloutAnchorPoint = MGLPointRounded([self convertCoordinate:annotation.coordinate toPointToView:self]); CGRect frame = CGRectInset({ calloutAnchorPoint, CGSizeZero }, -CGRectGetWidth(annotationView.frame) / 2, -CGRectGetHeight(annotationView.frame) / 2); annotationRect = UIEdgeInsetsInsetRect(frame, annotationView.alignmentRectInsets); } else { + if ([annotation isKindOfClass:[MGLShape class]]) + { + return false; + } + MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag]; if ( ! annotationImage.enabled) { return true; } - + MGLAnnotationImage *fallbackAnnotationImage = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName]; UIImage *fallbackImage = fallbackAnnotationImage.image; - + annotationRect = [self frameOfImage:annotationImage.image ?: fallbackImage centeredAtCoordinate:annotation.coordinate]; } - + // Filter out the annotation if the fattened finger didn’t land // within the image’s alignment rect. return !!!CGRectIntersectsRect(annotationRect, hitRect); }); - + nearbyAnnotations.resize(std::distance(nearbyAnnotations.begin(), end)); + } MGLAnnotationTag hitAnnotationTag = MGLAnnotationTagNotFound; @@ -3908,6 +4153,14 @@ public: }); } +- (std::vector<MGLAnnotationTag>)shapeAnnotationTagsInRect:(CGRect)rect +{ + return _rendererFrontend->getRenderer()->queryShapeAnnotations({ + { CGRectGetMinX(rect), CGRectGetMinY(rect) }, + { CGRectGetMaxX(rect), CGRectGetMaxY(rect) }, + }); +} + - (id <MGLAnnotation>)selectedAnnotation { if (_userLocationAnnotationIsSelected) @@ -3957,9 +4210,13 @@ public: - (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated { - if ( ! annotation) return; + CGRect positioningRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:CGPointZero]; + [self selectAnnotation:annotation animated:animated calloutPositioningRect:positioningRect]; +} - if ([annotation isKindOfClass:[MGLMultiPoint class]]) return; +- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated calloutPositioningRect:(CGRect)calloutPositioningRect +{ + if ( ! annotation) return; if (annotation == self.selectedAnnotation) return; @@ -3974,9 +4231,6 @@ public: if (annotationTag == MGLAnnotationTagNotFound) return; } - // By default attempt to use the GL annotation image frame as the positioning rect. - CGRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag]; - MGLAnnotationView *annotationView = nil; if (annotation != self.userLocation) @@ -3984,21 +4238,12 @@ public: MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); annotationView = annotationContext.annotationView; if (annotationView && annotationView.enabled) { - { - // Annotations represented by views use the view frame as the positioning rect. - positioningRect = annotationView.frame; - [annotationView.superview bringSubviewToFront:annotationView]; - [annotationView setSelected:YES animated:animated]; + // Annotations represented by views use the view frame as the positioning rect. + calloutPositioningRect = annotationView.frame; + [annotationView.superview bringSubviewToFront:annotationView]; + [annotationView setSelected:YES animated:animated]; } } - } - - // The client can request that any annotation be selected (even ones that are offscreen). - // The annotation can’t be selected if no part of it is hittable. - if ( ! CGRectIntersectsRect(positioningRect, self.bounds) && annotation != self.userLocation) - { - return; - } self.selectedAnnotation = annotation; @@ -4028,7 +4273,7 @@ public: if (_userLocationAnnotationIsSelected) { - positioningRect = [self.userLocationAnnotationView.layer.presentationLayer frame]; + calloutPositioningRect = [self.userLocationAnnotationView.layer.presentationLayer frame]; CGRect implicitAnnotationFrame = [self.userLocationAnnotationView.layer.presentationLayer frame]; CGRect explicitAnnotationFrame = self.userLocationAnnotationView.frame; @@ -4044,7 +4289,7 @@ public: if ([calloutView.leftAccessoryView isKindOfClass:[UIControl class]]) { UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(handleCalloutAccessoryTapGesture:)]; + action:@selector(handleCalloutAccessoryTapGesture:)]; [calloutView.leftAccessoryView addGestureRecognizer:calloutAccessoryTap]; } @@ -4057,7 +4302,7 @@ public: if ([calloutView.rightAccessoryView isKindOfClass:[UIControl class]]) { UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(handleCalloutAccessoryTapGesture:)]; + action:@selector(handleCalloutAccessoryTapGesture:)]; [calloutView.rightAccessoryView addGestureRecognizer:calloutAccessoryTap]; } @@ -4067,7 +4312,7 @@ public: calloutView.delegate = self; // present popup - [calloutView presentCalloutFromRect:positioningRect + [calloutView presentCalloutFromRect:calloutPositioningRect inView:self.glView constrainedToView:self.glView animated:animated]; @@ -4097,6 +4342,27 @@ public: /// Returns the rectangle that represents the annotation image of the annotation /// with the given tag. This rectangle is fitted to the image’s alignment rect /// and is appropriate for positioning a popover. +/// If a shape annotation is visible but its centroid is not, and a default point is specified, +/// the callout view is anchored to the default callout point. +- (CGRect)positioningRectForAnnotation:(id <MGLAnnotation>)annotation defaultCalloutPoint:(CGPoint)calloutPoint +{ + MGLAnnotationTag annotationTag = [self annotationTagForAnnotation:annotation]; + CGRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag]; + + // For annotations which `coordinate` falls offscreen it will use the current tap point as anchor instead. + if ( ! CGRectIntersectsRect(positioningRect, self.bounds) && annotation != self.userLocation) + { + if (!CGPointEqualToPoint(calloutPoint, CGPointZero)) { + positioningRect = CGRectMake(calloutPoint.x, calloutPoint.y, positioningRect.size.width, positioningRect.size.height); + } + } + + return positioningRect; +} + +/// Returns the rectangle that represents the annotation image of the annotation +/// with the given tag. This rectangle is fitted to the image’s alignment rect +/// and is appropriate for positioning a popover. - (CGRect)positioningRectForCalloutForAnnotationWithTag:(MGLAnnotationTag)annotationTag { id <MGLAnnotation> annotation = [self annotationWithTag:annotationTag]; @@ -4104,6 +4370,13 @@ public: { return CGRectZero; } + + if ([annotation isKindOfClass:[MGLMultiPoint class]]) { + CLLocationCoordinate2D origin = annotation.coordinate; + CGPoint originPoint = [self convertCoordinate:origin toPointToView:self]; + return CGRectMake(originPoint.x, originPoint.y, MGLAnnotationImagePaddingForHitTest, MGLAnnotationImagePaddingForHitTest); + + } UIImage *image = [self imageOfAnnotationWithTag:annotationTag].image; if ( ! image) { @@ -4125,7 +4398,7 @@ public: /// image centered at the given coordinate. - (CGRect)frameOfImage:(UIImage *)image centeredAtCoordinate:(CLLocationCoordinate2D)coordinate { - CGPoint calloutAnchorPoint = [self convertCoordinate:coordinate toPointToView:self]; + CGPoint calloutAnchorPoint = MGLPointRounded([self convertCoordinate:coordinate toPointToView:self]); CGRect frame = CGRectInset({ calloutAnchorPoint, CGSizeZero }, -image.size.width / 2, -image.size.height / 2); return UIEdgeInsetsInsetRect(frame, image.alignmentRectInsets); } @@ -4564,7 +4837,6 @@ public: if (_showsUserHeadingIndicator) { self.showsUserLocation = YES; - } [self validateUserHeadingUpdating]; } @@ -5100,12 +5372,26 @@ public: { if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) { - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + _featureAccessibilityElements = nil; + _visiblePlaceFeatures = nil; + _visibleRoadFeatures = nil; + if (_accessibilityValueAnnouncementIsPending) { + _accessibilityValueAnnouncementIsPending = NO; + [self performSelector:@selector(announceAccessibilityValue) withObject:nil afterDelay:0.1]; + } else { + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + } } [self.delegate mapView:self regionDidChangeAnimated:animated]; } } +- (void)announceAccessibilityValue +{ + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.accessibilityValue); + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); +} + - (void)mapViewWillStartLoadingMap { if (!_mbglMap) { return; @@ -5187,6 +5473,8 @@ public: if (!_mbglMap) { return; } + + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); if ([self.delegate respondsToSelector:@selector(mapViewDidFinishRenderingMap:fullyRendered:)]) { @@ -5270,7 +5558,7 @@ public: if (annotationView) { - annotationView.center = [self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self]; + annotationView.center = MGLPointRounded([self convertCoordinate:annotationContext.annotation.coordinate toPointToView:self]); } } @@ -5388,7 +5676,7 @@ public: } else { - userPoint = [self convertCoordinate:self.userLocation.coordinate toPointToView:self]; + userPoint = MGLPointRounded([self convertCoordinate:self.userLocation.coordinate toPointToView:self]); } if ( ! annotationView.superview) diff --git a/platform/ios/src/MGLScaleBar.mm b/platform/ios/src/MGLScaleBar.mm index 966555e46a..139dffdfab 100644 --- a/platform/ios/src/MGLScaleBar.mm +++ b/platform/ios/src/MGLScaleBar.mm @@ -175,10 +175,15 @@ static const CGFloat MGLFeetPerMeter = 3.28084; return [self usesMetricSystem] ? self.metersPerPoint : self.metersPerPoint * MGLFeetPerMeter; } -#pragma mark - Convenient methods +#pragma mark - Convenience methods - (BOOL)usesRightToLeftLayout { - return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.superview.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft; + // semanticContentAttribute is iOS 9+ + if ([self.superview respondsToSelector:@selector(semanticContentAttribute)]) { + return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.superview.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft; + } else { + return UIApplication.sharedApplication.userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; + } } - (BOOL)usesMetricSystem { @@ -241,7 +246,7 @@ static const CGFloat MGLFeetPerMeter = 3.28084; CGFloat alpha = maximumDistance > allowedDistance ? .0f : 1.0f; - if(self.alpha != alpha) { + if (self.alpha != alpha) { [UIView animateWithDuration:.2f delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ self.alpha = alpha; } completion:nil]; @@ -331,7 +336,7 @@ static const CGFloat MGLFeetPerMeter = 3.28084; } - (void)layoutBars { - CGFloat barWidth = (CGRectGetWidth(self.bounds) - self.borderWidth * 2.0f) / self.bars.count; + CGFloat barWidth = round((CGRectGetWidth(self.bounds) - self.borderWidth * 2.0f) / self.bars.count); NSUInteger i = 0; for (UIView *bar in self.bars) { @@ -354,11 +359,11 @@ static const CGFloat MGLFeetPerMeter = 3.28084; } - (void)layoutLabels { - CGFloat barWidth = self.bounds.size.width / self.bars.count; + CGFloat barWidth = round(self.bounds.size.width / self.bars.count); BOOL RTL = [self usesRightToLeftLayout]; NSUInteger i = RTL ? self.bars.count : 0; for (MGLScaleBarLabel *label in self.labels) { - CGFloat xPosition = barWidth * i - CGRectGetMidX(label.bounds) + self.borderWidth; + CGFloat xPosition = round(barWidth * i - CGRectGetMidX(label.bounds) + self.borderWidth); label.frame = CGRectMake(xPosition, 0, CGRectGetWidth(label.bounds), CGRectGetHeight(label.bounds)); diff --git a/platform/ios/src/MGLUserLocation.h b/platform/ios/src/MGLUserLocation.h index 91abadbcb7..4e01cf00c9 100644 --- a/platform/ios/src/MGLUserLocation.h +++ b/platform/ios/src/MGLUserLocation.h @@ -34,7 +34,8 @@ MGL_EXPORT The heading of the user location. (read-only) This property is `nil` if the user location tracking mode is not - `MGLUserTrackingModeFollowWithHeading`. + `MGLUserTrackingModeFollowWithHeading` or if + `MGLMapView.showsUserHeadingIndicator` is disabled. */ @property (nonatomic, readonly, nullable) CLHeading *heading; diff --git a/platform/ios/test/MGLMapAccessibilityElementTests.m b/platform/ios/test/MGLMapAccessibilityElementTests.m new file mode 100644 index 0000000000..5c79d85de1 --- /dev/null +++ b/platform/ios/test/MGLMapAccessibilityElementTests.m @@ -0,0 +1,87 @@ +#import <Mapbox/Mapbox.h> +#import <XCTest/XCTest.h> + +#import "../../ios/src/MGLMapAccessibilityElement.h" + +@interface MGLMapAccessibilityElementTests : XCTestCase +@end + +@implementation MGLMapAccessibilityElementTests + +- (void)testFeatureLabels { + MGLPointFeature *feature = [[MGLPointFeature alloc] init]; + feature.attributes = @{ + @"name": @"Local", + @"name_en": @"English", + @"name_es": @"Spanish", + @"name_fr": @"French", + @"name_tlh": @"Klingon", + }; + MGLFeatureAccessibilityElement *element = [[MGLFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; + XCTAssertEqualObjects(element.accessibilityLabel, @"English", @"Accessibility label should be localized."); + + feature.attributes = @{ + @"name": @"Цинциннати", + @"name_en": @"Цинциннати", + }; + element = [[MGLFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; + XCTAssertEqualObjects(element.accessibilityLabel, @"Cincinnati", @"Accessibility label should be romanized."); +} + +- (void)testPlaceFeatureValues { + MGLPointFeature *feature = [[MGLPointFeature alloc] init]; + feature.attributes = @{ + @"type": @"village_green", + }; + MGLPlaceFeatureAccessibilityElement *element = [[MGLPlaceFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; + XCTAssertEqualObjects(element.accessibilityValue, @"village green"); + + feature = [[MGLPointFeature alloc] init]; + feature.attributes = @{ + @"maki": @"cat", + }; + element = [[MGLPlaceFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; + XCTAssertEqualObjects(element.accessibilityValue, @"cat"); + + feature = [[MGLPointFeature alloc] init]; + feature.attributes = @{ + @"elevation_ft": @31337, + @"elevation_m": @1337, + }; + element = [[MGLPlaceFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; + XCTAssertEqualObjects(element.accessibilityValue, @"31,337 feet"); +} + +- (void)testRoadFeatureValues { + CLLocationCoordinate2D coordinates[] = { + CLLocationCoordinate2DMake(0, 0), + CLLocationCoordinate2DMake(0, 1), + CLLocationCoordinate2DMake(1, 2), + CLLocationCoordinate2DMake(2, 2), + }; + MGLPolylineFeature *roadFeature = [MGLPolylineFeature polylineWithCoordinates:coordinates count:sizeof(coordinates) / sizeof(coordinates[0])]; + roadFeature.attributes = @{ + @"ref": @"42", + @"oneway": @"true", + }; + MGLRoadFeatureAccessibilityElement *element = [[MGLRoadFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:roadFeature]; + XCTAssertEqualObjects(element.accessibilityValue, @"Route 42, One way, southwest to northeast"); + + CLLocationCoordinate2D opposingCoordinates[] = { + CLLocationCoordinate2DMake(2, 1), + CLLocationCoordinate2DMake(1, 0), + }; + MGLPolylineFeature *opposingRoadFeature = [MGLPolylineFeature polylineWithCoordinates:opposingCoordinates count:sizeof(opposingCoordinates) / sizeof(opposingCoordinates[0])]; + opposingRoadFeature.attributes = @{ + @"ref": @"42", + @"oneway": @"true", + }; + MGLMultiPolylineFeature *dividedRoadFeature = [MGLMultiPolylineFeature multiPolylineWithPolylines:@[roadFeature, opposingRoadFeature]]; + dividedRoadFeature.attributes = @{ + @"ref": @"42", + }; + element = [[MGLRoadFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:dividedRoadFeature]; + XCTAssertEqualObjects(element.accessibilityValue, @"Route 42, Divided road, southwest to northeast"); +} + +@end diff --git a/platform/ios/vendor/SMCalloutView b/platform/ios/vendor/SMCalloutView deleted file mode 160000 -Subproject d6ecaba377c9f963aef630faf86e3b8f8cdb88d diff --git a/platform/ios/vendor/SMCalloutView/SMCalloutView.h b/platform/ios/vendor/SMCalloutView/SMCalloutView.h new file mode 100755 index 0000000000..0b14913626 --- /dev/null +++ b/platform/ios/vendor/SMCalloutView/SMCalloutView.h @@ -0,0 +1,205 @@ +#import <UIKit/UIKit.h> +#import <QuartzCore/QuartzCore.h> + +/* + +SMCalloutView +------------- +Created by Nick Farina (nfarina@gmail.com) +Version 2.1.5 + +*/ + +/// options for which directions the callout is allowed to "point" in. +typedef NS_OPTIONS(NSUInteger, MGLSMCalloutArrowDirection) { + MGLSMCalloutArrowDirectionUp = 1 << 0, + MGLSMCalloutArrowDirectionDown = 1 << 1, + MGLSMCalloutArrowDirectionAny = MGLSMCalloutArrowDirectionUp | MGLSMCalloutArrowDirectionDown +}; + +/// options for the callout present/dismiss animation +typedef NS_ENUM(NSInteger, MGLSMCalloutAnimation) { + /// the "bounce" animation we all know and love from @c UIAlertView + MGLSMCalloutAnimationBounce, + /// a simple fade in or out + MGLSMCalloutAnimationFade, + /// grow or shrink linearly, like in the iPad Calendar app + MGLSMCalloutAnimationStretch +}; + +NS_ASSUME_NONNULL_BEGIN + +/// when delaying our popup in order to scroll content into view, you can use this amount to match the +/// animation duration of UIScrollView when using @c -setContentOffset:animated. +extern NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView; + +@protocol MGLSMCalloutViewDelegate; +@class MGLSMCalloutBackgroundView; + +// +// Callout view. +// + +// iOS 10+ expects CAAnimationDelegate to be set explicitly. +#if __IPHONE_OS_VERSION_MAX_ALLOWED < 100000 +@interface MGLSMCalloutView : UIView +#else +@interface MGLSMCalloutView : UIView <CAAnimationDelegate> +#endif + +@property (nonatomic, weak, nullable) id<MGLSMCalloutViewDelegate> delegate; +/// title/titleView relationship mimics UINavigationBar. +@property (nonatomic, copy, nullable) NSString *title; +@property (nonatomic, copy, nullable) NSString *subtitle; + +/// Left accessory view for the call out +@property (nonatomic, strong, nullable) UIView *leftAccessoryView; +/// Right accessoty view for the call out +@property (nonatomic, strong, nullable) UIView *rightAccessoryView; +/// Default @c SMCalloutArrowDirectionDown +@property (nonatomic, assign) MGLSMCalloutArrowDirection permittedArrowDirection; +/// The current arrow direction +@property (nonatomic, readonly) MGLSMCalloutArrowDirection currentArrowDirection; +/// if the @c UIView you're constraining to has portions that are overlapped by nav bar, tab bar, etc. you'll need to tell us those insets. +@property (nonatomic, assign) UIEdgeInsets constrainedInsets; +/// default is @c SMCalloutMaskedBackgroundView, or @c SMCalloutDrawnBackgroundView when using @c SMClassicCalloutView +@property (nonatomic, strong) MGLSMCalloutBackgroundView *backgroundView; + +/** + @brief Custom title view. + + @disucssion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on titleView/subtitleView if defined, so your view + may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized. + + @warning If this is set, the respective @c title property will be ignored. + */ +@property (nonatomic, strong, nullable) UIView *titleView; + +/** + @brief Custom subtitle view. + + @discussion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on subtitleView if defined, so your view + may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized. + + @warning If this is set, the respective @c subtitle property will be ignored. + */ +@property (nonatomic, strong, nullable) UIView *subtitleView; + +/// Custom "content" view that can be any width/height. If this is set, title/subtitle/titleView/subtitleView are all ignored. +@property (nonatomic, retain, nullable) UIView *contentView; + +/// Custom content view margin +@property (nonatomic, assign) UIEdgeInsets contentViewInset; + +/// calloutOffset is the offset in screen points from the top-middle of the target view, where the anchor of the callout should be shown. +@property (nonatomic, assign) CGPoint calloutOffset; + +/// default SMCalloutAnimationBounce, SMCalloutAnimationFade respectively +@property (nonatomic, assign) MGLSMCalloutAnimation presentAnimation, dismissAnimation; + +/// Returns a new instance of SMCalloutView if running on iOS 7 or better, otherwise a new instance of SMClassicCalloutView if available. ++ (MGLSMCalloutView *)platformCalloutView; + +/** + @brief Presents a callout view by adding it to "inView" and pointing at the given rect of inView's bounds. + + @discussion Constrains the callout to the bounds of the given view. Optionally scrolls the given rect into view (plus margins) + if @c -delegate is set and responds to @c -delayForRepositionWithSize. + + @param rect @c CGRect to present the view from + @param view view to 'constrain' the @c constrainedView to + @param constrainedView @c UIView to be constrainted in @c view + @param animated @c BOOL if presentation should be animated + */ +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated; + +/** + @brief Present a callout layer in the `layer` and pointing at the given rect of the `layer` bounds + + @discussion Same as the view-based presentation, but inserts the callout into a CALayer hierarchy instead. + @note Be aware that you'll have to direct your own touches to any accessory views, since CALayer doesn't relay touch events. + + @param rect @c CGRect to present the view from + @param layer layer to 'constrain' the @c constrainedLayer to + @param constrainedLayer @c CALayer to be constrained in @c layer + @param animated @c BOOL if presentation should be animated + */ +- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated; + +/** + Dismiss the callout view + + @param animated @c BOOL if dismissal should be animated + */ +- (void)dismissCalloutAnimated:(BOOL)animated; + +/// For subclassers. You can override this method to provide your own custom animation for presenting/dismissing the callout. +- (CAAnimation *)animationWithType:(MGLSMCalloutAnimation)type presenting:(BOOL)presenting; + +@end + +// +// Background view - default draws the iOS 7 system background style (translucent white with rounded arrow). +// + +/// Abstract base class +@interface MGLSMCalloutBackgroundView : UIView +/// indicates where the tip of the arrow should be drawn, as a pixel offset +@property (nonatomic, assign) CGPoint arrowPoint; +/// will be set by the callout when the callout is in a highlighted state +@property (nonatomic, assign) BOOL highlighted; +/// returns an optional layer whose contents should mask the callout view's contents (not honored by @c SMClassicCalloutView ) +@property (nonatomic, assign) CALayer *contentMask; +/// height of the callout "arrow" +@property (nonatomic, assign) CGFloat anchorHeight; +/// the smallest possible distance from the edge of our control to the "tip" of the anchor, from either left or right +@property (nonatomic, assign) CGFloat anchorMargin; +@end + +/// Default for iOS 7, this reproduces the "masked" behavior of the iOS 7-style callout view. +/// Accessories are masked by the shape of the callout (including the arrow itself). +@interface MGLSMCalloutMaskedBackgroundView : MGLSMCalloutBackgroundView +@end + +// +// Delegate methods +// + +@protocol MGLSMCalloutViewDelegate <NSObject> +@optional + +/// Controls whether the callout "highlights" when pressed. default YES. You must also respond to @c -calloutViewClicked below. +/// Not honored by @c SMClassicCalloutView. +- (BOOL)calloutViewShouldHighlight:(MGLSMCalloutView *)calloutView; + +/// Called when the callout view is clicked. Not honored by @c SMClassicCalloutView. +- (void)calloutViewClicked:(MGLSMCalloutView *)calloutView; + +/** + Called when the callout view detects that it will be outside the constrained view when it appears, + or if the target rect was already outside the constrained view. You can implement this selector + to respond to this situation by repositioning your content first in order to make everything visible. + The @c CGSize passed is the calculated offset necessary to make everything visible (plus a nice margin). + It expects you to return the amount of time you need to reposition things so the popup can be delayed. + Typically you would return @c kSMCalloutViewRepositionDelayForUIScrollView if you're repositioning by calling @c [UIScrollView @c setContentOffset:animated:]. + + @param calloutView the @c SMCalloutView to reposition + @param offset caluclated offset necessary to make everything visible + @returns @c NSTimeInterval to delay the repositioning + */ +- (NSTimeInterval)calloutView:(MGLSMCalloutView *)calloutView delayForRepositionWithSize:(CGSize)offset; + +/// Called before the callout view appears on screen, or before the appearance animation will start. +- (void)calloutViewWillAppear:(MGLSMCalloutView *)calloutView; + +/// Called after the callout view appears on screen, or after the appearance animation is complete. +- (void)calloutViewDidAppear:(MGLSMCalloutView *)calloutView; + +/// Called before the callout view is removed from the screen, or before the disappearance animation is complete. +- (void)calloutViewWillDisappear:(MGLSMCalloutView *)calloutView; + +/// Called after the callout view is removed from the screen, or after the disappearance animation is complete. +- (void)calloutViewDidDisappear:(MGLSMCalloutView *)calloutView; + +NS_ASSUME_NONNULL_END +@end diff --git a/platform/ios/vendor/SMCalloutView/SMCalloutView.m b/platform/ios/vendor/SMCalloutView/SMCalloutView.m new file mode 100755 index 0000000000..9631ca0367 --- /dev/null +++ b/platform/ios/vendor/SMCalloutView/SMCalloutView.m @@ -0,0 +1,851 @@ +#import "SMCalloutView.h" + +// +// UIView frame helpers - we do a lot of UIView frame fiddling in this class; these functions help keep things readable. +// + +@interface UIView (SMFrameAdditions) +@property (nonatomic, assign) CGPoint frameOrigin; +@property (nonatomic, assign) CGSize frameSize; +@property (nonatomic, assign) CGFloat frameX, frameY, frameWidth, frameHeight; // normal rect properties +@property (nonatomic, assign) CGFloat frameLeft, frameTop, frameRight, frameBottom; // these will stretch/shrink the rect +@end + +// +// Callout View. +// + +#define CALLOUT_DEFAULT_CONTAINER_HEIGHT 44 // height of just the main portion without arrow +#define CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT 52 // height of just the main portion without arrow (when subtitle is present) +#define CALLOUT_MIN_WIDTH 61 // minimum width of system callout +#define TITLE_HMARGIN 12 // the title/subtitle view's normal horizontal margin from the edges of our callout view or from the accessories +#define TITLE_TOP 11 // the top of the title view when no subtitle is present +#define TITLE_SUB_TOP 4 // the top of the title view when a subtitle IS present +#define TITLE_HEIGHT 21 // title height, fixed +#define SUBTITLE_TOP 28 // the top of the subtitle, when present +#define SUBTITLE_HEIGHT 15 // subtitle height, fixed +#define BETWEEN_ACCESSORIES_MARGIN 7 // margin between accessories when no title/subtitle is present +#define TOP_ANCHOR_MARGIN 13 // all the above measurements assume a bottom anchor! if we're pointing "up" we'll need to add this top margin to everything. +#define COMFORTABLE_MARGIN 10 // when we try to reposition content to be visible, we'll consider this margin around your target rect + +NSTimeInterval const kMGLSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0; + +@interface MGLSMCalloutView () +@property (nonatomic, strong) UIButton *containerView; // for masking and interaction +@property (nonatomic, strong) UILabel *titleLabel, *subtitleLabel; +@property (nonatomic, assign) MGLSMCalloutArrowDirection currentArrowDirection; +@property (nonatomic, assign) BOOL popupCancelled; +@end + +@implementation MGLSMCalloutView + ++ (MGLSMCalloutView *)platformCalloutView { + // MGL: Mapbox does not need or include the custom flavor, so this is modified to just use SMCalloutView. + return [MGLSMCalloutView new]; +} + +- (id)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.permittedArrowDirection = MGLSMCalloutArrowDirectionDown; + self.presentAnimation = MGLSMCalloutAnimationBounce; + self.dismissAnimation = MGLSMCalloutAnimationFade; + self.backgroundColor = [UIColor clearColor]; + self.containerView = [UIButton new]; + self.containerView.isAccessibilityElement = NO; + self.isAccessibilityElement = NO; + self.contentViewInset = UIEdgeInsetsMake(12, 12, 12, 12); + + [self.containerView addTarget:self action:@selector(highlightIfNecessary) forControlEvents:UIControlEventTouchDown | UIControlEventTouchDragInside]; + [self.containerView addTarget:self action:@selector(unhighlightIfNecessary) forControlEvents:UIControlEventTouchDragOutside | UIControlEventTouchCancel | UIControlEventTouchUpOutside | UIControlEventTouchUpInside]; + [self.containerView addTarget:self action:@selector(calloutClicked) forControlEvents:UIControlEventTouchUpInside]; + } + return self; +} + +- (BOOL)supportsHighlighting { + if (![self.delegate respondsToSelector:@selector(calloutViewClicked:)]) + return NO; + if ([self.delegate respondsToSelector:@selector(calloutViewShouldHighlight:)]) + return [self.delegate calloutViewShouldHighlight:self]; + return YES; +} + +- (void)highlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = YES; } +- (void)unhighlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = NO; } + +- (void)calloutClicked { + if ([self.delegate respondsToSelector:@selector(calloutViewClicked:)]) + [self.delegate calloutViewClicked:self]; +} + +- (UIView *)titleViewOrDefault { + if (self.titleView) + // if you have a custom title view defined, return that. + return self.titleView; + else { + if (!self.titleLabel) { + // create a default titleView + self.titleLabel = [UILabel new]; + self.titleLabel.frameHeight = TITLE_HEIGHT; + self.titleLabel.opaque = NO; + self.titleLabel.backgroundColor = [UIColor clearColor]; + self.titleLabel.font = [UIFont systemFontOfSize:17]; + self.titleLabel.textColor = [UIColor blackColor]; + } + return self.titleLabel; + } +} + +- (UIView *)subtitleViewOrDefault { + if (self.subtitleView) + // if you have a custom subtitle view defined, return that. + return self.subtitleView; + else { + if (!self.subtitleLabel) { + // create a default subtitleView + self.subtitleLabel = [UILabel new]; + self.subtitleLabel.frameHeight = SUBTITLE_HEIGHT; + self.subtitleLabel.opaque = NO; + self.subtitleLabel.backgroundColor = [UIColor clearColor]; + self.subtitleLabel.font = [UIFont systemFontOfSize:12]; + self.subtitleLabel.textColor = [UIColor blackColor]; + } + return self.subtitleLabel; + } +} + +- (MGLSMCalloutBackgroundView *)backgroundView { + // create our default background on first access only if it's nil, since you might have set your own background anyway. + return _backgroundView ? _backgroundView : (_backgroundView = [self defaultBackgroundView]); +} + +- (MGLSMCalloutBackgroundView *)defaultBackgroundView { + return [MGLSMCalloutMaskedBackgroundView new]; +} + +- (void)rebuildSubviews { + // remove and re-add our appropriate subviews in the appropriate order + [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; + [self.containerView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; + [self setNeedsDisplay]; + + [self addSubview:self.backgroundView]; + [self addSubview:self.containerView]; + + if (self.contentView) { + [self.containerView addSubview:self.contentView]; + } + else { + if (self.titleViewOrDefault) [self.containerView addSubview:self.titleViewOrDefault]; + if (self.subtitleViewOrDefault) [self.containerView addSubview:self.subtitleViewOrDefault]; + } + if (self.leftAccessoryView) [self.containerView addSubview:self.leftAccessoryView]; + if (self.rightAccessoryView) [self.containerView addSubview:self.rightAccessoryView]; +} + +// Accessory margins. Accessories are centered vertically when shorter +// than the callout, otherwise they grow from the upper corner. + +- (CGFloat)leftAccessoryVerticalMargin { + if (self.leftAccessoryView.frameHeight < self.calloutContainerHeight) + return roundf((self.calloutContainerHeight - self.leftAccessoryView.frameHeight) / 2); + else + return 0; +} + +- (CGFloat)leftAccessoryHorizontalMargin { + return fminf(self.leftAccessoryVerticalMargin, TITLE_HMARGIN); +} + +- (CGFloat)rightAccessoryVerticalMargin { + if (self.rightAccessoryView.frameHeight < self.calloutContainerHeight) + return roundf((self.calloutContainerHeight - self.rightAccessoryView.frameHeight) / 2); + else + return 0; +} + +- (CGFloat)rightAccessoryHorizontalMargin { + return fminf(self.rightAccessoryVerticalMargin, TITLE_HMARGIN); +} + +- (CGFloat)innerContentMarginLeft { + if (self.leftAccessoryView) + return self.leftAccessoryHorizontalMargin + self.leftAccessoryView.frameWidth + TITLE_HMARGIN; + else + return self.contentViewInset.left; +} + +- (CGFloat)innerContentMarginRight { + if (self.rightAccessoryView) + return self.rightAccessoryHorizontalMargin + self.rightAccessoryView.frameWidth + TITLE_HMARGIN; + else + return self.contentViewInset.right; +} + +- (CGFloat)calloutHeight { + return self.calloutContainerHeight + self.backgroundView.anchorHeight; +} + +- (CGFloat)calloutContainerHeight { + if (self.contentView) + return self.contentView.frameHeight + self.contentViewInset.bottom + self.contentViewInset.top; + else if (self.subtitleView || self.subtitle.length > 0) + return CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT; + else + return CALLOUT_DEFAULT_CONTAINER_HEIGHT; +} + +- (CGSize)sizeThatFits:(CGSize)size { + + // calculate how much non-negotiable space we need to reserve for margin and accessories + CGFloat margin = self.innerContentMarginLeft + self.innerContentMarginRight; + + // how much room is left for text? + CGFloat availableWidthForText = size.width - margin - 1; + + // no room for text? then we'll have to squeeze into the given size somehow. + if (availableWidthForText < 0) + availableWidthForText = 0; + + CGSize preferredTitleSize = [self.titleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, TITLE_HEIGHT)]; + CGSize preferredSubtitleSize = [self.subtitleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, SUBTITLE_HEIGHT)]; + + // total width we'd like + CGFloat preferredWidth; + + if (self.contentView) { + + // if we have a content view, then take our preferred size directly from that + preferredWidth = self.contentView.frameWidth + margin; + } + else if (preferredTitleSize.width >= 0.000001 || preferredSubtitleSize.width >= 0.000001) { + + // if we have a title or subtitle, then our assumed margins are valid, and we can apply them + preferredWidth = fmaxf(preferredTitleSize.width, preferredSubtitleSize.width) + margin; + } + else { + // ok we have no title or subtitle to speak of. In this case, the system callout would actually not display + // at all! But we can handle it. + preferredWidth = self.leftAccessoryView.frameWidth + self.rightAccessoryView.frameWidth + self.leftAccessoryHorizontalMargin + self.rightAccessoryHorizontalMargin; + + if (self.leftAccessoryView && self.rightAccessoryView) + preferredWidth += BETWEEN_ACCESSORIES_MARGIN; + } + + // ensure we're big enough to fit our graphics! + preferredWidth = fmaxf(preferredWidth, CALLOUT_MIN_WIDTH); + + // ask to be smaller if we have space, otherwise we'll fit into what we have by truncating the title/subtitle. + return CGSizeMake(fminf(preferredWidth, size.width), self.calloutHeight); +} + +- (CGSize)offsetToContainRect:(CGRect)innerRect inRect:(CGRect)outerRect { + CGFloat nudgeRight = fmaxf(0, CGRectGetMinX(outerRect) - CGRectGetMinX(innerRect)); + CGFloat nudgeLeft = fminf(0, CGRectGetMaxX(outerRect) - CGRectGetMaxX(innerRect)); + CGFloat nudgeTop = fmaxf(0, CGRectGetMinY(outerRect) - CGRectGetMinY(innerRect)); + CGFloat nudgeBottom = fminf(0, CGRectGetMaxY(outerRect) - CGRectGetMaxY(innerRect)); + return CGSizeMake(nudgeLeft ? nudgeLeft : nudgeRight, nudgeTop ? nudgeTop : nudgeBottom); +} + +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated { + [self presentCalloutFromRect:rect inLayer:view.layer ofView:view constrainedToLayer:constrainedView.layer animated:animated]; +} + +- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated { + [self presentCalloutFromRect:rect inLayer:layer ofView:nil constrainedToLayer:constrainedLayer animated:animated]; +} + +// this private method handles both CALayer and UIView parents depending on what's passed. +- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer ofView:(UIView *)view constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated { + + // Sanity check: dismiss this callout immediately if it's displayed somewhere + if (self.layer.superlayer) [self dismissCalloutAnimated:NO]; + + // cancel all animations that may be in progress + [self.layer removeAnimationForKey:@"present"]; + [self.layer removeAnimationForKey:@"dismiss"]; + + // figure out the constrained view's rect in our popup view's coordinate system + CGRect constrainedRect = [constrainedLayer convertRect:constrainedLayer.bounds toLayer:layer]; + + // apply our edge constraints + constrainedRect = UIEdgeInsetsInsetRect(constrainedRect, self.constrainedInsets); + + constrainedRect = CGRectInset(constrainedRect, COMFORTABLE_MARGIN, COMFORTABLE_MARGIN); + + // form our subviews based on our content set so far + [self rebuildSubviews]; + + // apply title/subtitle (if present + self.titleLabel.text = self.title; + self.subtitleLabel.text = self.subtitle; + + // size the callout to fit the width constraint as best as possible + self.frameSize = [self sizeThatFits:CGSizeMake(constrainedRect.size.width, self.calloutHeight)]; + + // how much room do we have in the constraint box, both above and below our target rect? + CGFloat topSpace = CGRectGetMinY(rect) - CGRectGetMinY(constrainedRect); + CGFloat bottomSpace = CGRectGetMaxY(constrainedRect) - CGRectGetMaxY(rect); + + // we prefer to point our arrow down. + MGLSMCalloutArrowDirection bestDirection = MGLSMCalloutArrowDirectionDown; + + // we'll point it up though if that's the only option you gave us. + if (self.permittedArrowDirection == MGLSMCalloutArrowDirectionUp) + bestDirection = MGLSMCalloutArrowDirectionUp; + + // or, if we don't have enough space on the top and have more space on the bottom, and you + // gave us a choice, then pointing up is the better option. + if (self.permittedArrowDirection == MGLSMCalloutArrowDirectionAny && topSpace < self.calloutHeight && bottomSpace > topSpace) + bestDirection = MGLSMCalloutArrowDirectionUp; + + self.currentArrowDirection = bestDirection; + + // we want to point directly at the horizontal center of the given rect. calculate our "anchor point" in terms of our + // target view's coordinate system. make sure to offset the anchor point as requested if necessary. + CGFloat anchorX = self.calloutOffset.x + CGRectGetMidX(rect); + CGFloat anchorY = self.calloutOffset.y + (bestDirection == MGLSMCalloutArrowDirectionDown ? CGRectGetMinY(rect) : CGRectGetMaxY(rect)); + + // we prefer to sit centered directly above our anchor + CGFloat calloutX = roundf(anchorX - self.frameWidth / 2); + + // but not if it's going to get too close to the edge of our constraints + if (calloutX < constrainedRect.origin.x) + calloutX = constrainedRect.origin.x; + + if (calloutX > constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth) + calloutX = constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth; + + // what's the farthest to the left and right that we could point to, given our background image constraints? + CGFloat minPointX = calloutX + self.backgroundView.anchorMargin; + CGFloat maxPointX = calloutX + self.frameWidth - self.backgroundView.anchorMargin; + + // we may need to scoot over to the left or right to point at the correct spot + CGFloat adjustX = 0; + if (anchorX < minPointX) adjustX = anchorX - minPointX; + if (anchorX > maxPointX) adjustX = anchorX - maxPointX; + + // add the callout to the given layer (or view if possible, to receive touch events) + if (view) + [view addSubview:self]; + else + [layer addSublayer:self.layer]; + + CGPoint calloutOrigin = { + .x = calloutX + adjustX, + .y = bestDirection == MGLSMCalloutArrowDirectionDown ? (anchorY - self.calloutHeight) : anchorY + }; + + self.frameOrigin = calloutOrigin; + + // now set the *actual* anchor point for our layer so that our "popup" animation starts from this point. + CGPoint anchorPoint = [layer convertPoint:CGPointMake(anchorX, anchorY) toLayer:self.layer]; + + // pass on the anchor point to our background view so it knows where to draw the arrow + self.backgroundView.arrowPoint = anchorPoint; + + // adjust it to unit coordinates for the actual layer.anchorPoint property + anchorPoint.x /= self.frameWidth; + anchorPoint.y /= self.frameHeight; + self.layer.anchorPoint = anchorPoint; + + // setting the anchor point moves the view a bit, so we need to reset + self.frameOrigin = calloutOrigin; + + // make sure our frame is not on half-pixels or else we may be blurry! + CGFloat scale = [UIScreen mainScreen].scale; + self.frameX = floorf(self.frameX*scale)/scale; + self.frameY = floorf(self.frameY*scale)/scale; + + // layout now so we can immediately start animating to the final position if needed + [self setNeedsLayout]; + [self layoutIfNeeded]; + + // if we're outside the bounds of our constraint rect, we'll give our delegate an opportunity to shift us into position. + // consider both our size and the size of our target rect (which we'll assume to be the size of the content you want to scroll into view. + CGRect contentRect = CGRectUnion(self.frame, rect); + CGSize offset = [self offsetToContainRect:contentRect inRect:constrainedRect]; + + NSTimeInterval delay = 0; + self.popupCancelled = NO; // reset this before calling our delegate below + + if ([self.delegate respondsToSelector:@selector(calloutView:delayForRepositionWithSize:)] && !CGSizeEqualToSize(offset, CGSizeZero)) + delay = [self.delegate calloutView:(id)self delayForRepositionWithSize:offset]; + + // there's a chance that user code in the delegate method may have called -dismissCalloutAnimated to cancel things; if that + // happened then we need to bail! + if (self.popupCancelled) return; + + // now we want to mask our contents to our background view (if requested) to match the iOS 7 style + self.containerView.layer.mask = self.backgroundView.contentMask; + + // if we need to delay, we don't want to be visible while we're delaying, so hide us in preparation for our popup + self.hidden = YES; + + // create the appropriate animation, even if we're not animated + CAAnimation *animation = [self animationWithType:self.presentAnimation presenting:YES]; + + // nuke the duration if no animation requested - we'll still need to "run" the animation to get delays and callbacks + if (!animated) + animation.duration = 0.0000001; // can't be zero or the animation won't "run" + + animation.beginTime = CACurrentMediaTime() + delay; + animation.delegate = self; + + [self.layer addAnimation:animation forKey:@"present"]; +} + +- (void)animationDidStart:(CAAnimation *)anim { + BOOL presenting = [[anim valueForKey:@"presenting"] boolValue]; + + if (presenting) { + if ([_delegate respondsToSelector:@selector(calloutViewWillAppear:)]) + [_delegate calloutViewWillAppear:(id)self]; + + // ok, animation is on, let's make ourselves visible! + self.hidden = NO; + } + else if (!presenting) { + if ([_delegate respondsToSelector:@selector(calloutViewWillDisappear:)]) + [_delegate calloutViewWillDisappear:(id)self]; + } +} + +- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)finished { + BOOL presenting = [[anim valueForKey:@"presenting"] boolValue]; + + if (presenting && finished) { + if ([_delegate respondsToSelector:@selector(calloutViewDidAppear:)]) + [_delegate calloutViewDidAppear:(id)self]; + } + else if (!presenting && finished) { + + [self removeFromParent]; + [self.layer removeAnimationForKey:@"dismiss"]; + + if ([_delegate respondsToSelector:@selector(calloutViewDidDisappear:)]) + [_delegate calloutViewDidDisappear:(id)self]; + } +} + +- (void)dismissCalloutAnimated:(BOOL)animated { + + // cancel all animations that may be in progress + [self.layer removeAnimationForKey:@"present"]; + [self.layer removeAnimationForKey:@"dismiss"]; + + self.popupCancelled = YES; + + if (animated) { + CAAnimation *animation = [self animationWithType:self.dismissAnimation presenting:NO]; + animation.delegate = self; + [self.layer addAnimation:animation forKey:@"dismiss"]; + } + else { + [self removeFromParent]; + } +} + +- (void)removeFromParent { + if (self.superview) + [self removeFromSuperview]; + else { + // removing a layer from a superlayer causes an implicit fade-out animation that we wish to disable. + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + [self.layer removeFromSuperlayer]; + [CATransaction commit]; + } +} + +- (CAAnimation *)animationWithType:(MGLSMCalloutAnimation)type presenting:(BOOL)presenting { + CAAnimation *animation = nil; + + if (type == MGLSMCalloutAnimationBounce) { + + CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"]; + fade.duration = 0.23; + fade.fromValue = presenting ? @0.0 : @1.0; + fade.toValue = presenting ? @1.0 : @0.0; + fade.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + + CABasicAnimation *bounce = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; + bounce.duration = 0.23; + bounce.fromValue = presenting ? @0.7 : @1.0; + bounce.toValue = presenting ? @1.0 : @0.7; + bounce.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.59367:0.12066:0.18878:1.5814]; + + CAAnimationGroup *group = [CAAnimationGroup animation]; + group.animations = @[fade, bounce]; + group.duration = 0.23; + + animation = group; + } + else if (type == MGLSMCalloutAnimationFade) { + CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"]; + fade.duration = 1.0/3.0; + fade.fromValue = presenting ? @0.0 : @1.0; + fade.toValue = presenting ? @1.0 : @0.0; + animation = fade; + } + else if (type == MGLSMCalloutAnimationStretch) { + CABasicAnimation *stretch = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; + stretch.duration = 0.1; + stretch.fromValue = presenting ? @0.0 : @1.0; + stretch.toValue = presenting ? @1.0 : @0.0; + animation = stretch; + } + + // CAAnimation is KVC compliant, so we can store whether we're presenting for lookup in our delegate methods + [animation setValue:@(presenting) forKey:@"presenting"]; + + animation.fillMode = kCAFillModeForwards; + animation.removedOnCompletion = NO; + return animation; +} + +- (void)layoutSubviews { + + self.containerView.frame = self.bounds; + self.backgroundView.frame = self.bounds; + + // if we're pointing up, we'll need to push almost everything down a bit + CGFloat dy = self.currentArrowDirection == MGLSMCalloutArrowDirectionUp ? TOP_ANCHOR_MARGIN : 0; + + self.titleViewOrDefault.frameX = self.innerContentMarginLeft; + self.titleViewOrDefault.frameY = (self.subtitleView || self.subtitle.length ? TITLE_SUB_TOP : TITLE_TOP) + dy; + self.titleViewOrDefault.frameWidth = self.frameWidth - self.innerContentMarginLeft - self.innerContentMarginRight; + + self.subtitleViewOrDefault.frameX = self.titleViewOrDefault.frameX; + self.subtitleViewOrDefault.frameY = SUBTITLE_TOP + dy; + self.subtitleViewOrDefault.frameWidth = self.titleViewOrDefault.frameWidth; + + self.leftAccessoryView.frameX = self.leftAccessoryHorizontalMargin; + self.leftAccessoryView.frameY = self.leftAccessoryVerticalMargin + dy; + + self.rightAccessoryView.frameX = self.frameWidth - self.rightAccessoryHorizontalMargin - self.rightAccessoryView.frameWidth; + self.rightAccessoryView.frameY = self.rightAccessoryVerticalMargin + dy; + + if (self.contentView) { + self.contentView.frameX = self.innerContentMarginLeft; + self.contentView.frameY = self.contentViewInset.top + dy; + } +} + +#pragma mark - Accessibility + +- (NSInteger)accessibilityElementCount { + return (!!self.leftAccessoryView + !!self.titleViewOrDefault + + !!self.subtitleViewOrDefault + !!self.rightAccessoryView); +} + +- (id)accessibilityElementAtIndex:(NSInteger)index { + if (index == 0) { + return self.leftAccessoryView ? self.leftAccessoryView : self.titleViewOrDefault; + } + if (index == 1) { + return self.leftAccessoryView ? self.titleViewOrDefault : self.subtitleViewOrDefault; + } + if (index == 2) { + return self.leftAccessoryView ? self.subtitleViewOrDefault : self.rightAccessoryView; + } + if (index == 3) { + return self.leftAccessoryView ? self.rightAccessoryView : nil; + } + return nil; +} + +- (NSInteger)indexOfAccessibilityElement:(id)element { + if (element == nil) return NSNotFound; + if (element == self.leftAccessoryView) return 0; + if (element == self.titleViewOrDefault) { + return self.leftAccessoryView ? 1 : 0; + } + if (element == self.subtitleViewOrDefault) { + return self.leftAccessoryView ? 2 : 1; + } + if (element == self.rightAccessoryView) { + return self.leftAccessoryView ? 3 : 2; + } + return NSNotFound; +} + +@end + +// import this known "private API" from SMCalloutBackgroundView +@interface MGLSMCalloutBackgroundView (EmbeddedImages) ++ (UIImage *)embeddedImageNamed:(NSString *)name; +@end + +// +// Callout Background View. +// + +@interface MGLSMCalloutMaskedBackgroundView () +@property (nonatomic, strong) UIView *containerView, *containerBorderView, *arrowView; +@property (nonatomic, strong) UIImageView *arrowImageView, *arrowHighlightedImageView, *arrowBorderView; +@end + +static UIImage *blackArrowImage = nil, *whiteArrowImage = nil, *grayArrowImage = nil; + +@implementation MGLSMCalloutMaskedBackgroundView + +- (id)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + + // Here we're mimicking the very particular (and odd) structure of the system callout view. + // The hierarchy and view/layer values were discovered by inspecting map kit using Reveal.app + + self.containerView = [UIView new]; + self.containerView.backgroundColor = [UIColor whiteColor]; + self.containerView.alpha = 0.96; + self.containerView.layer.cornerRadius = 8; + self.containerView.layer.shadowRadius = 30; + self.containerView.layer.shadowOpacity = 0.1; + + self.containerBorderView = [UIView new]; + self.containerBorderView.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor; + self.containerBorderView.layer.borderWidth = 0.5; + self.containerBorderView.layer.cornerRadius = 8.5; + + if (!blackArrowImage) { + blackArrowImage = [MGLSMCalloutBackgroundView embeddedImageNamed:@"CalloutArrow"]; + whiteArrowImage = [self image:blackArrowImage withColor:[UIColor whiteColor]]; + grayArrowImage = [self image:blackArrowImage withColor:[UIColor colorWithWhite:0.85 alpha:1]]; + } + + self.anchorHeight = 13; + self.anchorMargin = 27; + + self.arrowView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, blackArrowImage.size.width, blackArrowImage.size.height)]; + self.arrowView.alpha = 0.96; + self.arrowImageView = [[UIImageView alloc] initWithImage:whiteArrowImage]; + self.arrowHighlightedImageView = [[UIImageView alloc] initWithImage:grayArrowImage]; + self.arrowHighlightedImageView.hidden = YES; + self.arrowBorderView = [[UIImageView alloc] initWithImage:blackArrowImage]; + self.arrowBorderView.alpha = 0.1; + self.arrowBorderView.frameY = 0.5; + + [self addSubview:self.containerView]; + [self.containerView addSubview:self.containerBorderView]; + [self addSubview:self.arrowView]; + [self.arrowView addSubview:self.arrowBorderView]; + [self.arrowView addSubview:self.arrowImageView]; + [self.arrowView addSubview:self.arrowHighlightedImageView]; + } + return self; +} + +// Make sure we relayout our images when our arrow point changes! +- (void)setArrowPoint:(CGPoint)arrowPoint { + [super setArrowPoint:arrowPoint]; + [self setNeedsLayout]; +} + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + self.containerView.backgroundColor = highlighted ? [UIColor colorWithWhite:0.85 alpha:1] : [UIColor whiteColor]; + self.arrowImageView.hidden = highlighted; + self.arrowHighlightedImageView.hidden = !highlighted; +} + +- (UIImage *)image:(UIImage *)image withColor:(UIColor *)color { + + UIGraphicsBeginImageContextWithOptions(image.size, NO, 0); + CGRect imageRect = (CGRect){.size=image.size}; + CGContextRef c = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(c, 0, image.size.height); + CGContextScaleCTM(c, 1, -1); + CGContextClipToMask(c, imageRect, image.CGImage); + [color setFill]; + CGContextFillRect(c, imageRect); + UIImage *whiteImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return whiteImage; +} + +- (void)layoutSubviews { + + BOOL pointingUp = self.arrowPoint.y < self.frameHeight/2; + + // if we're pointing up, we'll need to push almost everything down a bit + CGFloat dy = pointingUp ? TOP_ANCHOR_MARGIN : 0; + + self.containerView.frame = CGRectMake(0, dy, self.frameWidth, self.frameHeight - self.arrowView.frameHeight + 0.5); + self.containerBorderView.frame = CGRectInset(self.containerView.bounds, -0.5, -0.5); + + self.arrowView.frameX = roundf(self.arrowPoint.x - self.arrowView.frameWidth / 2); + + if (pointingUp) { + self.arrowView.frameY = 1; + self.arrowView.transform = CGAffineTransformMakeRotation(M_PI); + } + else { + self.arrowView.frameY = self.containerView.frameHeight - 0.5; + self.arrowView.transform = CGAffineTransformIdentity; + } +} + +- (CALayer *)contentMask { + + UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0); + + [self.layer renderInContext:UIGraphicsGetCurrentContext()]; + + UIImage *maskImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + CALayer *layer = [CALayer layer]; + layer.frame = self.bounds; + layer.contents = (id)maskImage.CGImage; + return layer; +} + +@end + +@implementation MGLSMCalloutBackgroundView + ++ (NSData *)dataWithBase64EncodedString:(NSString *)string { + // + // NSData+Base64.m + // + // Version 1.0.2 + // + // Created by Nick Lockwood on 12/01/2012. + // Copyright (C) 2012 Charcoal Design + // + // Distributed under the permissive zlib License + // Get the latest version from here: + // + // https://github.com/nicklockwood/Base64 + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // + // 3. This notice may not be removed or altered from any source distribution. + // + const char lookup[] = { + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99, + 99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99, + 99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99 + }; + + NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES]; + long long inputLength = [inputData length]; + const unsigned char *inputBytes = [inputData bytes]; + + long long maxOutputLength = (inputLength / 4 + 1) * 3; + NSMutableData *outputData = [NSMutableData dataWithLength:(NSUInteger)maxOutputLength]; + unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes]; + + int accumulator = 0; + long long outputLength = 0; + unsigned char accumulated[] = {0, 0, 0, 0}; + for (long long i = 0; i < inputLength; i++) { + unsigned char decoded = lookup[inputBytes[i] & 0x7F]; + if (decoded != 99) { + accumulated[accumulator] = decoded; + if (accumulator == 3) { + outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4); + outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2); + outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3]; + } + accumulator = (accumulator + 1) % 4; + } + } + + //handle left-over data + if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4); + if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2); + if (accumulator > 2) outputLength++; + + //truncate data to match actual output length + outputData.length = (NSUInteger)outputLength; + return outputLength? outputData: nil; +} + ++ (UIImage *)embeddedImageNamed:(NSString *)name { + CGFloat screenScale = [UIScreen mainScreen].scale; + if (screenScale > 1.0) { + name = [name stringByAppendingString:@"_2x"]; + screenScale = 2.0; + } + + SEL selector = NSSelectorFromString(name); + + if (![(id)self respondsToSelector:selector]) { + NSLog(@"Could not find an embedded image. Ensure that you've added a class-level method named +%@", name); + return nil; + } + + // We need to hush the compiler here - but we know what we're doing! + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" + NSString *base64String = [(id)self performSelector:selector]; + #pragma clang diagnostic pop + + UIImage *rawImage = [UIImage imageWithData:[self dataWithBase64EncodedString:base64String]]; + return [UIImage imageWithCGImage:rawImage.CGImage scale:screenScale orientation:UIImageOrientationUp]; +} + ++ (NSString *)CalloutArrow { return @"iVBORw0KGgoAAAANSUhEUgAAACcAAAANCAYAAAAqlHdlAAAAHGlET1QAAAACAAAAAAAAAAcAAAAoAAAABwAAAAYAAADJEgYpIwAAAJVJREFUOBFiYIAAdn5+fkFOTkE5Dg5eW05O3lJOTr6zQPyfDhhoD28pxF5BOZA7gE5ih7oLN8XJyR8MdNwrGjkQaC5/MG7biZDh4OBXBDruLpUdeBdkLhHWE1bCzs6nAnTcUyo58DnIPMK2kqAC6DALIP5JoQNB+i1IsJZ4pcBEm0iJ40D6ibeNDJVAx00k04ETSbUOAAAA//+SwicfAAAAe0lEQVRjYCAdMHNy8u7l5OT7Tzzm3Qu0hpl0q8jQwcPDIwp02B0iHXeHl5dXhAxryNfCzc2tC3TcJwIO/ARSR74tFOjk4uL1BzruHw4H/gPJU2A85Vq5uPjTgY77g+bAPyBxyk2nggkcHPxOnJz8B4AOfAGiQXwqGMsAACGK1kPPMHNBAAAAAElFTkSuQmCC"; } + ++ (NSString *)CalloutArrow_2x { return @"iVBORw0KGgoAAAANSUhEUgAAAE4AAAAaCAYAAAAZtWr8AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAHGlET1QAAAACAAAAAAAAAA0AAAAoAAAADQAAAA0AAAFMRh0LGwAAARhJREFUWAnclbENwjAQRZ0mih2fDYgsQEVDxQZMgKjpWYAJkBANI8AGDIEoM0WkzBDRAf8klB44g0OkU1zE3/+9RIpS7VVY730/y/woTWlsjJ9iPcN9pbXfY85auyvm/qcDNmb0e2Z+sk/ZBTthN0oVttX12mJIWeaWEFf+kbySmZQa0msu3nzaGJprTXV3BVLNDG/if7bNOTeAvFP35NGJu39GL7Abb27bFXncVQBZLgJf3jp+ebSWIxZMgrxdvPJoJ4gqHpXgV36ITR46HUGaiNMKB6YQd4lI3gV8qTBjmDhrbQFxVQTyKu4ShjJQap7nE4hrfiiv4Q6B8MLGat1bQNztB/JwZm8Rli5wujFu821xfGZgLPUAAAD//4wvm4gAAAD7SURBVOWXMQ6CMBiFgaFpi6VyBEedXJy4hMQTeBSvRDgJEySegI3EQWOivkZnqUB/k0LyL7R9L++D9G+DwP0TCZGUqCdRlYgUuY9F4JCmqQa0hgBcY7wIItFZMLZYS5l0ruAZbXhs6BIROgmhcoB7OIAHTZUTRqG3wp9xmhqc0aRPQu8YAlwxIbwCEUL6GH9wfDcLXY2HpyvvmkHf9+BcrwCuHQGvNRp9Pl6OY0PPAO42AB7WqMxLKLahpFR7gLv/AA9zPe+gtvAMCIC7WMC7CqEPtrqzmBfHyy3A1V/g1Th27GYBY0BIxrk6Ap65254/VZp30GID9JwteQEZrVMWXqGn8gAAAABJRU5ErkJggg=="; } + +@end + +// +// Our UIView frame helpers implementation +// + +@implementation UIView (SMFrameAdditions) + +- (CGPoint)frameOrigin { return self.frame.origin; } +- (void)setFrameOrigin:(CGPoint)origin { self.frame = (CGRect){ .origin=origin, .size=self.frame.size }; } + +- (CGFloat)frameX { return self.frame.origin.x; } +- (void)setFrameX:(CGFloat)x { self.frame = (CGRect){ .origin.x=x, .origin.y=self.frame.origin.y, .size=self.frame.size }; } + +- (CGFloat)frameY { return self.frame.origin.y; } +- (void)setFrameY:(CGFloat)y { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=y, .size=self.frame.size }; } + +- (CGSize)frameSize { return self.frame.size; } +- (void)setFrameSize:(CGSize)size { self.frame = (CGRect){ .origin=self.frame.origin, .size=size }; } + +- (CGFloat)frameWidth { return self.frame.size.width; } +- (void)setFrameWidth:(CGFloat)width { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=width, .size.height=self.frame.size.height }; } + +- (CGFloat)frameHeight { return self.frame.size.height; } +- (void)setFrameHeight:(CGFloat)height { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=height }; } + +- (CGFloat)frameLeft { return self.frame.origin.x; } +- (void)setFrameLeft:(CGFloat)left { self.frame = (CGRect){ .origin.x=left, .origin.y=self.frame.origin.y, .size.width=fmaxf(self.frame.origin.x+self.frame.size.width-left,0), .size.height=self.frame.size.height }; } + +- (CGFloat)frameTop { return self.frame.origin.y; } +- (void)setFrameTop:(CGFloat)top { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=top, .size.width=self.frame.size.width, .size.height=fmaxf(self.frame.origin.y+self.frame.size.height-top,0) }; } + +- (CGFloat)frameRight { return self.frame.origin.x + self.frame.size.width; } +- (void)setFrameRight:(CGFloat)right { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=fmaxf(right-self.frame.origin.x,0), .size.height=self.frame.size.height }; } + +- (CGFloat)frameBottom { return self.frame.origin.y + self.frame.size.height; } +- (void)setFrameBottom:(CGFloat)bottom { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=fmaxf(bottom-self.frame.origin.y,0) }; } + +@end diff --git a/platform/macos/CHANGELOG.md b/platform/macos/CHANGELOG.md index 31b3540a7c..cad254e670 100644 --- a/platform/macos/CHANGELOG.md +++ b/platform/macos/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog for Mapbox macOS SDK -## master +## v0.6.0 ### Networking and storage @@ -27,9 +27,15 @@ * Increased the default maximum zoom level from 20 to 22. ([#9835](https://github.com/mapbox/mapbox-gl-native/pull/9835)) * Added an `overlays` property to `MGLMapView`. ([#8617](https://github.com/mapbox/mapbox-gl-native/pull/8617)) * Added `-[MGLMapView cameraThatFitsShape:direction:edgePadding:]` 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)) +<<<<<<< HEAD +======= +* Added support selection of shape and polyline annotations.([#9984](https://github.com/mapbox/mapbox-gl-native/pull/9984)) +* Fixed an issue where a shape annotation callout was not displayed if the centroid was not visible. ([#10255](https://github.com/mapbox/mapbox-gl-native/pull/10255)) +>>>>>>> release-agua ### Other changes +* Added a Bulgarian localization. ([#10309](https://github.com/mapbox/mapbox-gl-native/pull/10309)) * Fixed an issue that could cause line label rendering glitches when the line geometry is projected to a point behind the plane of the camera. ([#9865](https://github.com/mapbox/mapbox-gl-native/pull/9865)) * Fixed an issue that could cause a crash when using `-[MGLMapView flyToCamera:completionHandler:]` and related methods with zoom levels at or near the maximum value. ([#9381](https://github.com/mapbox/mapbox-gl-native/pull/9381)) diff --git a/platform/macos/app/bg.lproj/Localizable.strings b/platform/macos/app/bg.lproj/Localizable.strings new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/platform/macos/app/bg.lproj/Localizable.strings diff --git a/platform/macos/bitrise.yml b/platform/macos/bitrise.yml index 1f2495dab2..057193967e 100644 --- a/platform/macos/bitrise.yml +++ b/platform/macos/bitrise.yml @@ -12,39 +12,9 @@ workflows: primary: steps: - script: - title: Build - inputs: - - content: |- - #!/bin/bash - set -eu -o pipefail - brew install cmake - gem install xcpretty --no-rdoc --no-ri - export BUILDTYPE=Debug - export XCPRETTY="| tee ${BITRISE_DEPLOY_DIR}/raw-xcodebuild-output.txt | xcpretty --color --report html --output ${BITRISE_DEPLOY_DIR}/xcode-test-results.html" - make run-test - - deploy-to-bitrise-io: - title: Deploy to Bitrise.io - inputs: - - deploy_path: "test/fixtures" - - notify_user_groups: none - - is_compress: 'true' - - slack: - title: Post to Slack + title: Skip Workflow inputs: - - webhook_url: "$SLACK_HOOK_URL" - - channel: "#gl-bots" - - from_username: 'Bitrise macOS' - - from_username_on_error: 'Bitrise macOS' - - message: '<${BITRISE_BUILD_URL}|Build #${BITRISE_BUILD_NUMBER}> - for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}> - by ${GIT_CLONE_COMMIT_COMMITER_NAME} - passed' - - message_on_error: '<${BITRISE_BUILD_URL}|Build #${BITRISE_BUILD_NUMBER}> - for <https://github.com/mapbox/mapbox-gl-native/compare/${BITRISE_GIT_BRANCH}|mapbox/mapbox-gl-native@${BITRISE_GIT_BRANCH}> - by ${GIT_CLONE_COMMIT_COMMITER_NAME} - failed' - - icon_url: https://bitrise-public-content-production.s3.amazonaws.com/slack/bitrise-slack-icon-128.png - - icon_url_on_error: https://bitrise-public-content-production.s3.amazonaws.com/slack/bitrise-slack-error-icon-128.png + - content: echo "This workflow is obsolete — see CircleCi." nightly-release: steps: - script: diff --git a/platform/macos/macos.xcodeproj/project.pbxproj b/platform/macos/macos.xcodeproj/project.pbxproj index 34f8860686..c839bfadd3 100644 --- a/platform/macos/macos.xcodeproj/project.pbxproj +++ b/platform/macos/macos.xcodeproj/project.pbxproj @@ -385,6 +385,12 @@ DA2207BB1DC076940002F84D /* MGLStyleValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MGLStyleValueTests.swift; sourceTree = "<group>"; }; DA2784FD1DF03060001D5B8D /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Media.xcassets; path = ../../darwin/test/Media.xcassets; sourceTree = "<group>"; }; DA2987591E1A4290002299F5 /* MGLDocumentationExampleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MGLDocumentationExampleTests.swift; path = ../../darwin/test/MGLDocumentationExampleTests.swift; sourceTree = "<group>"; }; + DA3389601FA3EAC4001EA329 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Foundation.strings"; sourceTree = "<group>"; }; + DA3389611FA3EDCE001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; }; + DA3389621FA3EDEF001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Foundation.strings; sourceTree = "<group>"; }; + DA3389631FA3EDF5001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Foundation.stringsdict; sourceTree = "<group>"; }; + DA3389641FA3EE00001EA329 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; }; + DA33896C1FA3EF51001EA329 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Foundation.stringsdict; sourceTree = "<group>"; }; DA35A2A31CC9EB1A00E826B2 /* MGLCoordinateFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLCoordinateFormatter.h; sourceTree = "<group>"; }; DA35A2A51CC9EB2700E826B2 /* MGLCoordinateFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLCoordinateFormatter.m; sourceTree = "<group>"; }; DA35A2A71CC9F41600E826B2 /* MGLCoordinateFormatterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MGLCoordinateFormatterTests.m; path = ../../darwin/test/MGLCoordinateFormatterTests.m; sourceTree = "<group>"; }; @@ -664,6 +670,8 @@ 352742791D4C235C00A1ECE6 /* Categories */ = { isa = PBXGroup; children = ( + 1FCDF1401F2A4F3600A46694 /* MGLVectorSource+MGLAdditions.h */, + 1FCDF1411F2A4F3600A46694 /* MGLVectorSource+MGLAdditions.m */, DA8F25A61D51CB270010E6B5 /* NSValue+MGLStyleAttributeAdditions.h */, DA8F25A71D51CB270010E6B5 /* NSValue+MGLStyleAttributeAdditions.mm */, ); @@ -727,6 +735,14 @@ name = "Test Helpers"; sourceTree = "<group>"; }; + DA33895E1FA3E997001EA329 /* Recovered References */ = { + isa = PBXGroup; + children = ( + 30E578141DAA7D920050F07E /* NSImage+MGLAdditions.h */, + ); + name = "Recovered References"; + sourceTree = "<group>"; + }; DA839E891CC2E3400062CAFB = { isa = PBXGroup; children = ( @@ -736,6 +752,7 @@ DAE6C31E1CC308BC00DB3429 /* Frameworks */, DAE6C3C41CC31F7800DB3429 /* Configuration */, DA839E931CC2E3400062CAFB /* Products */, + DA33895E1FA3E997001EA329 /* Recovered References */, ); sourceTree = "<group>"; }; @@ -944,8 +961,6 @@ DAD1657F1CF4CF50001FF4B9 /* Categories */ = { isa = PBXGroup; children = ( - 1FCDF1401F2A4F3600A46694 /* MGLVectorSource+MGLAdditions.h */, - 1FCDF1411F2A4F3600A46694 /* MGLVectorSource+MGLAdditions.m */, 408AA8601DAEED3300022900 /* MGLPolygon+MGLAdditions.h */, 408AA85C1DAEED3300022900 /* MGLPolygon+MGLAdditions.m */, 408AA8611DAEED3300022900 /* MGLPolyline+MGLAdditions.h */, @@ -1345,6 +1360,7 @@ fi, nl, hu, + bg, ); mainGroup = DA839E891CC2E3400062CAFB; productRefGroup = DA839E931CC2E3400062CAFB /* Products */; @@ -1573,6 +1589,7 @@ DA618B271E68926E00CB7F44 /* fi */, DAE8CCAB1E6E8B72009B5CB0 /* nl */, DA704CBE1F637531004B3F28 /* hu */, + DA3389611FA3EDCE001EA329 /* bg */, ); name = Localizable.strings; sourceTree = "<group>"; @@ -1622,6 +1639,7 @@ DA618B2A1E6892B500CB7F44 /* fi */, DAE8CCAC1E6E8B8D009B5CB0 /* nl */, DA704CBF1F637548004B3F28 /* hu */, + DA3389641FA3EE00001EA329 /* bg */, ); name = Localizable.strings; sourceTree = "<group>"; @@ -1642,6 +1660,8 @@ DACCD9C71F1F443B00BB09A1 /* fr */, DA704CBA1F6372E8004B3F28 /* ru */, DA704CC61F666385004B3F28 /* uk */, + DA3389601FA3EAC4001EA329 /* pt-BR */, + DA3389621FA3EDEF001EA329 /* bg */, ); name = Foundation.strings; sourceTree = "<group>"; @@ -1661,6 +1681,8 @@ DA618B151E6886DF00CB7F44 /* ca */, DA618B241E6891F300CB7F44 /* lt */, DACFE7971F66EA0C00630DA8 /* vi */, + DA3389631FA3EDF5001EA329 /* bg */, + DA33896C1FA3EF51001EA329 /* hu */, ); name = Foundation.stringsdict; sourceTree = "<group>"; diff --git a/platform/macos/sdk/bg.lproj/Localizable.strings b/platform/macos/sdk/bg.lproj/Localizable.strings new file mode 100644 index 0000000000..5c71390076 --- /dev/null +++ b/platform/macos/sdk/bg.lproj/Localizable.strings @@ -0,0 +1,27 @@ +/* User-friendly error description */ +"LOAD_MAP_FAILED_DESC" = "Картата не се зареди поради неизвестна грешка."; + +/* User-friendly error description */ +"LOAD_STYLE_FAILED_DESC" = "Картата не се зареди, поради незареждане на стила."; + +/* Accessibility title */ +"MAP_A11Y_TITLE" = "Mapbox"; + +/* User-friendly error description */ +"PARSE_STYLE_FAILED_DESC" = "Картата не се зареди поради повреден стил."; + +/* User-friendly error description */ +"STYLE_NOT_FOUND_DESC" = "Картата не се зареди поради неоткрит или несъвместим стил."; + +/* Label of Zoom In button */ +"ZOOM_IN_LABEL" = "+"; + +/* Tooltip of Zoom In button */ +"ZOOM_IN_TOOLTIP" = "Приближи"; + +/* Label of Zoom Out button; U+2212 MINUS SIGN */ +"ZOOM_OUT_LABEL" = "−"; + +/* Tooltip of Zoom Out button */ +"ZOOM_OUT_TOOLTIP" = "Отдалечи"; + diff --git a/platform/macos/sdk/pt-BR.lproj/Localizable.strings b/platform/macos/sdk/pt-BR.lproj/Localizable.strings index c7490ec8d8..72fa43b657 100644 --- a/platform/macos/sdk/pt-BR.lproj/Localizable.strings +++ b/platform/macos/sdk/pt-BR.lproj/Localizable.strings @@ -1,6 +1,18 @@ -/* Accessibility title */ +/* User-friendly error description */ +"LOAD_MAP_FAILED_DESC" = "Falha ao carregar mapa devido a um erro desconhecido."; + +/* User-friendly error description */ +"LOAD_STYLE_FAILED_DESC" = "Falha ao carregar mapa porque o estilo não pode ser carregado."; + +/* Accessibility title */ "MAP_A11Y_TITLE" = "Mapbox"; +/* User-friendly error description */ +"PARSE_STYLE_FAILED_DESC" = "Falha ao carregar mapa porque o estilo está corrompido."; + +/* User-friendly error description */ +"STYLE_NOT_FOUND_DESC" = "Falha ao carregar mapa porque o estilo não pode ser encontrado ou é incompatível."; + /* Label of Zoom In button */ "ZOOM_IN_LABEL" = "+"; diff --git a/platform/macos/src/MGLMapView.mm b/platform/macos/src/MGLMapView.mm index 0aa5bdc9db..8df6f4545d 100644 --- a/platform/macos/src/MGLMapView.mm +++ b/platform/macos/src/MGLMapView.mm @@ -1488,7 +1488,7 @@ public: if (hitAnnotationTag != _selectedAnnotationTag) { id <MGLAnnotation> annotation = [self annotationWithTag:hitAnnotationTag]; NSAssert(annotation, @"Cannot select nonexistent annotation with tag %u", hitAnnotationTag); - [self selectAnnotation:annotation]; + [self selectAnnotation:annotation atPoint:gesturePoint]; } } else { [self deselectAnnotation:self.selectedAnnotation]; @@ -1791,6 +1791,12 @@ public: } std::vector<MGLAnnotationTag> annotationTags = [self annotationTagsInRect:rect]; + std::vector<MGLAnnotationTag> shapeAnnotationTags = [self shapeAnnotationTagsInRect:rect]; + + if (shapeAnnotationTags.size()) { + annotationTags.insert(annotationTags.end(), shapeAnnotationTags.begin(), shapeAnnotationTags.end()); + } + if (annotationTags.size()) { NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:annotationTags.size()]; @@ -2050,13 +2056,18 @@ public: queryRect = NSInsetRect(queryRect, -MGLAnnotationImagePaddingForHitTest, -MGLAnnotationImagePaddingForHitTest); std::vector<MGLAnnotationTag> nearbyAnnotations = [self annotationTagsInRect:queryRect]; + std::vector<MGLAnnotationTag> nearbyShapeAnnotations = [self shapeAnnotationTagsInRect:queryRect]; + + if (nearbyShapeAnnotations.size()) { + nearbyAnnotations.insert(nearbyAnnotations.end(), nearbyShapeAnnotations.begin(), nearbyShapeAnnotations.end()); + } if (nearbyAnnotations.size()) { // Assume that the user is fat-fingering an annotation. NSRect hitRect = NSInsetRect({ point, NSZeroSize }, -MGLAnnotationImagePaddingForHitTest, -MGLAnnotationImagePaddingForHitTest); - + // Filter out any annotation whose image is unselectable or for which // hit testing fails. auto end = std::remove_if(nearbyAnnotations.begin(), nearbyAnnotations.end(), [&](const MGLAnnotationTag annotationTag) { @@ -2065,12 +2076,17 @@ public: if (!annotation) { return true; } - + + if ([annotation isKindOfClass:[MGLShape class]]) + { + return false; + } + MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag]; if (!annotationImage.selectable) { return true; } - + // Filter out the annotation if the fattened finger didn’t land on a // translucent or opaque pixel in the image. NSRect annotationRect = [self frameOfImage:annotationImage.image @@ -2149,6 +2165,14 @@ public: }); } +- (std::vector<MGLAnnotationTag>)shapeAnnotationTagsInRect:(NSRect)rect { + // Cocoa origin is at the lower-left corner. + return _rendererFrontend->getRenderer()->queryShapeAnnotations({ + { NSMinX(rect), NSHeight(self.bounds) - NSMaxY(rect) }, + { NSMaxX(rect), NSHeight(self.bounds) - NSMinY(rect) }, + }); +} + - (id <MGLAnnotation>)selectedAnnotation { if ( ! _annotationContextsByAnnotationTag.count(_selectedAnnotationTag) || _selectedAnnotationTag == MGLAnnotationTagNotFound) { @@ -2192,11 +2216,11 @@ public: - (void)selectAnnotation:(id <MGLAnnotation>)annotation { - // Only point annotations can be selected. - if (!annotation || [annotation isKindOfClass:[MGLMultiPoint class]]) { - return; - } + [self selectAnnotation:annotation atPoint:NSZeroPoint]; +} +- (void)selectAnnotation:(id <MGLAnnotation>)annotation atPoint:(NSPoint)gesturePoint +{ id <MGLAnnotation> selectedAnnotation = self.selectedAnnotation; if (annotation == selectedAnnotation) { return; @@ -2211,10 +2235,10 @@ public: [self addAnnotation:annotation]; } - // The annotation can’t be selected if no part of it is hittable. + // The annotation's anchor will bounce to the current click. NSRect positioningRect = [self positioningRectForCalloutForAnnotationWithTag:annotationTag]; if (NSIsEmptyRect(NSIntersectionRect(positioningRect, self.bounds))) { - return; + positioningRect = CGRectMake(gesturePoint.x, gesturePoint.y, positioningRect.size.width, positioningRect.size.height); } self.selectedAnnotation = annotation; @@ -2314,6 +2338,13 @@ public: if (!annotation) { return NSZeroRect; } + if ([annotation isKindOfClass:[MGLMultiPoint class]]) { + CLLocationCoordinate2D origin = annotation.coordinate; + CGPoint originPoint = [self convertCoordinate:origin toPointToView:self]; + return CGRectMake(originPoint.x, originPoint.y, MGLAnnotationImagePaddingForHitTest, MGLAnnotationImagePaddingForHitTest); + + } + NSImage *image = [self imageOfAnnotationWithTag:annotationTag].image; if (!image) { image = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName].image; |