diff --git a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS index b5b0e84d473..1fd9e9fae16 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS +++ b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS @@ -65,3 +65,4 @@ Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> Justin Baumann +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 0427818ca50..b5ad9f8f8fb 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 @@ ## 0.5.2 +* Add "My Location" Widget. Issue [#64073](https://github.com/flutter/flutter/issues/64073) * Adds options for gesture handling and tilt controls. ## 0.5.1 diff --git a/packages/google_maps_flutter/google_maps_flutter_web/README.md b/packages/google_maps_flutter/google_maps_flutter_web/README.md index b52da95119c..5fe99e56278 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md @@ -60,11 +60,6 @@ The following map options are not available in web, because the map doesn't rota There's no "Map Toolbar" in web, so the `mapToolbarEnabled` option is unused. -There's no "My Location" widget in web ([tracking issue](https://github.com/flutter/flutter/issues/64073)), so the following options are ignored, for now: - -* `myLocationButtonEnabled` -* `myLocationEnabled` - There's no `defaultMarkerWithHue` in web. If you need colored pins/markers, you may need to use your own asset images. Indoor and building layers are still not available on the web. Traffic is. 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 00a448a979b..c1b2aa3a917 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 @@ -21,6 +21,9 @@ import 'google_maps_controller_test.mocks.dart'; 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 @@ -483,6 +486,107 @@ 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: anyNamed('timeout'), + )).thenAnswer((_) async => mockGeoposition); + + when(mockGeolocation.watchPosition()).thenAnswer((_) { + return Stream.fromIterable( + [mockGeoposition]); + }); + + controller.init(); + await tester.pumpAndSettle(); + + final Set capturedMarkers = + verify(markers.addMarkers(captureAny)).captured[1] as Set; + + expect(controller.myLocationButton, isNotNull); + expect(capturedMarkers.length, 1); + expect(capturedMarkers.first.position, currentLocation); + expect(capturedMarkers.first.zIndex, 0.5); + }); + + 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.call( + timeout: const Duration(seconds: 30), + )).thenAnswer((_) async => mockGeoposition); + + when(mockGeolocation.watchPosition()).thenAnswer((_) { + return Stream.fromIterable( + [mockGeoposition]); + }); + + controller.init(); + + await tester.pumpAndSettle(); + + final Set capturedMarkers = + verify(markers.addMarkers(captureAny)).captured[1] as Set; + + expect(controller.myLocationButton, isNull); + expect(capturedMarkers.length, 1); + expect(capturedMarkers.first.position, currentLocation); + expect(capturedMarkers.first.zIndex, 0.5); + }); + }); }); // 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 4bc5f365d2d..7940be6c925 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 @@ -5,10 +5,13 @@ // @dart=2.19 // 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 @@ -32,16 +35,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), @@ -77,7 +90,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], @@ -85,7 +98,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], @@ -93,7 +106,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, @@ -122,13 +135,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), @@ -164,7 +177,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], @@ -172,7 +185,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], @@ -180,7 +193,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, @@ -209,13 +222,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), @@ -251,7 +264,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], @@ -259,7 +272,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, @@ -268,7 +281,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, @@ -296,13 +309,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), @@ -338,7 +351,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], @@ -346,7 +359,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], @@ -354,7 +367,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, @@ -363,7 +376,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], @@ -371,7 +384,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], @@ -379,7 +392,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], @@ -403,3 +416,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 36e6052a21c..fbe04939b9e 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 @@ -6,8 +6,9 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i3; +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 _i2; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i4; @@ -126,6 +127,7 @@ class MockGoogleMapController extends _i1.Mock _i4.CirclesController? circles, _i4.PolygonsController? polygons, _i4.PolylinesController? polylines, + _i5.Geolocation? geolocation, }) => super.noSuchMethod( Invocation.method( @@ -137,6 +139,7 @@ class MockGoogleMapController extends _i1.Mock #circles: circles, #polygons: polygons, #polylines: polylines, + #geolocation: geolocation, }, ), returnValueForMissingStub: null, @@ -159,7 +162,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 df0b8de4c4a..2d09f00472b 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 @@ -13,7 +13,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart' as flutter_web_plugins; import 'package:google_maps/google_maps.dart' as gmaps; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:sanitize_html/sanitize_html.dart'; @@ -35,3 +35,4 @@ part 'src/polygon.dart'; part 'src/polygons.dart'; part 'src/polyline.dart'; part 'src/polylines.dart'; +part 'src/my_location.dart'; 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 f49a6878df5..5126df07b24 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 @@ -83,6 +83,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; @@ -122,12 +129,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; @@ -176,6 +185,7 @@ class GoogleMapController { // Create the map... final gmaps.GMap map = _createMap(_div, options); + _googleMap = map; _attachMapEvents(map); @@ -190,6 +200,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); + } + await _displayAndWatchMyLocation(this); + await _centerMyCurrentLocation(this); + } } // Funnels map gmap events into the plugin's stream controller. @@ -439,6 +463,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/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart index 049a6a25ded..4dd285e13b4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart @@ -9,7 +9,7 @@ part of google_maps_flutter_web; /// This class implements the `package:google_maps_flutter` functionality for the web. class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { /// Registers this class as the default instance of [GoogleMapsFlutterPlatform]. - static void registerWith(Registrar registrar) { + static void registerWith(flutter_web_plugins.Registrar registrar) { GoogleMapsFlutterPlatform.instance = GoogleMapsPlugin(); } 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..f2171631a55 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/my_location.dart @@ -0,0 +1,210 @@ +// 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; + +// Default values for when the get current location fails +const LatLng _nullLatLng = LatLng(0, 0); +Geolocation _geolocation = window.navigator.geolocation; +LatLng _lastKnownLocation = _nullLatLng; + +// Watch current location and update blue dot +Future _displayAndWatchMyLocation(GoogleMapController controller) async { + final Marker marker = await _createBlueDotMarker(); + _geolocation.watchPosition().listen( + (Geoposition location) async { + _lastKnownLocation = LatLng( + location.coords?.latitude?.toDouble() ?? _nullLatLng.latitude, + location.coords?.longitude?.toDouble() ?? _nullLatLng.longitude, + ); + // 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). + + final Marker markerUpdate = + marker.copyWith(positionParam: _lastKnownLocation); + + if (controller._markersController?.markers.containsKey(marker.markerId) ?? + false) { + controller._markersController?._changeMarker(markerUpdate); + } else { + controller._markersController?.addMarkers({markerUpdate}); + } + }, + onError: (_) { + controller._myLocationButton?.doneAnimation(); + }, + ); +} + +// Get current location +Future _getCurrentLocation() async { + if (_lastKnownLocation != _nullLatLng) { + return _lastKnownLocation; + } + final Geoposition location = await _geolocation.getCurrentPosition( + timeout: const Duration(seconds: 30), + ); + _lastKnownLocation = LatLng( + location.coords?.latitude?.toDouble() ?? _nullLatLng.latitude, + location.coords?.longitude?.toDouble() ?? _nullLatLng.longitude, + ); + return _lastKnownLocation; +} + +// 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 { + await _centerMyCurrentLocation(controller); + }, + ); + map.addListener('dragend', () { + controller._myLocationButton?.resetAnimation(); + }); + map.addListener('center_changed', () { + 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.classes.remove('waiting'); + _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 f1eb09c6c57..be5d9ce14ff 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -15,6 +15,8 @@ flutter: web: pluginClass: GoogleMapsPlugin fileName: google_maps_flutter_web.dart + assets: + - icons/ dependencies: flutter: @@ -33,3 +35,4 @@ dev_dependencies: # The example deliberately includes limited-use secrets. false_secrets: - /example/web/index.html +