diff --git a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS index 493a0b4ef9c..16db024a216 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS +++ b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS @@ -64,3 +64,4 @@ Aleksandr Yurkovskiy Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> +Nguyễn Phúc Lợi diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index 42930348965..a8698d6cc1f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.4.1 +* Add "My Location" Widget. Issue [#64073](https://github.com/flutter/flutter/issues/64073) * Updates minimum Flutter version to 3.0. ## 0.4.0+5 diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart index 0226234ea97..038d1256dd5 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart @@ -25,6 +25,9 @@ const double _acceptableDelta = 0.0000000001; MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), ]) /// Test Google Map Controller @@ -35,7 +38,6 @@ void main() { const int mapId = 33930; late GoogleMapController controller; late StreamController> stream; - // Creates a controller with the default mapId and stream controller, and any `options` needed. GoogleMapController createController({ CameraPosition initialCameraPosition = @@ -485,6 +487,153 @@ void main() { expect(controller.trafficLayer, isNotNull); }); }); + + group('My Location', () { + testWidgets('by default is disabled', (WidgetTester tester) async { + controller = createController(); + controller.init(); + expect(controller.myLocationButton, isNull); + }); + + testWidgets('initializes with my location & display my location button', + (WidgetTester tester) async { + late final MockGeolocation mockGeolocation = MockGeolocation(); + late final MockGeoposition mockGeoposition = MockGeoposition(); + late final MockCoordinates mockCoordinates = MockCoordinates(); + const LatLng currentLocation = LatLng(10.8231, 106.6297); + + controller = createController( + mapConfiguration: const MapConfiguration( + myLocationEnabled: true, + myLocationButtonEnabled: true, + )); + controller.debugSetOverrides( + createMap: (_, __) => map, + markers: markers, + geolocation: mockGeolocation, + ); + + when(mockGeoposition.coords).thenReturn(mockCoordinates); + + when(mockCoordinates.longitude).thenReturn(currentLocation.longitude); + + when(mockCoordinates.latitude).thenReturn(currentLocation.latitude); + + when(mockGeolocation.getCurrentPosition( + timeout: const Duration(seconds: 30))) + .thenAnswer((_) async => mockGeoposition); + + when(mockGeolocation.watchPosition()).thenAnswer((_) { + return Stream.fromIterable( + [mockGeoposition]); + }); + + controller.init(); + + await Future.delayed(const Duration(milliseconds: 50)); + + final Set capturedMarkers = + verify(markers.addMarkers(captureAny)).captured[1] as Set; + + final gmaps.LatLng gmCenter = map.center!; + + expect(controller.myLocationButton, isNotNull); + expect(capturedMarkers.length, 1); + expect(capturedMarkers.first.position, currentLocation); + expect(capturedMarkers.first.zIndex, 0.5); + expect(gmCenter.lat, currentLocation.latitude); + expect(gmCenter.lng, currentLocation.longitude); + }); + + testWidgets('initializes with my location only', + (WidgetTester tester) async { + late final MockGeolocation mockGeolocation = MockGeolocation(); + late final MockGeoposition mockGeoposition = MockGeoposition(); + late final MockCoordinates mockCoordinates = MockCoordinates(); + const LatLng currentLocation = LatLng(10.8231, 106.6297); + + controller = createController( + mapConfiguration: const MapConfiguration( + myLocationEnabled: true, + myLocationButtonEnabled: false, + )); + controller.debugSetOverrides( + createMap: (_, __) => map, + markers: markers, + geolocation: mockGeolocation, + ); + + when(mockGeoposition.coords).thenReturn(mockCoordinates); + + when(mockCoordinates.longitude).thenReturn(currentLocation.longitude); + + when(mockCoordinates.latitude).thenReturn(currentLocation.latitude); + + when(mockGeolocation.getCurrentPosition( + timeout: const Duration(seconds: 30))) + .thenAnswer((_) async => mockGeoposition); + + when(mockGeolocation.watchPosition()).thenAnswer((_) { + return Stream.fromIterable( + [mockGeoposition]); + }); + + controller.init(); + + await Future.delayed(const Duration(milliseconds: 50)); + + final Set capturedMarkers = + verify(markers.addMarkers(captureAny)).captured[1] as Set; + + final gmaps.LatLng gmCenter = map.center!; + + expect(controller.myLocationButton, isNull); + expect(capturedMarkers.length, 1); + expect(capturedMarkers.first.position, currentLocation); + expect(capturedMarkers.first.zIndex, 0.5); + expect(gmCenter.lat, currentLocation.latitude); + expect(gmCenter.lng, currentLocation.longitude); + }); + }); + + testWidgets( + 'My location button should be disable when dont have permission access to location', + (WidgetTester tester) async { + late final MockGeolocation mockGeolocation = MockGeolocation(); + + controller = createController( + mapConfiguration: const MapConfiguration( + myLocationEnabled: true, + myLocationButtonEnabled: true, + )); + + controller.debugSetOverrides( + createMap: (_, __) => map, + markers: markers, + geolocation: mockGeolocation, + ); + + when(mockGeolocation.getCurrentPosition( + timeout: const Duration(seconds: 30))) + .thenAnswer( + (_) async => throw Exception('permission denied'), + ); + + when(mockGeolocation.watchPosition()).thenAnswer((_) { + return Stream.fromIterable([]); + }); + + controller.init(); + + await Future.delayed(const Duration(milliseconds: 50)); + + final Set capturedMarkers = + verify(markers.addMarkers(captureAny)).captured[0] as Set; + + expect(controller.myLocationButton, isNotNull); + expect(controller.myLocationButton?.isDisabled(), true); + expect(capturedMarkers.length, 0); + }); }); // These are the methods that are delegated to the gmaps.GMap object, that we can mock... diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart index efde6645932..9bae50540ad 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart @@ -3,10 +3,13 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i6; +import 'dart:html' as _i3; + import 'package:google_maps/google_maps.dart' as _i2; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' - as _i4; -import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i3; + as _i5; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: type=lint @@ -30,16 +33,26 @@ class _FakeGMap_0 extends _i1.SmartFake implements _i2.GMap { ); } +class _FakeGeoposition_1 extends _i1.SmartFake implements _i3.Geoposition { + _FakeGeoposition_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [CirclesController]. /// /// See the documentation for Mockito's code generation for more information. -class MockCirclesController extends _i1.Mock implements _i3.CirclesController { +class MockCirclesController extends _i1.Mock implements _i4.CirclesController { @override - Map<_i4.CircleId, _i3.CircleController> get circles => (super.noSuchMethod( + Map<_i5.CircleId, _i4.CircleController> get circles => (super.noSuchMethod( Invocation.getter(#circles), - returnValue: <_i4.CircleId, _i3.CircleController>{}, - returnValueForMissingStub: <_i4.CircleId, _i3.CircleController>{}, - ) as Map<_i4.CircleId, _i3.CircleController>); + returnValue: <_i5.CircleId, _i4.CircleController>{}, + returnValueForMissingStub: <_i5.CircleId, _i4.CircleController>{}, + ) as Map<_i5.CircleId, _i4.CircleController>); @override _i2.GMap get googleMap => (super.noSuchMethod( Invocation.getter(#googleMap), @@ -75,7 +88,7 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { returnValueForMissingStub: null, ); @override - void addCircles(Set<_i4.Circle>? circlesToAdd) => super.noSuchMethod( + void addCircles(Set<_i5.Circle>? circlesToAdd) => super.noSuchMethod( Invocation.method( #addCircles, [circlesToAdd], @@ -83,7 +96,7 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { returnValueForMissingStub: null, ); @override - void changeCircles(Set<_i4.Circle>? circlesToChange) => super.noSuchMethod( + void changeCircles(Set<_i5.Circle>? circlesToChange) => super.noSuchMethod( Invocation.method( #changeCircles, [circlesToChange], @@ -91,7 +104,7 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { returnValueForMissingStub: null, ); @override - void removeCircles(Set<_i4.CircleId>? circleIdsToRemove) => + void removeCircles(Set<_i5.CircleId>? circleIdsToRemove) => super.noSuchMethod( Invocation.method( #removeCircles, @@ -120,13 +133,13 @@ class MockCirclesController extends _i1.Mock implements _i3.CirclesController { /// /// See the documentation for Mockito's code generation for more information. class MockPolygonsController extends _i1.Mock - implements _i3.PolygonsController { + implements _i4.PolygonsController { @override - Map<_i4.PolygonId, _i3.PolygonController> get polygons => (super.noSuchMethod( + Map<_i5.PolygonId, _i4.PolygonController> get polygons => (super.noSuchMethod( Invocation.getter(#polygons), - returnValue: <_i4.PolygonId, _i3.PolygonController>{}, - returnValueForMissingStub: <_i4.PolygonId, _i3.PolygonController>{}, - ) as Map<_i4.PolygonId, _i3.PolygonController>); + returnValue: <_i5.PolygonId, _i4.PolygonController>{}, + returnValueForMissingStub: <_i5.PolygonId, _i4.PolygonController>{}, + ) as Map<_i5.PolygonId, _i4.PolygonController>); @override _i2.GMap get googleMap => (super.noSuchMethod( Invocation.getter(#googleMap), @@ -162,7 +175,7 @@ class MockPolygonsController extends _i1.Mock returnValueForMissingStub: null, ); @override - void addPolygons(Set<_i4.Polygon>? polygonsToAdd) => super.noSuchMethod( + void addPolygons(Set<_i5.Polygon>? polygonsToAdd) => super.noSuchMethod( Invocation.method( #addPolygons, [polygonsToAdd], @@ -170,7 +183,7 @@ class MockPolygonsController extends _i1.Mock returnValueForMissingStub: null, ); @override - void changePolygons(Set<_i4.Polygon>? polygonsToChange) => super.noSuchMethod( + void changePolygons(Set<_i5.Polygon>? polygonsToChange) => super.noSuchMethod( Invocation.method( #changePolygons, [polygonsToChange], @@ -178,7 +191,7 @@ class MockPolygonsController extends _i1.Mock returnValueForMissingStub: null, ); @override - void removePolygons(Set<_i4.PolygonId>? polygonIdsToRemove) => + void removePolygons(Set<_i5.PolygonId>? polygonIdsToRemove) => super.noSuchMethod( Invocation.method( #removePolygons, @@ -207,13 +220,13 @@ class MockPolygonsController extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockPolylinesController extends _i1.Mock - implements _i3.PolylinesController { + implements _i4.PolylinesController { @override - Map<_i4.PolylineId, _i3.PolylineController> get lines => (super.noSuchMethod( + Map<_i5.PolylineId, _i4.PolylineController> get lines => (super.noSuchMethod( Invocation.getter(#lines), - returnValue: <_i4.PolylineId, _i3.PolylineController>{}, - returnValueForMissingStub: <_i4.PolylineId, _i3.PolylineController>{}, - ) as Map<_i4.PolylineId, _i3.PolylineController>); + returnValue: <_i5.PolylineId, _i4.PolylineController>{}, + returnValueForMissingStub: <_i5.PolylineId, _i4.PolylineController>{}, + ) as Map<_i5.PolylineId, _i4.PolylineController>); @override _i2.GMap get googleMap => (super.noSuchMethod( Invocation.getter(#googleMap), @@ -249,7 +262,7 @@ class MockPolylinesController extends _i1.Mock returnValueForMissingStub: null, ); @override - void addPolylines(Set<_i4.Polyline>? polylinesToAdd) => super.noSuchMethod( + void addPolylines(Set<_i5.Polyline>? polylinesToAdd) => super.noSuchMethod( Invocation.method( #addPolylines, [polylinesToAdd], @@ -257,7 +270,7 @@ class MockPolylinesController extends _i1.Mock returnValueForMissingStub: null, ); @override - void changePolylines(Set<_i4.Polyline>? polylinesToChange) => + void changePolylines(Set<_i5.Polyline>? polylinesToChange) => super.noSuchMethod( Invocation.method( #changePolylines, @@ -266,7 +279,7 @@ class MockPolylinesController extends _i1.Mock returnValueForMissingStub: null, ); @override - void removePolylines(Set<_i4.PolylineId>? polylineIdsToRemove) => + void removePolylines(Set<_i5.PolylineId>? polylineIdsToRemove) => super.noSuchMethod( Invocation.method( #removePolylines, @@ -294,13 +307,13 @@ class MockPolylinesController extends _i1.Mock /// A class which mocks [MarkersController]. /// /// See the documentation for Mockito's code generation for more information. -class MockMarkersController extends _i1.Mock implements _i3.MarkersController { +class MockMarkersController extends _i1.Mock implements _i4.MarkersController { @override - Map<_i4.MarkerId, _i3.MarkerController> get markers => (super.noSuchMethod( + Map<_i5.MarkerId, _i4.MarkerController> get markers => (super.noSuchMethod( Invocation.getter(#markers), - returnValue: <_i4.MarkerId, _i3.MarkerController>{}, - returnValueForMissingStub: <_i4.MarkerId, _i3.MarkerController>{}, - ) as Map<_i4.MarkerId, _i3.MarkerController>); + returnValue: <_i5.MarkerId, _i4.MarkerController>{}, + returnValueForMissingStub: <_i5.MarkerId, _i4.MarkerController>{}, + ) as Map<_i5.MarkerId, _i4.MarkerController>); @override _i2.GMap get googleMap => (super.noSuchMethod( Invocation.getter(#googleMap), @@ -336,7 +349,7 @@ class MockMarkersController extends _i1.Mock implements _i3.MarkersController { returnValueForMissingStub: null, ); @override - void addMarkers(Set<_i4.Marker>? markersToAdd) => super.noSuchMethod( + void addMarkers(Set<_i5.Marker>? markersToAdd) => super.noSuchMethod( Invocation.method( #addMarkers, [markersToAdd], @@ -344,7 +357,7 @@ class MockMarkersController extends _i1.Mock implements _i3.MarkersController { returnValueForMissingStub: null, ); @override - void changeMarkers(Set<_i4.Marker>? markersToChange) => super.noSuchMethod( + void changeMarkers(Set<_i5.Marker>? markersToChange) => super.noSuchMethod( Invocation.method( #changeMarkers, [markersToChange], @@ -352,7 +365,7 @@ class MockMarkersController extends _i1.Mock implements _i3.MarkersController { returnValueForMissingStub: null, ); @override - void removeMarkers(Set<_i4.MarkerId>? markerIdsToRemove) => + void removeMarkers(Set<_i5.MarkerId>? markerIdsToRemove) => super.noSuchMethod( Invocation.method( #removeMarkers, @@ -361,7 +374,7 @@ class MockMarkersController extends _i1.Mock implements _i3.MarkersController { returnValueForMissingStub: null, ); @override - void showMarkerInfoWindow(_i4.MarkerId? markerId) => super.noSuchMethod( + void showMarkerInfoWindow(_i5.MarkerId? markerId) => super.noSuchMethod( Invocation.method( #showMarkerInfoWindow, [markerId], @@ -369,7 +382,7 @@ class MockMarkersController extends _i1.Mock implements _i3.MarkersController { returnValueForMissingStub: null, ); @override - void hideMarkerInfoWindow(_i4.MarkerId? markerId) => super.noSuchMethod( + void hideMarkerInfoWindow(_i5.MarkerId? markerId) => super.noSuchMethod( Invocation.method( #hideMarkerInfoWindow, [markerId], @@ -377,7 +390,7 @@ class MockMarkersController extends _i1.Mock implements _i3.MarkersController { returnValueForMissingStub: null, ); @override - bool isInfoWindowShown(_i4.MarkerId? markerId) => (super.noSuchMethod( + bool isInfoWindowShown(_i5.MarkerId? markerId) => (super.noSuchMethod( Invocation.method( #isInfoWindowShown, [markerId], @@ -401,3 +414,80 @@ class MockMarkersController extends _i1.Mock implements _i3.MarkersController { returnValueForMissingStub: null, ); } + +/// A class which mocks [Geolocation]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGeolocation extends _i1.Mock implements _i3.Geolocation { + @override + _i6.Future<_i3.Geoposition> getCurrentPosition({ + bool? enableHighAccuracy, + Duration? timeout, + Duration? maximumAge, + }) => + (super.noSuchMethod( + Invocation.method( + #getCurrentPosition, + [], + { + #enableHighAccuracy: enableHighAccuracy, + #timeout: timeout, + #maximumAge: maximumAge, + }, + ), + returnValue: _i6.Future<_i3.Geoposition>.value(_FakeGeoposition_1( + this, + Invocation.method( + #getCurrentPosition, + [], + { + #enableHighAccuracy: enableHighAccuracy, + #timeout: timeout, + #maximumAge: maximumAge, + }, + ), + )), + returnValueForMissingStub: + _i6.Future<_i3.Geoposition>.value(_FakeGeoposition_1( + this, + Invocation.method( + #getCurrentPosition, + [], + { + #enableHighAccuracy: enableHighAccuracy, + #timeout: timeout, + #maximumAge: maximumAge, + }, + ), + )), + ) as _i6.Future<_i3.Geoposition>); + @override + _i6.Stream<_i3.Geoposition> watchPosition({ + bool? enableHighAccuracy, + Duration? timeout, + Duration? maximumAge, + }) => + (super.noSuchMethod( + Invocation.method( + #watchPosition, + [], + { + #enableHighAccuracy: enableHighAccuracy, + #timeout: timeout, + #maximumAge: maximumAge, + }, + ), + returnValue: _i6.Stream<_i3.Geoposition>.empty(), + returnValueForMissingStub: _i6.Stream<_i3.Geoposition>.empty(), + ) as _i6.Stream<_i3.Geoposition>); +} + +/// A class which mocks [Geoposition]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGeoposition extends _i1.Mock implements _i3.Geoposition {} + +/// A class which mocks [Coordinates]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCoordinates extends _i1.Mock implements _i3.Coordinates {} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart index a85bce31e20..d6653e81524 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart @@ -4,8 +4,9 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i2; +import 'dart:html' as _i5; -import 'package:google_maps/google_maps.dart' as _i5; +import 'package:google_maps/google_maps.dart' as _i6; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' as _i3; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i4; @@ -101,6 +102,7 @@ class MockGoogleMapController extends _i1.Mock _i4.CirclesController? circles, _i4.PolygonsController? polygons, _i4.PolylinesController? polylines, + _i5.Geolocation? geolocation, }) => super.noSuchMethod( Invocation.method( @@ -112,6 +114,7 @@ class MockGoogleMapController extends _i1.Mock #circles: circles, #polygons: polygons, #polylines: polylines, + #geolocation: geolocation, }, ), returnValueForMissingStub: null, @@ -134,7 +137,7 @@ class MockGoogleMapController extends _i1.Mock returnValueForMissingStub: null, ); @override - void updateStyles(List<_i5.MapTypeStyle>? styles) => super.noSuchMethod( + void updateStyles(List<_i6.MapTypeStyle>? styles) => super.noSuchMethod( Invocation.method( #updateStyles, [styles], diff --git a/packages/google_maps_flutter/google_maps_flutter_web/icons/blue-dot.png b/packages/google_maps_flutter/google_maps_flutter_web/icons/blue-dot.png new file mode 100644 index 00000000000..77780f8ab4b Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_web/icons/blue-dot.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_web/icons/mylocation-sprite-2x.png b/packages/google_maps_flutter/google_maps_flutter_web/icons/mylocation-sprite-2x.png new file mode 100644 index 00000000000..546b97a245d Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_web/icons/mylocation-sprite-2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart index 0650184a14d..9887ab6be90 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart @@ -30,6 +30,7 @@ part 'src/google_maps_controller.dart'; part 'src/google_maps_flutter_web.dart'; part 'src/marker.dart'; part 'src/markers.dart'; +part 'src/my_location.dart'; part 'src/polygon.dart'; part 'src/polygons.dart'; part 'src/polyline.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart index 25cba849475..9ec45f217a0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart @@ -41,7 +41,7 @@ double _getCssOpacity(Color color) { // myLocationEnabled needs to be built through dart:html navigator.geolocation // See: https://api.dart.dev/stable/2.8.4/dart-html/Geolocation-class.html // trafficEnabled is handled when creating the GMap object, since it needs to be added as a layer. -// trackCameraPosition is just a boolan value that indicates if the map has an onCameraMove handler. +// trackCameraPosition is just a boolean value that indicates if the map has an onCameraMove handler. // indoorViewEnabled seems to not have an equivalent in web // buildingsEnabled seems to not have an equivalent in web // padding seems to behave differently in web than mobile. You can't move UI elements in web. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart index a659fb21880..aa0a7f4ab95 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart @@ -75,6 +75,13 @@ class GoogleMapController { return _widget; } + // Get current location + MyLocationButton? _myLocationButton; + + /// A getter for the my location button + @visibleForTesting + MyLocationButton? get myLocationButton => _myLocationButton; + // The currently-enabled traffic layer. gmaps.TrafficLayer? _trafficLayer; @@ -114,12 +121,14 @@ class GoogleMapController { CirclesController? circles, PolygonsController? polygons, PolylinesController? polylines, + Geolocation? geolocation, }) { _overrideCreateMap = createMap; _markersController = markers ?? _markersController; _circlesController = circles ?? _circlesController; _polygonsController = polygons ?? _polygonsController; _polylinesController = polylines ?? _polylinesController; + _geolocation = geolocation ?? _geolocation; } DebugCreateMapFunction? _overrideCreateMap; @@ -163,6 +172,7 @@ class GoogleMapController { // Create the map... final gmaps.GMap map = _createMap(_div, options); + _googleMap = map; _attachMapEvents(map); @@ -177,6 +187,20 @@ class GoogleMapController { ); _setTrafficLayer(map, _lastMapConfiguration.trafficEnabled ?? false); + + _renderMyLocation(map, _lastMapConfiguration); + } + + // Render my location + Future _renderMyLocation( + gmaps.GMap map, MapConfiguration mapConfiguration) async { + if (mapConfiguration.myLocationEnabled ?? false) { + if (mapConfiguration.myLocationButtonEnabled ?? false) { + _addMyLocationButton(map, this); + } + _displayAndWatchMyLocation(_markersController!); + _centerMyCurrentLocation(this); + } } // Funnels map gmap events into the plugin's stream controller. @@ -426,6 +450,7 @@ class GoogleMapController { _polygonsController = null; _polylinesController = null; _markersController = null; + _myLocationButton = null; _streamController.close(); } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/my_location.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/my_location.dart new file mode 100644 index 00000000000..e776fb12018 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/my_location.dart @@ -0,0 +1,187 @@ +// Copyright 2013 The Flutter 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_web; + +Geolocation _geolocation = window.navigator.geolocation; + +// Watch current location and update blue dot +Future _displayAndWatchMyLocation(MarkersController controller) async { + final Marker marker = await _createBlueDotMarker(); + _geolocation.watchPosition().listen((Geoposition location) async { + // TODO(nploi): https://github.com/flutter/plugins/pull/6868#discussion_r1057898052 + // We're discarding a lot of information from coords, like its accuracy, heading and speed. Those can be used to: + // - Render a bigger "blue halo" around the current position marker when the accuracy is low. + // - Render the direction in which we're looking at with a small "cone" using the heading information. + // - Render the current position marker as an arrow when the current position is "moving" (speed > certain threshold), and the direction in which the arrow should point (again, with the heading information). + controller.addMarkers({ + marker.copyWith( + positionParam: LatLng( + location.coords!.latitude!.toDouble(), + location.coords!.longitude!.toDouble(), + )) + }); + }); +} + +// Get current location +Future _getCurrentLocation() async { + final Geoposition location = await _geolocation.getCurrentPosition( + timeout: const Duration(seconds: 30)); + return LatLng( + location.coords!.latitude!.toDouble(), + location.coords!.longitude!.toDouble(), + ); +} + +// Find and move to current location +Future _centerMyCurrentLocation( + GoogleMapController controller, +) async { + try { + final LatLng location = await _getCurrentLocation(); + await controller.moveCamera( + CameraUpdate.newLatLng(location), + ); + controller._myLocationButton?.doneAnimation(); + } catch (e) { + controller._myLocationButton?.disableBtn(); + } +} + +// Add my location to map +void _addMyLocationButton(gmaps.GMap map, GoogleMapController controller) { + controller._myLocationButton = MyLocationButton(); + controller._myLocationButton?.addClickListener( + (_) async { + controller._myLocationButton?.startAnimation(); + await _centerMyCurrentLocation(controller); + }, + ); + map.addListener('dragend', () { + controller._myLocationButton?.resetAnimation(); + }); + + map.controls![gmaps.ControlPosition.RIGHT_BOTTOM as int] + ?.push(controller._myLocationButton?.getButton); +} + +// Create blue dot marker +Future _createBlueDotMarker() async { + final BitmapDescriptor icon = await BitmapDescriptor.fromAssetImage( + const ImageConfiguration(size: Size(18, 18)), + 'icons/blue-dot.png', + package: 'google_maps_flutter_web', + ); + return Marker( + markerId: const MarkerId('my_location_blue_dot'), + icon: icon, + zIndex: 0.5, + ); +} + +/// This class support create my location button & handle animation +@visibleForTesting +class MyLocationButton { + /// Add css and create my location button + MyLocationButton() { + _addCss(); + _createButton(); + } + + late ButtonElement _btnChild; + late DivElement _imageChild; + late DivElement _controlDiv; + + // Add animation css + void _addCss() { + final StyleElement styleElement = StyleElement(); + document.head?.append(styleElement); + // ignore: cast_nullable_to_non_nullable + final CssStyleSheet sheet = styleElement.sheet as CssStyleSheet; + String rule = + '.waiting { animation: 1000ms infinite step-end blink-position-icon;}'; + sheet.insertRule(rule); + rule = + '@keyframes blink-position-icon {0% {background-position: -24px 0px;} ' + '50% {background-position: 0px 0px;}}'; + sheet.insertRule(rule); + } + + // Add My Location widget to right bottom + void _createButton() { + _controlDiv = DivElement(); + + _controlDiv.style.marginRight = '10px'; + + _btnChild = ButtonElement(); + _btnChild.className = 'gm-control-active'; + _btnChild.style.backgroundColor = '#fff'; + _btnChild.style.border = 'none'; + _btnChild.style.outline = 'none'; + _btnChild.style.width = '40px'; + _btnChild.style.height = '40px'; + _btnChild.style.borderRadius = '2px'; + _btnChild.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)'; + _btnChild.style.cursor = 'pointer'; + _btnChild.style.padding = '8px'; + _controlDiv.append(_btnChild); + + _imageChild = DivElement(); + _imageChild.style.width = '24px'; + _imageChild.style.height = '24px'; + _imageChild.style.backgroundImage = + 'url(${window.location.href.replaceAll('/#', '')}/assets/packages/google_maps_flutter_web/icons/mylocation-sprite-2x.png)'; + _imageChild.style.backgroundSize = '240px 24px'; + _imageChild.style.backgroundPosition = '0px 0px'; + _imageChild.style.backgroundRepeat = 'no-repeat'; + _imageChild.id = 'my_location_btn'; + _btnChild.append(_imageChild); + } + + /// Get button element + HtmlElement get getButton => _controlDiv; + + /// Add click listener + void addClickListener(EventListener? listener) { + _btnChild.addEventListener('click', listener); + } + + /// Reset animation + void resetAnimation() { + if (_btnChild.disabled) { + _imageChild.style.backgroundPosition = '-24px 0px'; + } else { + _imageChild.style.backgroundPosition = '0px 0px'; + } + } + + /// Start animation + void startAnimation() { + if (_btnChild.disabled) { + return; + } + _imageChild.classes.add('waiting'); + } + + /// Done animation + void doneAnimation() { + if (_btnChild.disabled) { + return; + } + _imageChild.classes.remove('waiting'); + _imageChild.style.backgroundPosition = '-192px 0px'; + } + + /// Disable button + void disableBtn() { + _btnChild.disabled = true; + _imageChild.style.backgroundPosition = '-24px 0px'; + } + + /// Check button disabled or enabled + bool isDisabled() { + return _btnChild.disabled; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 072d584b133..fd97ed3dfcd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.4.0+5 +version: 0.4.1 environment: sdk: ">=2.12.0 <3.0.0" @@ -15,7 +15,8 @@ flutter: web: pluginClass: GoogleMapsPlugin fileName: google_maps_flutter_web.dart - + assets: + - icons/ dependencies: flutter: sdk: flutter