| // 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. |
| |
| import 'dart:typed_data'; |
| |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:google_maps_flutter/google_maps_flutter.dart'; |
| |
| class FakePlatformGoogleMap { |
| FakePlatformGoogleMap(int id, Map<dynamic, dynamic> params) |
| : cameraPosition = |
| CameraPosition.fromMap(params['initialCameraPosition']), |
| channel = MethodChannel('plugins.flutter.io/google_maps_$id') { |
| _ambiguate(TestDefaultBinaryMessengerBinding.instance)! |
| .defaultBinaryMessenger |
| .setMockMethodCallHandler(channel, onMethodCall); |
| updateOptions(params['options'] as Map<dynamic, dynamic>); |
| updateMarkers(params); |
| updatePolygons(params); |
| updatePolylines(params); |
| updateCircles(params); |
| updateTileOverlays(Map.castFrom<dynamic, dynamic, String, dynamic>(params)); |
| } |
| |
| MethodChannel channel; |
| |
| CameraPosition? cameraPosition; |
| |
| bool? compassEnabled; |
| |
| bool? mapToolbarEnabled; |
| |
| CameraTargetBounds? cameraTargetBounds; |
| |
| MapType? mapType; |
| |
| MinMaxZoomPreference? minMaxZoomPreference; |
| |
| bool? rotateGesturesEnabled; |
| |
| bool? scrollGesturesEnabled; |
| |
| bool? tiltGesturesEnabled; |
| |
| bool? zoomGesturesEnabled; |
| |
| bool? zoomControlsEnabled; |
| |
| bool? liteModeEnabled; |
| |
| bool? trackCameraPosition; |
| |
| bool? myLocationEnabled; |
| |
| bool? trafficEnabled; |
| |
| bool? buildingsEnabled; |
| |
| bool? myLocationButtonEnabled; |
| |
| List<dynamic>? padding; |
| |
| Set<MarkerId> markerIdsToRemove = <MarkerId>{}; |
| |
| Set<Marker> markersToAdd = <Marker>{}; |
| |
| Set<Marker> markersToChange = <Marker>{}; |
| |
| Set<PolygonId> polygonIdsToRemove = <PolygonId>{}; |
| |
| Set<Polygon> polygonsToAdd = <Polygon>{}; |
| |
| Set<Polygon> polygonsToChange = <Polygon>{}; |
| |
| Set<PolylineId> polylineIdsToRemove = <PolylineId>{}; |
| |
| Set<Polyline> polylinesToAdd = <Polyline>{}; |
| |
| Set<Polyline> polylinesToChange = <Polyline>{}; |
| |
| Set<CircleId> circleIdsToRemove = <CircleId>{}; |
| |
| Set<Circle> circlesToAdd = <Circle>{}; |
| |
| Set<Circle> circlesToChange = <Circle>{}; |
| |
| Set<TileOverlayId> tileOverlayIdsToRemove = <TileOverlayId>{}; |
| |
| Set<TileOverlay> tileOverlaysToAdd = <TileOverlay>{}; |
| |
| Set<TileOverlay> tileOverlaysToChange = <TileOverlay>{}; |
| |
| Future<dynamic> onMethodCall(MethodCall call) { |
| switch (call.method) { |
| case 'map#update': |
| final Map<String, Object?> arguments = |
| (call.arguments as Map<Object?, Object?>).cast<String, Object?>(); |
| updateOptions(arguments['options']! as Map<dynamic, dynamic>); |
| return Future<void>.sync(() {}); |
| case 'markers#update': |
| updateMarkers(call.arguments as Map<dynamic, dynamic>?); |
| return Future<void>.sync(() {}); |
| case 'polygons#update': |
| updatePolygons(call.arguments as Map<dynamic, dynamic>?); |
| return Future<void>.sync(() {}); |
| case 'polylines#update': |
| updatePolylines(call.arguments as Map<dynamic, dynamic>?); |
| return Future<void>.sync(() {}); |
| case 'tileOverlays#update': |
| updateTileOverlays(Map.castFrom<dynamic, dynamic, String, dynamic>( |
| call.arguments as Map<dynamic, dynamic>)); |
| return Future<void>.sync(() {}); |
| case 'circles#update': |
| updateCircles(call.arguments as Map<dynamic, dynamic>?); |
| return Future<void>.sync(() {}); |
| default: |
| return Future<void>.sync(() {}); |
| } |
| } |
| |
| void updateMarkers(Map<dynamic, dynamic>? markerUpdates) { |
| if (markerUpdates == null) { |
| return; |
| } |
| markersToAdd = _deserializeMarkers(markerUpdates['markersToAdd']); |
| markerIdsToRemove = _deserializeMarkerIds( |
| markerUpdates['markerIdsToRemove'] as List<dynamic>?); |
| markersToChange = _deserializeMarkers(markerUpdates['markersToChange']); |
| } |
| |
| Set<MarkerId> _deserializeMarkerIds(List<dynamic>? markerIds) { |
| if (markerIds == null) { |
| return <MarkerId>{}; |
| } |
| return markerIds |
| .map((dynamic markerId) => MarkerId(markerId as String)) |
| .toSet(); |
| } |
| |
| Set<Marker> _deserializeMarkers(dynamic markers) { |
| if (markers == null) { |
| return <Marker>{}; |
| } |
| final List<dynamic> markersData = markers as List<dynamic>; |
| final Set<Marker> result = <Marker>{}; |
| for (final Map<dynamic, dynamic> markerData |
| in markersData.cast<Map<dynamic, dynamic>>()) { |
| final String markerId = markerData['markerId'] as String; |
| final double alpha = markerData['alpha'] as double; |
| final bool draggable = markerData['draggable'] as bool; |
| final bool visible = markerData['visible'] as bool; |
| |
| final dynamic infoWindowData = markerData['infoWindow']; |
| InfoWindow infoWindow = InfoWindow.noText; |
| if (infoWindowData != null) { |
| final Map<dynamic, dynamic> infoWindowMap = |
| infoWindowData as Map<dynamic, dynamic>; |
| infoWindow = InfoWindow( |
| title: infoWindowMap['title'] as String?, |
| snippet: infoWindowMap['snippet'] as String?, |
| ); |
| } |
| |
| result.add(Marker( |
| markerId: MarkerId(markerId), |
| draggable: draggable, |
| visible: visible, |
| infoWindow: infoWindow, |
| alpha: alpha, |
| )); |
| } |
| |
| return result; |
| } |
| |
| void updatePolygons(Map<dynamic, dynamic>? polygonUpdates) { |
| if (polygonUpdates == null) { |
| return; |
| } |
| polygonsToAdd = _deserializePolygons(polygonUpdates['polygonsToAdd']); |
| polygonIdsToRemove = _deserializePolygonIds( |
| polygonUpdates['polygonIdsToRemove'] as List<dynamic>?); |
| polygonsToChange = _deserializePolygons(polygonUpdates['polygonsToChange']); |
| } |
| |
| Set<PolygonId> _deserializePolygonIds(List<dynamic>? polygonIds) { |
| if (polygonIds == null) { |
| return <PolygonId>{}; |
| } |
| return polygonIds |
| .map((dynamic polygonId) => PolygonId(polygonId as String)) |
| .toSet(); |
| } |
| |
| Set<Polygon> _deserializePolygons(dynamic polygons) { |
| if (polygons == null) { |
| return <Polygon>{}; |
| } |
| final List<dynamic> polygonsData = polygons as List<dynamic>; |
| final Set<Polygon> result = <Polygon>{}; |
| for (final Map<dynamic, dynamic> polygonData |
| in polygonsData.cast<Map<dynamic, dynamic>>()) { |
| final String polygonId = polygonData['polygonId'] as String; |
| final bool visible = polygonData['visible'] as bool; |
| final bool geodesic = polygonData['geodesic'] as bool; |
| final List<LatLng> points = |
| _deserializePoints(polygonData['points'] as List<dynamic>); |
| final List<List<LatLng>> holes = |
| _deserializeHoles(polygonData['holes'] as List<dynamic>); |
| |
| result.add(Polygon( |
| polygonId: PolygonId(polygonId), |
| visible: visible, |
| geodesic: geodesic, |
| points: points, |
| holes: holes, |
| )); |
| } |
| |
| return result; |
| } |
| |
| // Converts a list of points expressed as two-element lists of doubles into |
| // a list of `LatLng`s. All list items are assumed to be non-null. |
| List<LatLng> _deserializePoints(List<dynamic> points) { |
| return points.map<LatLng>((dynamic item) { |
| final List<Object?> list = item as List<Object?>; |
| return LatLng(list[0]! as double, list[1]! as double); |
| }).toList(); |
| } |
| |
| List<List<LatLng>> _deserializeHoles(List<dynamic> holes) { |
| return holes.map<List<LatLng>>((dynamic hole) { |
| return _deserializePoints(hole as List<dynamic>); |
| }).toList(); |
| } |
| |
| void updatePolylines(Map<dynamic, dynamic>? polylineUpdates) { |
| if (polylineUpdates == null) { |
| return; |
| } |
| polylinesToAdd = _deserializePolylines(polylineUpdates['polylinesToAdd']); |
| polylineIdsToRemove = _deserializePolylineIds( |
| polylineUpdates['polylineIdsToRemove'] as List<dynamic>?); |
| polylinesToChange = |
| _deserializePolylines(polylineUpdates['polylinesToChange']); |
| } |
| |
| Set<PolylineId> _deserializePolylineIds(List<dynamic>? polylineIds) { |
| if (polylineIds == null) { |
| return <PolylineId>{}; |
| } |
| return polylineIds |
| .map((dynamic polylineId) => PolylineId(polylineId as String)) |
| .toSet(); |
| } |
| |
| Set<Polyline> _deserializePolylines(dynamic polylines) { |
| if (polylines == null) { |
| return <Polyline>{}; |
| } |
| final List<dynamic> polylinesData = polylines as List<dynamic>; |
| final Set<Polyline> result = <Polyline>{}; |
| for (final Map<dynamic, dynamic> polylineData |
| in polylinesData.cast<Map<dynamic, dynamic>>()) { |
| final String polylineId = polylineData['polylineId'] as String; |
| final bool visible = polylineData['visible'] as bool; |
| final bool geodesic = polylineData['geodesic'] as bool; |
| final List<LatLng> points = |
| _deserializePoints(polylineData['points'] as List<dynamic>); |
| |
| result.add(Polyline( |
| polylineId: PolylineId(polylineId), |
| visible: visible, |
| geodesic: geodesic, |
| points: points, |
| )); |
| } |
| |
| return result; |
| } |
| |
| void updateCircles(Map<dynamic, dynamic>? circleUpdates) { |
| if (circleUpdates == null) { |
| return; |
| } |
| circlesToAdd = _deserializeCircles(circleUpdates['circlesToAdd']); |
| circleIdsToRemove = _deserializeCircleIds( |
| circleUpdates['circleIdsToRemove'] as List<dynamic>?); |
| circlesToChange = _deserializeCircles(circleUpdates['circlesToChange']); |
| } |
| |
| void updateTileOverlays(Map<String, dynamic> updateTileOverlayUpdates) { |
| if (updateTileOverlayUpdates == null) { |
| return; |
| } |
| final List<Map<dynamic, dynamic>>? tileOverlaysToAddList = |
| updateTileOverlayUpdates['tileOverlaysToAdd'] != null |
| ? List.castFrom<dynamic, Map<dynamic, dynamic>>( |
| updateTileOverlayUpdates['tileOverlaysToAdd'] as List<dynamic>) |
| : null; |
| final List<String>? tileOverlayIdsToRemoveList = |
| updateTileOverlayUpdates['tileOverlayIdsToRemove'] != null |
| ? List.castFrom<dynamic, String>( |
| updateTileOverlayUpdates['tileOverlayIdsToRemove'] |
| as List<dynamic>) |
| : null; |
| final List<Map<dynamic, dynamic>>? tileOverlaysToChangeList = |
| updateTileOverlayUpdates['tileOverlaysToChange'] != null |
| ? List.castFrom<dynamic, Map<dynamic, dynamic>>( |
| updateTileOverlayUpdates['tileOverlaysToChange'] |
| as List<dynamic>) |
| : null; |
| tileOverlaysToAdd = _deserializeTileOverlays(tileOverlaysToAddList); |
| tileOverlayIdsToRemove = |
| _deserializeTileOverlayIds(tileOverlayIdsToRemoveList); |
| tileOverlaysToChange = _deserializeTileOverlays(tileOverlaysToChangeList); |
| } |
| |
| Set<CircleId> _deserializeCircleIds(List<dynamic>? circleIds) { |
| if (circleIds == null) { |
| return <CircleId>{}; |
| } |
| return circleIds |
| .map((dynamic circleId) => CircleId(circleId as String)) |
| .toSet(); |
| } |
| |
| Set<Circle> _deserializeCircles(dynamic circles) { |
| if (circles == null) { |
| return <Circle>{}; |
| } |
| final List<dynamic> circlesData = circles as List<dynamic>; |
| final Set<Circle> result = <Circle>{}; |
| for (final Map<dynamic, dynamic> circleData |
| in circlesData.cast<Map<dynamic, dynamic>>()) { |
| final String circleId = circleData['circleId'] as String; |
| final bool visible = circleData['visible'] as bool; |
| final double radius = circleData['radius'] as double; |
| |
| result.add(Circle( |
| circleId: CircleId(circleId), |
| visible: visible, |
| radius: radius, |
| )); |
| } |
| |
| return result; |
| } |
| |
| Set<TileOverlayId> _deserializeTileOverlayIds(List<String>? tileOverlayIds) { |
| if (tileOverlayIds == null || tileOverlayIds.isEmpty) { |
| return <TileOverlayId>{}; |
| } |
| return tileOverlayIds |
| .map((String tileOverlayId) => TileOverlayId(tileOverlayId)) |
| .toSet(); |
| } |
| |
| Set<TileOverlay> _deserializeTileOverlays( |
| List<Map<dynamic, dynamic>>? tileOverlays) { |
| if (tileOverlays == null || tileOverlays.isEmpty) { |
| return <TileOverlay>{}; |
| } |
| final Set<TileOverlay> result = <TileOverlay>{}; |
| for (final Map<dynamic, dynamic> tileOverlayData in tileOverlays) { |
| final String tileOverlayId = tileOverlayData['tileOverlayId'] as String; |
| final bool fadeIn = tileOverlayData['fadeIn'] as bool; |
| final double transparency = tileOverlayData['transparency'] as double; |
| final int zIndex = tileOverlayData['zIndex'] as int; |
| final bool visible = tileOverlayData['visible'] as bool; |
| |
| result.add(TileOverlay( |
| tileOverlayId: TileOverlayId(tileOverlayId), |
| fadeIn: fadeIn, |
| transparency: transparency, |
| zIndex: zIndex, |
| visible: visible, |
| )); |
| } |
| |
| return result; |
| } |
| |
| void updateOptions(Map<dynamic, dynamic> options) { |
| if (options.containsKey('compassEnabled')) { |
| compassEnabled = options['compassEnabled'] as bool?; |
| } |
| if (options.containsKey('mapToolbarEnabled')) { |
| mapToolbarEnabled = options['mapToolbarEnabled'] as bool?; |
| } |
| if (options.containsKey('cameraTargetBounds')) { |
| final List<dynamic> boundsList = |
| options['cameraTargetBounds'] as List<dynamic>; |
| cameraTargetBounds = boundsList[0] == null |
| ? CameraTargetBounds.unbounded |
| : CameraTargetBounds(LatLngBounds.fromList(boundsList[0])); |
| } |
| if (options.containsKey('mapType')) { |
| mapType = MapType.values[options['mapType'] as int]; |
| } |
| if (options.containsKey('minMaxZoomPreference')) { |
| final List<dynamic> minMaxZoomList = |
| options['minMaxZoomPreference'] as List<dynamic>; |
| minMaxZoomPreference = MinMaxZoomPreference( |
| minMaxZoomList[0] as double?, minMaxZoomList[1] as double?); |
| } |
| if (options.containsKey('rotateGesturesEnabled')) { |
| rotateGesturesEnabled = options['rotateGesturesEnabled'] as bool?; |
| } |
| if (options.containsKey('scrollGesturesEnabled')) { |
| scrollGesturesEnabled = options['scrollGesturesEnabled'] as bool?; |
| } |
| if (options.containsKey('tiltGesturesEnabled')) { |
| tiltGesturesEnabled = options['tiltGesturesEnabled'] as bool?; |
| } |
| if (options.containsKey('trackCameraPosition')) { |
| trackCameraPosition = options['trackCameraPosition'] as bool?; |
| } |
| if (options.containsKey('zoomGesturesEnabled')) { |
| zoomGesturesEnabled = options['zoomGesturesEnabled'] as bool?; |
| } |
| if (options.containsKey('zoomControlsEnabled')) { |
| zoomControlsEnabled = options['zoomControlsEnabled'] as bool?; |
| } |
| if (options.containsKey('liteModeEnabled')) { |
| liteModeEnabled = options['liteModeEnabled'] as bool?; |
| } |
| if (options.containsKey('myLocationEnabled')) { |
| myLocationEnabled = options['myLocationEnabled'] as bool?; |
| } |
| if (options.containsKey('myLocationButtonEnabled')) { |
| myLocationButtonEnabled = options['myLocationButtonEnabled'] as bool?; |
| } |
| if (options.containsKey('trafficEnabled')) { |
| trafficEnabled = options['trafficEnabled'] as bool?; |
| } |
| if (options.containsKey('buildingsEnabled')) { |
| buildingsEnabled = options['buildingsEnabled'] as bool?; |
| } |
| if (options.containsKey('padding')) { |
| padding = options['padding'] as List<dynamic>?; |
| } |
| } |
| } |
| |
| class FakePlatformViewsController { |
| FakePlatformGoogleMap? lastCreatedView; |
| |
| Future<dynamic> fakePlatformViewsMethodHandler(MethodCall call) { |
| switch (call.method) { |
| case 'create': |
| final Map<dynamic, dynamic> args = |
| call.arguments as Map<dynamic, dynamic>; |
| final Map<dynamic, dynamic> params = |
| _decodeParams(args['params'] as Uint8List)!; |
| lastCreatedView = FakePlatformGoogleMap( |
| args['id'] as int, |
| params, |
| ); |
| return Future<int>.sync(() => 1); |
| default: |
| return Future<void>.sync(() {}); |
| } |
| } |
| |
| void reset() { |
| lastCreatedView = null; |
| } |
| } |
| |
| Map<dynamic, dynamic>? _decodeParams(Uint8List paramsMessage) { |
| final ByteBuffer buffer = paramsMessage.buffer; |
| final ByteData messageBytes = buffer.asByteData( |
| paramsMessage.offsetInBytes, |
| paramsMessage.lengthInBytes, |
| ); |
| return const StandardMessageCodec().decodeMessage(messageBytes) |
| as Map<dynamic, dynamic>?; |
| } |
| |
| /// This allows a value of type T or T? to be treated as a value of type T?. |
| /// |
| /// We use this so that APIs that have become non-nullable can still be used |
| /// with `!` and `?` on the stable branch. |
| T? _ambiguate<T>(T? value) => value; |