diff --git a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index 6bd36da0ee9a..273c0004e323 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -32,3 +32,8 @@ flutter: uses-material-design: true assets: - assets/ +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_maps_flutter_platform_interface: {path: /Users/zubii/Projects/packages/packages/google_maps_flutter/google_maps_flutter_platform_interface} + google_maps_flutter_web: {path: ../../../../packages/google_maps_flutter/google_maps_flutter_web} diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 0342292634b8..ce30cd7979bb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -41,3 +41,9 @@ topics: # The example deliberately includes limited-use secrets. false_secrets: - /example/web/index.html + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_maps_flutter_platform_interface: {path: /Users/zubii/Projects/packages/packages/google_maps_flutter/google_maps_flutter_platform_interface} + google_maps_flutter_web: {path: ../../../packages/google_maps_flutter/google_maps_flutter_web} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS index 906ab219beee..2d5b11ddc211 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS +++ b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS @@ -66,3 +66,4 @@ Alex Li Rahul Raj <64.rahulraj@gmail.com> Justin Baumann Joonas Kerttula +Zubsacu Ionut 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 c8657c54f8d8..fa2d54c310a2 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.3 + +* Adds support for my location button. + ## 0.6.2+3 * Updates README to include setup information. 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 ada93c74d823..e69ee0b28f65 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md @@ -100,11 +100,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/assets/blue-dot.png b/packages/google_maps_flutter/google_maps_flutter_web/assets/blue-dot.png new file mode 100644 index 000000000000..77780f8ab4b7 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_web/assets/blue-dot.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_web/assets/my_location-sprite-2x.png b/packages/google_maps_flutter/google_maps_flutter_web/assets/my_location-sprite-2x.png new file mode 100644 index 000000000000..546b97a245d9 Binary files /dev/null and b/packages/google_maps_flutter/google_maps_flutter_web/assets/my_location-sprite-2x.png differ diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_controller_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_controller_test.dart index bf1a77889f2b..a4183aaf9146 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_controller_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_controller_test.dart @@ -32,6 +32,8 @@ gmaps.Map mapShim() => throw UnimplementedError(); ), MockSpec(fallbackGenerators: {#googleMap: mapShim}), MockSpec(fallbackGenerators: {#googleMap: mapShim}), + MockSpec(fallbackGenerators: {#googleMap: mapShim}), + MockSpec(fallbackGenerators: {#googleMap: mapShim}), ]) /// Test Google Map Controller void main() { @@ -1025,5 +1027,121 @@ void main() { verify(mock.isInfoWindowShown(markerId)); }); }); + + group('My Location', () { + late MockMyLocationController mockMyLocationController; + late MockGeolocationApi mockGeolocationApi; + + setUp(() { + mockMyLocationController = MockMyLocationController(); + mockGeolocationApi = MockGeolocationApi(); + }); + + testWidgets('by default is disabled', (WidgetTester tester) async { + controller = createController(); + controller.init(); + expect(mockMyLocationController.myLocationButton, isNull); + }); + + testWidgets('initializes with my location & display my location button', ( + WidgetTester tester, + ) async { + const currentLocation = LatLng(10.8231, 106.6297); + final map = gmaps.Map(createDivElement()); + final markers = MockMarkersController(); + + controller = createController( + mapConfiguration: const MapConfiguration( + myLocationEnabled: true, + myLocationButtonEnabled: true, + ), + ); + + final myLocationController = MyLocationController(geolocationApi: mockGeolocationApi); + + controller.debugSetOverrides( + createMap: (_, _) => map, + markers: markers, + myLocation: myLocationController, + ); + + when(mockGeolocationApi.watchPosition(any, any)).thenAnswer((inv) { + final onSuccess = inv.positionalArguments[0] as void Function(double, double); + onSuccess(currentLocation.latitude, currentLocation.longitude); + return 0; + }); + when( + mockGeolocationApi.getCurrentPosition(any, any, timeoutMs: anyNamed('timeoutMs')), + ).thenAnswer((inv) { + final onSuccess = inv.positionalArguments[0] as void Function(double, double); + onSuccess(currentLocation.latitude, currentLocation.longitude); + }); + + controller.init(); + await tester.pumpAndSettle(); + + final List> allAddMarkersCalls = verify( + markers.addMarkers(captureAny), + ).captured.cast>(); + final Set blueDotCall = allAddMarkersCalls.firstWhere( + (set) => set.any((m) => m.markerId == const MarkerId('my_location_blue_dot')), + orElse: () => {}, + ); + + expect(blueDotCall.length, 1); + expect(blueDotCall.first.position, currentLocation); + expect(blueDotCall.first.zIndex, .5); + expect(map.controls[gmaps.ControlPosition.RIGHT_BOTTOM as int].length, equals(1)); + }); + + testWidgets('initializes with my location only', (WidgetTester tester) async { + final map = gmaps.Map(createDivElement()); + final markers = MockMarkersController(); + const currentLocation = LatLng(10.8231, 106.6297); + + controller = createController( + mapConfiguration: const MapConfiguration( + myLocationEnabled: true, + myLocationButtonEnabled: false, + ), + ); + + final myLocationController = MyLocationController(geolocationApi: mockGeolocationApi); + + controller.debugSetOverrides( + createMap: (_, _) => map, + markers: markers, + myLocation: myLocationController, + ); + + when(mockGeolocationApi.watchPosition(any, any)).thenAnswer((inv) { + final onSuccess = inv.positionalArguments[0] as void Function(double, double); + onSuccess(currentLocation.latitude, currentLocation.longitude); + return 0; + }); + when( + mockGeolocationApi.getCurrentPosition(any, any, timeoutMs: anyNamed('timeoutMs')), + ).thenAnswer((inv) { + final onSuccess = inv.positionalArguments[0] as void Function(double, double); + onSuccess(currentLocation.latitude, currentLocation.longitude); + }); + + controller.init(); + await tester.pumpAndSettle(); + + final List> allAddMarkersCalls = verify( + markers.addMarkers(captureAny), + ).captured.cast>(); + final Set blueDotCall = allAddMarkersCalls.firstWhere( + (set) => set.any((m) => m.markerId == const MarkerId('my_location_blue_dot')), + orElse: () => {}, + ); + + expect(blueDotCall.length, 1); + expect(blueDotCall.first.position, currentLocation); + expect(blueDotCall.first.zIndex, 0.5); + expect(map.controls[gmaps.ControlPosition.RIGHT_BOTTOM as int].length, equals(0)); + }); + }); }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_controller_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_controller_test.mocks.dart index 6da195b8f167..249bf6f37bfb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_controller_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_controller_test.mocks.dart @@ -498,3 +498,87 @@ class MockGroundOverlaysController extends _i1.Mock implements _i2.GroundOverlay returnValueForMissingStub: null, ); } + +/// A class which mocks [MyLocationController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockMyLocationController extends _i1.Mock implements _i2.MyLocationController { + @override + set myLocationButton(_i2.MyLocationButton? value) => super.noSuchMethod( + Invocation.setter(#myLocationButton, value), + returnValueForMissingStub: null, + ); + + @override + _i6.Future displayAndWatchMyLocation( + _i2.MarkersController? markersController, + ) => + (super.noSuchMethod( + Invocation.method(#displayAndWatchMyLocation, [markersController]), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) + as _i6.Future); + + @override + _i6.Future centerMyCurrentLocation(_i2.GoogleMapController? controller) => + (super.noSuchMethod( + Invocation.method(#centerMyCurrentLocation, [controller]), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) + as _i6.Future); + + @override + void addMyLocationButton(_i4.Map? map, _i2.GoogleMapController? controller) => super.noSuchMethod( + Invocation.method(#addMyLocationButton, [map, controller]), + returnValueForMissingStub: null, + ); + + @override + void removeMyLocationButton(_i4.Map? map) => super.noSuchMethod( + Invocation.method(#removeMyLocationButton, [map]), + returnValueForMissingStub: null, + ); + + @override + void removeBlueDot(_i2.MarkersController? markersController) => + super.noSuchMethod( + Invocation.method(#removeBlueDot, [markersController]), + returnValueForMissingStub: null, + ); + + @override + void dispose() => + super.noSuchMethod(Invocation.method(#dispose, []), returnValueForMissingStub: null); +} + +/// A class which mocks [GeolocationApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGeolocationApi extends _i1.Mock implements _i2.GeolocationApi { + @override + int watchPosition(void Function(double, double)? onSuccess, void Function(dynamic)? onError) => + (super.noSuchMethod( + Invocation.method(#watchPosition, [onSuccess, onError]), + returnValue: 0, + returnValueForMissingStub: 0, + ) + as int); + + @override + void getCurrentPosition( + void Function(double, double)? onSuccess, + void Function(dynamic)? onError, { + int? timeoutMs = 30000, + }) => super.noSuchMethod( + Invocation.method(#getCurrentPosition, [onSuccess, onError], {#timeoutMs: timeoutMs}), + returnValueForMissingStub: null, + ); + + @override + void clearWatch(int? watchId) => super.noSuchMethod( + Invocation.method(#clearWatch, [watchId]), + returnValueForMissingStub: null, + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_plugin_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_plugin_test.dart index ca1cde5334d9..e9b7cb492d2e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_plugin_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_plugin_test.dart @@ -15,7 +15,10 @@ import 'package:integration_test/integration_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -@GenerateNiceMocks(>[MockSpec()]) +@GenerateNiceMocks(>[ + MockSpec(), + MockSpec(), +]) import 'google_maps_plugin_test.mocks.dart'; /// Test GoogleMapsPlugin diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_plugin_test.mocks.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_plugin_test.mocks.dart index 94cfdf241871..5fc689e3ca39 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_plugin_test.mocks.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/integration_test/google_maps_plugin_test.mocks.dart @@ -120,6 +120,7 @@ class MockGoogleMapController extends _i1.Mock implements _i4.GoogleMapControlle _i6.ClusterManagersController? clusterManagers, _i4.TileOverlaysController? tileOverlays, _i4.GroundOverlaysController? groundOverlays, + _i4.MyLocationController? myLocation, }) => super.noSuchMethod( Invocation.method(#debugSetOverrides, [], { #createMap: createMap, @@ -132,6 +133,7 @@ class MockGoogleMapController extends _i1.Mock implements _i4.GoogleMapControlle #clusterManagers: clusterManagers, #tileOverlays: tileOverlays, #groundOverlays: groundOverlays, + #myLocation: myLocation, }), returnValueForMissingStub: null, ); @@ -297,3 +299,57 @@ class MockGoogleMapController extends _i1.Mock implements _i4.GoogleMapControlle void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), returnValueForMissingStub: null); } + +/// A class which mocks [MyLocationController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockMyLocationController extends _i1.Mock implements _i4.MyLocationController { + @override + set myLocationButton(_i4.MyLocationButton? value) => super.noSuchMethod( + Invocation.setter(#myLocationButton, value), + returnValueForMissingStub: null, + ); + + @override + _i3.Future displayAndWatchMyLocation( + _i4.MarkersController? markersController, + ) => + (super.noSuchMethod( + Invocation.method(#displayAndWatchMyLocation, [markersController]), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) + as _i3.Future); + + @override + _i3.Future centerMyCurrentLocation(_i4.GoogleMapController? controller) => + (super.noSuchMethod( + Invocation.method(#centerMyCurrentLocation, [controller]), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) + as _i3.Future); + + @override + void addMyLocationButton(_i5.Map? map, _i4.GoogleMapController? controller) => super.noSuchMethod( + Invocation.method(#addMyLocationButton, [map, controller]), + returnValueForMissingStub: null, + ); + + @override + void removeMyLocationButton(_i5.Map? map) => super.noSuchMethod( + Invocation.method(#removeMyLocationButton, [map]), + returnValueForMissingStub: null, + ); + + @override + void removeBlueDot(_i4.MarkersController? markersController) => + super.noSuchMethod( + Invocation.method(#removeBlueDot, [markersController]), + returnValueForMissingStub: null, + ); + + @override + void dispose() => + super.noSuchMethod(Invocation.method(#dispose, []), returnValueForMissingStub: null); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/pubspec.yaml index b5e0c77bbe65..7424ecea40dc 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/latest/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/latest/pubspec.yaml @@ -28,7 +28,11 @@ flutter: assets: - assets/ +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins dependency_overrides: + google_maps_flutter: + { path: ../../../../../packages/google_maps_flutter/google_maps_flutter } # Override the google_maps_flutter dependency on google_maps_flutter_web. # TODO(ditman): Unwind the circular dependency. This will create problems # if we need to make a breaking change to google_maps_flutter_web. 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 0413faaf104d..e163ca4ab295 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 @@ -33,6 +33,7 @@ import 'src/utils.dart'; part 'src/circle.dart'; part 'src/circles.dart'; part 'src/convert.dart'; +part 'src/geolocation_api.dart'; part 'src/google_maps_controller.dart'; part 'src/google_maps_flutter_web.dart'; part 'src/ground_overlay.dart'; @@ -41,6 +42,7 @@ part 'src/heatmap.dart'; part 'src/heatmaps.dart'; part 'src/marker.dart'; part 'src/markers.dart'; +part 'src/my_location.dart'; part 'src/overlay.dart'; part 'src/overlays.dart'; part 'src/polygon.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/geolocation_api.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/geolocation_api.dart new file mode 100644 index 000000000000..a7f40c3e46bb --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/geolocation_api.dart @@ -0,0 +1,71 @@ +// 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.dart'; + +/// A pure-Dart interface that Mockito can easily mock. +abstract class GeolocationApi { + /// Watches the current position and calls [onSuccess] with the coordinates whenever they change. + /// + /// [onError] is called if there is an error while watching the position. + int watchPosition( + void Function(double latitude, double longitude) onSuccess, + void Function(dynamic error) onError, + ); + + /// Fetches the current position and calls [onSuccess] with the coordinates. + /// + /// [onError] is called if there is an error while fetching the position. + /// + /// [timeoutMs] specifies the maximum time in milliseconds + void getCurrentPosition( + void Function(double latitude, double longitude) onSuccess, + void Function(dynamic error) onError, { + int timeoutMs = 30000, + }); + + /// Stops watching the position for the given [watchId]. + void clearWatch(int watchId); +} + +/// The real implementation that uses package:web. +class WebGeolocationApi implements GeolocationApi { + final web.Geolocation _geolocation = web.window.navigator.geolocation; + + @override + int watchPosition( + void Function(double latitude, double longitude) onSuccess, + void Function(dynamic error) onError, + ) { + return _geolocation.watchPosition( + (web.GeolocationPosition location) { + onSuccess(location.coords.latitude, location.coords.longitude); + }.toJS, + (web.GeolocationPositionError error) { + onError(error); + }.toJS, + web.PositionOptions(), + ); + } + + @override + void getCurrentPosition( + void Function(double latitude, double longitude) onSuccess, + void Function(dynamic error) onError, { + int timeoutMs = 30000, + }) { + _geolocation.getCurrentPosition( + (web.GeolocationPosition location) { + onSuccess(location.coords.latitude, location.coords.longitude); + }.toJS, + (web.GeolocationPositionError error) { + onError(error); + }.toJS, + web.PositionOptions(timeout: timeoutMs), + ); + } + + @override + void clearWatch(int watchId) => _geolocation.clearWatch(watchId); +} 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 13e9054d9c7c..e901f95b1334 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 @@ -37,6 +37,7 @@ class GoogleMapController { _heatmapsController = HeatmapsController(); _polygonsController = PolygonsController(stream: _streamController); _polylinesController = PolylinesController(stream: _streamController); + _myLocationController = MyLocationController(geolocationApi: WebGeolocationApi()); // Check if all markers are of the same type. Mixing marker types is not // allowed. @@ -170,6 +171,7 @@ class GoogleMapController { ClusterManagersController? _clusterManagersController; TileOverlaysController? _tileOverlaysController; GroundOverlaysController? _groundOverlaysController; + MyLocationController? _myLocationController; StreamSubscription? _onClickSubscription; StreamSubscription? _onRightClickSubscription; @@ -203,6 +205,7 @@ class GoogleMapController { ClusterManagersController? clusterManagers, TileOverlaysController? tileOverlays, GroundOverlaysController? groundOverlays, + MyLocationController? myLocation, }) { _overrideCreateMap = createMap; _overrideSetOptions = setOptions; @@ -214,6 +217,7 @@ class GoogleMapController { _clusterManagersController = clusterManagers ?? _clusterManagersController; _tileOverlaysController = tileOverlays ?? _tileOverlaysController; _groundOverlaysController = groundOverlays ?? _groundOverlaysController; + _myLocationController = myLocation ?? _myLocationController; } DebugCreateMapFunction? _overrideCreateMap; @@ -276,6 +280,7 @@ class GoogleMapController { // Now attach the geometry, traffic and any other layers... _renderInitialGeometry(); _setTrafficLayer(map, _lastMapConfiguration.trafficEnabled ?? false); + _renderMyLocation(map, _lastMapConfiguration); } // Funnels map gmap events into the plugin's stream controller. @@ -430,6 +435,7 @@ class GoogleMapController { _setOptions(newOptions); _setTrafficLayer(_googleMap!, newConfiguration.trafficEnabled ?? false); + _renderMyLocation(_googleMap!, newConfiguration); } /// Updates the map options with a new list of [styles]. @@ -610,6 +616,25 @@ class GoogleMapController { _markersController?.hideMarkerInfoWindow(markerId); } + /// Render my location + Future _renderMyLocation(gmaps.Map map, MapConfiguration mapConfiguration) async { + if (mapConfiguration.myLocationEnabled != true) { + _myLocationController?.removeMyLocationButton(map); + _myLocationController?.removeBlueDot(_markersController!); + return; + } + + await _myLocationController?.displayAndWatchMyLocation(_markersController!); + await _myLocationController?.centerMyCurrentLocation(this); + + if (mapConfiguration.myLocationButtonEnabled != true) { + _myLocationController?.removeMyLocationButton(map); + return; + } + + _myLocationController?.addMyLocationButton(map, this); + } + /// Returns true if the [InfoWindow] of the marker identified by [MarkerId] is shown. bool isInfoWindowShown(MarkerId markerId) { return _markersController?.isInfoWindowShown(markerId) ?? false; @@ -648,6 +673,8 @@ class GoogleMapController { _onIdleSubscription?.cancel(); _onIdleSubscription = null; _streamController.close(); + _myLocationController?.dispose(); + _myLocationController = null; } } diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart index 863dcfad514c..c665e9278d1a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_inspector_web.dart @@ -124,7 +124,7 @@ class GoogleMapsInspectorWeb extends GoogleMapsInspectorPlatform { @override Future isMyLocationButtonEnabled({required int mapId}) async { - return false; // My Location widget not supported on the web + return _configurationProvider(mapId).myLocationButtonEnabled ?? false; } @override 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 000000000000..ad3b00755ba9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/my_location.dart @@ -0,0 +1,260 @@ +// 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.dart'; + +const _kMyLocationButtonId = 'my_location_button'; +const _kMyLocationBlueDot = 'my_location_blue_dot'; + +/// This class manages the current location and the my location button. +class MyLocationController { + /// Creates a [MyLocationController] with the given [GeolocationApi]. + MyLocationController({required GeolocationApi geolocationApi}) : _geolocationApi = geolocationApi; + + final GeolocationApi _geolocationApi; + + /// The my location button + MyLocationButton? myLocationButton; + LatLng? _lastKnownLocation; + int? _watchId; + + /// Watch current location and update blue dot + Future displayAndWatchMyLocation( + MarkersController markersController, + ) async { + final Marker marker = await _createBlueDotMarker(); + + if (_lastKnownLocation != null) { + _setBlueDotMarker(markersController, marker.copyWith(positionParam: _lastKnownLocation)); + } + + _watchId = _geolocationApi.watchPosition((double latitude, double longitude) { + _lastKnownLocation = LatLng(latitude, longitude); + + // TODO(Zubii12): 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); + + _setBlueDotMarker(markersController, markerUpdate); + }, (dynamic error) => myLocationButton?.doneAnimation()); + } + + void _setBlueDotMarker(MarkersController markersController, Marker marker) { + if (markersController.markers.containsKey(marker.markerId)) { + markersController.changeMarkers({marker}); + } else { + markersController.addMarkers({marker}); + } + } + + /// Get current location + Future _getCurrentLocation() async { + if (_lastKnownLocation != null) { + return _lastKnownLocation; + } + + final completer = Completer(); + + _geolocationApi.getCurrentPosition( + (double latitude, double longitude) { + if (completer.isCompleted) { + return; + } + final latLng = LatLng(latitude, longitude); + _lastKnownLocation = latLng; + completer.complete(latLng); + }, + (dynamic error) { + if (completer.isCompleted) { + return; + } + completer.complete(null); + }, + ); + + try { + return await completer.future.timeout(const Duration(seconds: 31)); + } on TimeoutException { + return null; + } + } + + /// Find and move to current location + Future centerMyCurrentLocation(GoogleMapController controller) async { + try { + final LatLng? location = await _getCurrentLocation(); + + if (location != null) { + await controller.moveCamera(CameraUpdate.newLatLng(location)); + } + myLocationButton?.doneAnimation(); + } catch (e) { + myLocationButton?.disableBtn(); + } + } + + /// Add my location to map + void addMyLocationButton(gmaps.Map map, GoogleMapController controller) { + myLocationButton = MyLocationButton(); + myLocationButton?.addClickListener((_) => centerMyCurrentLocation(controller)); + map.addListener( + 'dragend', + () { + myLocationButton?.resetAnimation(); + }.toJS, + ); + map.addListener( + 'center_changed', + () { + myLocationButton?.resetAnimation(); + }.toJS, + ); + if (myLocationButton != null) { + map.controls[gmaps.ControlPosition.RIGHT_BOTTOM as int].push(myLocationButton!.getButton); + } + } + + /// Remove my location button from map + void removeMyLocationButton(gmaps.Map map) { + if (myLocationButton != null) { + map.controls[gmaps.ControlPosition.RIGHT_BOTTOM as int].pop(); + myLocationButton = null; + } + } + + /// Remove blue dot from map + void removeBlueDot(MarkersController markersController) { + const markerId = MarkerId(_kMyLocationBlueDot); + + if (markersController.markers.containsKey(markerId)) { + markersController.removeMarkers({markerId}); + } + } + + /// Create blue dot marker + Future _createBlueDotMarker() async { + final BitmapDescriptor icon = await BitmapDescriptor.fromAssetImage( + const ImageConfiguration(size: Size(18, 18)), + 'assets/blue-dot.png', + package: 'google_maps_flutter_web', + ); + + return Marker(markerId: const MarkerId(_kMyLocationBlueDot), icon: icon, zIndex: .5); + } + + /// Dispose the controller and stop watching the position + void dispose() { + if (_watchId == null) { + return; + } + + _geolocationApi.clearWatch(_watchId!); + _watchId = null; + } +} + +/// This class support create my location button & handle animation +class MyLocationButton { + /// Add css and create my location button + MyLocationButton() { + _addCss(); + _createButton(); + } + + late web.HTMLButtonElement _btnChild; + late web.HTMLDivElement _imageChild; + late web.HTMLDivElement _controlDiv; + + /// Add animation css + void _addCss() { + final styleElement = web.HTMLStyleElement(); + web.document.head?.append(styleElement); + final web.CSSStyleSheet? sheet = styleElement.sheet; + var 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 = web.HTMLDivElement(); + + _controlDiv.style.marginRight = '10px'; + + _btnChild = web.HTMLButtonElement(); + _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 = web.HTMLDivElement(); + _imageChild.style.width = '24px'; + _imageChild.style.height = '24px'; + _imageChild.style.backgroundImage = + 'url(${web.window.location.href.replaceAll('/#', '')}/assets/packages/google_maps_flutter_web/assets/my_location-sprite-2x.png)'; + _imageChild.style.backgroundSize = '240px 24px'; + _imageChild.style.backgroundPosition = '0px 0px'; + _imageChild.style.backgroundRepeat = 'no-repeat'; + _imageChild.id = _kMyLocationButtonId; + _btnChild.append(_imageChild); + } + + /// Get button element + web.HTMLElement get getButton => _controlDiv; + + /// Add click listener + void addClickListener(void Function(web.Event)? listener) { + _btnChild.addEventListener('click', listener?.toJS); + } + + /// 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.classList.add('waiting'); + } + + /// Done animation + void doneAnimation() { + if (_btnChild.disabled) { + return; + } + _imageChild.classList.remove('waiting'); + _imageChild.style.backgroundPosition = '-192px 0px'; + } + + /// Disable button + void disableBtn() { + _btnChild.disabled = true; + _imageChild.classList.remove('waiting'); + _imageChild.style.backgroundPosition = '-24px 0px'; + } + + /// Check button disabled or enabled + bool isDisabled() => _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 9a19bffc4e88..c11ba1fac963 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/packages/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.6.2+3 +version: 0.6.3 environment: sdk: ^3.10.0 @@ -15,6 +15,8 @@ flutter: web: pluginClass: GoogleMapsPlugin fileName: google_maps_flutter_web.dart + assets: + - assets/ dependencies: collection: ^1.16.0 @@ -40,3 +42,11 @@ topics: # The example deliberately includes limited-use secrets. false_secrets: - /example/**/web/index.html + +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_maps_flutter_platform_interface: + { + path: /Users/zubii/Projects/packages/packages/google_maps_flutter/google_maps_flutter_platform_interface, + }