From 7020f8298a9f2c000422dd67dc384c5aebcb7907 Mon Sep 17 00:00:00 2001 From: Kaushik Iska Date: Wed, 25 Sep 2019 09:20:52 -0700 Subject: [PATCH 1/3] [google_maps_flutter] Add Projection methods to google_maps This doesn't yet expose the full projection functionality but addresses the most common usecases to translate screen coordinates to latlngs. Issue: https://github.com/flutter/flutter/issues/37959 --- packages/google_maps_flutter/CHANGELOG.md | 4 + .../flutter/plugins/googlemaps/Convert.java | 22 +++++- .../googlemaps/GoogleMapController.java | 29 ++++++++ .../example/test_driver/google_maps.dart | 74 +++++++++++++++++++ .../ios/Classes/GoogleMapController.m | 35 +++++++++ .../lib/google_maps_flutter.dart | 7 +- .../lib/src/controller.dart | 21 ++++++ .../lib/src/screen_coordinate.dart | 35 +++++++++ packages/google_maps_flutter/pubspec.yaml | 2 +- 9 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 packages/google_maps_flutter/lib/src/screen_coordinate.dart diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index 3c569ed01a79..76b3e23c693c 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.21+4 + +* Support projection methods to translate between screen and latlng coordinates. + ## 0.5.21+3 * Fix `myLocationButton` bug in `google_maps_flutter` iOS. diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 76e14faaf01e..3dfd03f4fb6a 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -30,7 +30,9 @@ import java.util.List; import java.util.Map; -/** Conversions between JSON-like values and GoogleMaps data types. */ +/** + * Conversions between JSON-like values and GoogleMaps data types. + */ class Convert { private static BitmapDescriptor toBitmapDescriptor(Object o) { @@ -201,11 +203,23 @@ static Object latLngToJson(LatLng latLng) { return Arrays.asList(latLng.latitude, latLng.longitude); } - private static LatLng toLatLng(Object o) { + static LatLng toLatLng(Object o) { final List data = toList(o); return new LatLng(toDouble(data.get(0)), toDouble(data.get(1))); } + static Point toPoint(Object o) { + Map screenCoordinate = (Map) o; + return new Point(screenCoordinate.get("x"), screenCoordinate.get("y")); + } + + static Map pointToJson(Point point) { + final Map data = new HashMap<>(2); + data.put("x", point.x); + data.put("y", point.y); + return data; + } + private static LatLngBounds toLatLngBounds(Object o) { if (o == null) { return null; @@ -322,7 +336,9 @@ static void interpretGoogleMapOptions(Object o, GoogleMapOptionsSink sink) { } } - /** Returns the dartMarkerId of the interpreted marker. */ + /** + * Returns the dartMarkerId of the interpreted marker. + */ static String interpretMarkerOptions(Object o, MarkerOptionsSink sink) { final Map data = toMap(o); final Object alpha = data.get("alpha"); diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index de6a1158023d..f362b6256fa2 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -17,6 +17,7 @@ import android.app.Application; import android.content.Context; import android.content.pm.PackageManager; +import android.graphics.Point; import android.os.Bundle; import android.util.Log; import android.view.View; @@ -226,6 +227,34 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { } break; } + case "map#getScreenCoordinate": + { + if (googleMap != null) { + LatLng latLng = Convert.toLatLng(call.arguments); + Point screenLocation = googleMap.getProjection().toScreenLocation(latLng); + result.success(Convert.pointToJson(screenLocation)); + } else { + result.error( + "GoogleMap uninitialized", + "getScreenCoordinate called prior to map initialization", + null); + } + break; + } + case "map#getLatLng": + { + if (googleMap != null) { + Point point = Convert.toPoint(call.arguments); + LatLng latLng = googleMap.getProjection().fromScreenLocation(point); + result.success(Convert.latLngToJson(latLng)); + } else { + result.error( + "GoogleMap uninitialized", + "getLatLng called prior to map initialization", + null); + } + break; + } case "camera#move": { final CameraUpdate cameraUpdate = diff --git a/packages/google_maps_flutter/example/test_driver/google_maps.dart b/packages/google_maps_flutter/example/test_driver/google_maps.dart index 6f2eacd49c00..8bcf2e3f2f5d 100644 --- a/packages/google_maps_flutter/example/test_driver/google_maps.dart +++ b/packages/google_maps_flutter/example/test_driver/google_maps.dart @@ -564,4 +564,78 @@ void main() { final GoogleMapController controller = await controllerCompleter.future; await controller.setMapStyle(null); }); + + test('testGetLatLng', () async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final GoogleMapController controller = await controllerCompleter.future; + + // We suspected a bug in the iOS Google Maps SDK caused the camera is not properly positioned at + // initialization. https://github.com/flutter/flutter/issues/24806 + // This temporary workaround fix is provided while the actual fix in the Google Maps SDK is + // still being investigated. + // TODO(cyanglaz): Remove this temporary fix once the Maps SDK issue is resolved. + // https://github.com/flutter/flutter/issues/27550 + await Future.delayed(const Duration(seconds: 3)); + + final LatLngBounds visibleRegion = await controller.getVisibleRegion(); + final LatLng topLeft = + await controller.getLatLng(const ScreenCoordinate(x: 0, y: 0)); + final LatLng northWest = LatLng( + visibleRegion.northeast.latitude, + visibleRegion.southwest.longitude, + ); + + expect(topLeft, northWest); + }); + + test('testScreenCoordinate', () async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final GoogleMapController controller = await controllerCompleter.future; + + // We suspected a bug in the iOS Google Maps SDK caused the camera is not properly positioned at + // initialization. https://github.com/flutter/flutter/issues/24806 + // This temporary workaround fix is provided while the actual fix in the Google Maps SDK is + // still being investigated. + // TODO(cyanglaz): Remove this temporary fix once the Maps SDK issue is resolved. + // https://github.com/flutter/flutter/issues/27550 + await Future.delayed(const Duration(seconds: 3)); + + final LatLngBounds visibleRegion = await controller.getVisibleRegion(); + final LatLng northWest = LatLng( + visibleRegion.northeast.latitude, + visibleRegion.southwest.longitude, + ); + final ScreenCoordinate topLeft = + await controller.getScreenCoordinate(northWest); + + expect(topLeft, const ScreenCoordinate(x: 0, y: 0)); + }); } diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index 04a9f415e6fc..9a0c0f4d6179 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -8,7 +8,9 @@ #pragma mark - Conversion of JSON-like values sent via platform channels. Forward declarations. static NSDictionary* PositionToJson(GMSCameraPosition* position); +static NSDictionary* PointToJson(CGPoint point); static NSArray* LocationToJson(CLLocationCoordinate2D position); +static CGPoint ToCGPoint(NSDictionary* json); static GMSCameraPosition* ToOptionalCameraPosition(NSDictionary* json); static GMSCoordinateBounds* ToOptionalBounds(NSArray* json); static GMSCameraUpdate* ToCameraUpdate(NSArray* data); @@ -147,6 +149,26 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { message:@"getVisibleRegion called prior to map initialization" details:nil]); } + } else if ([call.method isEqualToString:@"map#getScreenCoordinate"]) { + if (_mapView != nil) { + CLLocationCoordinate2D location = [FLTGoogleMapJsonConversions toLocation:call.arguments]; + CGPoint point = [_mapView.projection pointForCoordinate:location]; + result(PointToJson(point)); + } else { + result([FlutterError errorWithCode:@"GoogleMap uninitialized" + message:@"getScreenCoordinate called prior to map initialization" + details:nil]); + } + } else if ([call.method isEqualToString:@"map#getLatLng"]) { + if (_mapView != nil && call.arguments) { + CGPoint point = ToCGPoint(call.arguments); + CLLocationCoordinate2D latlng = [_mapView.projection coordinateForPoint:point]; + result(LocationToJson(latlng)); + } else { + result([FlutterError errorWithCode:@"GoogleMap uninitialized" + message:@"getLatLng called prior to map initialization" + details:nil]); + } } else if ([call.method isEqualToString:@"map#waitForMap"]) { result(nil); } else if ([call.method isEqualToString:@"markers#update"]) { @@ -427,6 +449,13 @@ - (void)mapView:(GMSMapView*)mapView didLongPressAtCoordinate:(CLLocationCoordin }; } +static NSDictionary* PointToJson(CGPoint point) { + return @{ + @"x" : @((int) point.x), + @"y" : @((int) point.y), + }; +} + static NSDictionary* GMSCoordinateBoundsToJson(GMSCoordinateBounds* bounds) { if (!bounds) { return nil; @@ -460,6 +489,12 @@ static CLLocationCoordinate2D ToLocation(NSArray* data) { return json ? ToCameraPosition(json) : nil; } +static CGPoint ToCGPoint(NSDictionary* json) { + double x = ToDouble(json[@"x"]); + double y = ToDouble(json[@"y"]); + return CGPointMake(x, y); +} + static GMSCoordinateBounds* ToBounds(NSArray* data) { return [[GMSCoordinateBounds alloc] initWithCoordinate:ToLocation(data[0]) coordinate:ToLocation(data[1])]; diff --git a/packages/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/lib/google_maps_flutter.dart index 91f037192255..5f3889f7b2f1 100644 --- a/packages/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/lib/google_maps_flutter.dart @@ -17,17 +17,18 @@ part 'src/bitmap.dart'; part 'src/callbacks.dart'; part 'src/camera.dart'; part 'src/cap.dart'; +part 'src/circle.dart'; +part 'src/circle_updates.dart'; part 'src/controller.dart'; part 'src/google_map.dart'; part 'src/joint_type.dart'; +part 'src/location.dart'; part 'src/marker.dart'; part 'src/marker_updates.dart'; -part 'src/location.dart'; part 'src/pattern_item.dart'; part 'src/polygon.dart'; part 'src/polygon_updates.dart'; part 'src/polyline.dart'; part 'src/polyline_updates.dart'; -part 'src/circle.dart'; -part 'src/circle_updates.dart'; +part 'src/screen_coordinate.dart'; part 'src/ui.dart'; diff --git a/packages/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/lib/src/controller.dart index ec77111bae9d..4ef8956c581a 100644 --- a/packages/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/lib/src/controller.dart @@ -208,4 +208,25 @@ class GoogleMapController { return LatLngBounds(northeast: northeast, southwest: southwest); } + + /// Return [ScreenCoordinate] of the [LatLng] in the current map view. + /// + /// A projection is used to translate between on screen location and geographic coordinates. + /// Screen location is in screen pixels (not display pixels) with respect to the top left corner + /// of the map, not necessarily of the whole screen. + Future getScreenCoordinate(LatLng latLng) async { + final Map point = await channel.invokeMapMethod( + 'map#getScreenCoordinate', latLng._toJson()); + return ScreenCoordinate(x: point['x'], y: point['y']); + } + + /// Returns [LatLng] corresponding to the [ScreenCoordinate] in the current map view. + /// + /// Returned [LatLng] corresponds to a screen location. The screen location is specified in screen + /// pixels (not display pixels) relative to the top left of the map, not top left of the whole screen. + Future getLatLng(ScreenCoordinate screenCoordinate) async { + final List latLng = await channel.invokeMethod>( + 'map#getLatLng', screenCoordinate._toJson()); + return LatLng(latLng[0], latLng[1]); + } } diff --git a/packages/google_maps_flutter/lib/src/screen_coordinate.dart b/packages/google_maps_flutter/lib/src/screen_coordinate.dart new file mode 100644 index 000000000000..7924b5538454 --- /dev/null +++ b/packages/google_maps_flutter/lib/src/screen_coordinate.dart @@ -0,0 +1,35 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter; + +/// Represents a point coordinate in the [GoogleMap]'s view. +@immutable +class ScreenCoordinate { + const ScreenCoordinate({ + @required this.x, + @required this.y, + }); + + final int x; + final int y; + + dynamic _toJson() { + return { + "x": x, + "y": y, + }; + } + + @override + String toString() => '$runtimeType($x, $y)'; + + @override + bool operator ==(Object o) { + return o is ScreenCoordinate && o.x == x && o.y == y; + } + + @override + int get hashCode => hashValues(x, y); +} diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index bdc41c873909..908fb5b250e6 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter -version: 0.5.21+3 +version: 0.5.21+4 dependencies: flutter: From 172362911064a4834d44f70215a2177392051f28 Mon Sep 17 00:00:00 2001 From: Kaushik Iska Date: Wed, 25 Sep 2019 10:38:22 -0700 Subject: [PATCH 2/3] Fix format --- .../main/java/io/flutter/plugins/googlemaps/Convert.java | 8 ++------ .../flutter/plugins/googlemaps/GoogleMapController.java | 4 +--- .../google_maps_flutter/ios/Classes/GoogleMapController.m | 4 ++-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 3dfd03f4fb6a..6ca3f1ffbd73 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -30,9 +30,7 @@ import java.util.List; import java.util.Map; -/** - * Conversions between JSON-like values and GoogleMaps data types. - */ +/** Conversions between JSON-like values and GoogleMaps data types. */ class Convert { private static BitmapDescriptor toBitmapDescriptor(Object o) { @@ -336,9 +334,7 @@ static void interpretGoogleMapOptions(Object o, GoogleMapOptionsSink sink) { } } - /** - * Returns the dartMarkerId of the interpreted marker. - */ + /** Returns the dartMarkerId of the interpreted marker. */ static String interpretMarkerOptions(Object o, MarkerOptionsSink sink) { final Map data = toMap(o); final Object alpha = data.get("alpha"); diff --git a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index f362b6256fa2..f2ea28e32412 100644 --- a/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -249,9 +249,7 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { result.success(Convert.latLngToJson(latLng)); } else { result.error( - "GoogleMap uninitialized", - "getLatLng called prior to map initialization", - null); + "GoogleMap uninitialized", "getLatLng called prior to map initialization", null); } break; } diff --git a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m index 9a0c0f4d6179..70a278af45de 100644 --- a/packages/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -451,8 +451,8 @@ - (void)mapView:(GMSMapView*)mapView didLongPressAtCoordinate:(CLLocationCoordin static NSDictionary* PointToJson(CGPoint point) { return @{ - @"x" : @((int) point.x), - @"y" : @((int) point.y), + @"x" : @((int)point.x), + @"y" : @((int)point.y), }; } From 426139b2706a82088701f1412a64a28188d377e6 Mon Sep 17 00:00:00 2001 From: Kaushik Iska Date: Wed, 25 Sep 2019 11:21:35 -0700 Subject: [PATCH 3/3] better docs for ScreenCoordinate --- packages/google_maps_flutter/lib/src/screen_coordinate.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/google_maps_flutter/lib/src/screen_coordinate.dart b/packages/google_maps_flutter/lib/src/screen_coordinate.dart index 7924b5538454..83e57514c2a0 100644 --- a/packages/google_maps_flutter/lib/src/screen_coordinate.dart +++ b/packages/google_maps_flutter/lib/src/screen_coordinate.dart @@ -5,6 +5,10 @@ part of google_maps_flutter; /// Represents a point coordinate in the [GoogleMap]'s view. +/// +/// The screen location is specified in screen pixels (not display pixels) relative +/// to the top left of the map, not top left of the whole screen. (x, y) = (0, 0) +/// corresponds to top-left of the [GoogleMap] not the whole screen. @immutable class ScreenCoordinate { const ScreenCoordinate({