blob: 31392354d946d46fd46b431414a24419a05c0613 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/gestures.dart';
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
import 'package:stream_transform/stream_transform.dart';
/// An implementation of [GoogleMapsFlutterPlatform] that uses [MethodChannel] to communicate with the native code.
///
/// The `google_maps_flutter` plugin code itself never talks to the native code directly. It delegates
/// all those calls to an instance of a class that extends the GoogleMapsFlutterPlatform.
///
/// The architecture above allows for platforms that communicate differently with the native side
/// (like web) to have a common interface to extend.
///
/// This is the instance that runs when the native side talks to your Flutter app through MethodChannels,
/// like the Android and iOS platforms.
class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform {
// Keep a collection of id -> channel
// Every method call passes the int mapId
final Map<int, MethodChannel> _channels = {};
/// Accesses the MethodChannel associated to the passed mapId.
MethodChannel channel(int mapId) {
return _channels[mapId];
}
/// Initializes the platform interface with [id].
///
/// This method is called when the plugin is first initialized.
@override
Future<void> init(int mapId) {
MethodChannel channel;
if (!_channels.containsKey(mapId)) {
channel = MethodChannel('plugins.flutter.io/google_maps_$mapId');
channel.setMethodCallHandler(
(MethodCall call) => _handleMethodCall(call, mapId));
_channels[mapId] = channel;
}
return channel.invokeMethod<void>('map#waitForMap');
}
/// Dispose of the native resources.
@override
void dispose({int mapId}) {
// Noop!
}
// The controller we need to broadcast the different events coming
// from handleMethodCall.
//
// It is a `broadcast` because multiple controllers will connect to
// different stream views of this Controller.
final StreamController<MapEvent> _mapEventStreamController =
StreamController<MapEvent>.broadcast();
// Returns a filtered view of the events in the _controller, by mapId.
Stream<MapEvent> _events(int mapId) =>
_mapEventStreamController.stream.where((event) => event.mapId == mapId);
@override
Stream<CameraMoveStartedEvent> onCameraMoveStarted({@required int mapId}) {
return _events(mapId).whereType<CameraMoveStartedEvent>();
}
@override
Stream<CameraMoveEvent> onCameraMove({@required int mapId}) {
return _events(mapId).whereType<CameraMoveEvent>();
}
@override
Stream<CameraIdleEvent> onCameraIdle({@required int mapId}) {
return _events(mapId).whereType<CameraIdleEvent>();
}
@override
Stream<MarkerTapEvent> onMarkerTap({@required int mapId}) {
return _events(mapId).whereType<MarkerTapEvent>();
}
@override
Stream<InfoWindowTapEvent> onInfoWindowTap({@required int mapId}) {
return _events(mapId).whereType<InfoWindowTapEvent>();
}
@override
Stream<MarkerDragEndEvent> onMarkerDragEnd({@required int mapId}) {
return _events(mapId).whereType<MarkerDragEndEvent>();
}
@override
Stream<PolylineTapEvent> onPolylineTap({@required int mapId}) {
return _events(mapId).whereType<PolylineTapEvent>();
}
@override
Stream<PolygonTapEvent> onPolygonTap({@required int mapId}) {
return _events(mapId).whereType<PolygonTapEvent>();
}
@override
Stream<CircleTapEvent> onCircleTap({@required int mapId}) {
return _events(mapId).whereType<CircleTapEvent>();
}
@override
Stream<MapTapEvent> onTap({@required int mapId}) {
return _events(mapId).whereType<MapTapEvent>();
}
@override
Stream<MapLongPressEvent> onLongPress({@required int mapId}) {
return _events(mapId).whereType<MapLongPressEvent>();
}
Future<dynamic> _handleMethodCall(MethodCall call, int mapId) async {
switch (call.method) {
case 'camera#onMoveStarted':
_mapEventStreamController.add(CameraMoveStartedEvent(mapId));
break;
case 'camera#onMove':
_mapEventStreamController.add(CameraMoveEvent(
mapId,
CameraPosition.fromMap(call.arguments['position']),
));
break;
case 'camera#onIdle':
_mapEventStreamController.add(CameraIdleEvent(mapId));
break;
case 'marker#onTap':
_mapEventStreamController.add(MarkerTapEvent(
mapId,
MarkerId(call.arguments['markerId']),
));
break;
case 'marker#onDragEnd':
_mapEventStreamController.add(MarkerDragEndEvent(
mapId,
LatLng.fromJson(call.arguments['position']),
MarkerId(call.arguments['markerId']),
));
break;
case 'infoWindow#onTap':
_mapEventStreamController.add(InfoWindowTapEvent(
mapId,
MarkerId(call.arguments['markerId']),
));
break;
case 'polyline#onTap':
_mapEventStreamController.add(PolylineTapEvent(
mapId,
PolylineId(call.arguments['polylineId']),
));
break;
case 'polygon#onTap':
_mapEventStreamController.add(PolygonTapEvent(
mapId,
PolygonId(call.arguments['polygonId']),
));
break;
case 'circle#onTap':
_mapEventStreamController.add(CircleTapEvent(
mapId,
CircleId(call.arguments['circleId']),
));
break;
case 'map#onTap':
_mapEventStreamController.add(MapTapEvent(
mapId,
LatLng.fromJson(call.arguments['position']),
));
break;
case 'map#onLongPress':
_mapEventStreamController.add(MapLongPressEvent(
mapId,
LatLng.fromJson(call.arguments['position']),
));
break;
default:
throw MissingPluginException();
}
}
/// Updates configuration options of the map user interface.
///
/// Change listeners are notified once the update has been made on the
/// platform side.
///
/// The returned [Future] completes after listeners have been notified.
@override
Future<void> updateMapOptions(
Map<String, dynamic> optionsUpdate, {
@required int mapId,
}) {
assert(optionsUpdate != null);
return channel(mapId).invokeMethod<void>(
'map#update',
<String, dynamic>{
'options': optionsUpdate,
},
);
}
/// Updates marker configuration.
///
/// Change listeners are notified once the update has been made on the
/// platform side.
///
/// The returned [Future] completes after listeners have been notified.
@override
Future<void> updateMarkers(
MarkerUpdates markerUpdates, {
@required int mapId,
}) {
assert(markerUpdates != null);
return channel(mapId).invokeMethod<void>(
'markers#update',
markerUpdates.toJson(),
);
}
/// Updates polygon configuration.
///
/// Change listeners are notified once the update has been made on the
/// platform side.
///
/// The returned [Future] completes after listeners have been notified.
@override
Future<void> updatePolygons(
PolygonUpdates polygonUpdates, {
@required int mapId,
}) {
assert(polygonUpdates != null);
return channel(mapId).invokeMethod<void>(
'polygons#update',
polygonUpdates.toJson(),
);
}
/// Updates polyline configuration.
///
/// Change listeners are notified once the update has been made on the
/// platform side.
///
/// The returned [Future] completes after listeners have been notified.
@override
Future<void> updatePolylines(
PolylineUpdates polylineUpdates, {
@required int mapId,
}) {
assert(polylineUpdates != null);
return channel(mapId).invokeMethod<void>(
'polylines#update',
polylineUpdates.toJson(),
);
}
/// Updates circle configuration.
///
/// Change listeners are notified once the update has been made on the
/// platform side.
///
/// The returned [Future] completes after listeners have been notified.
@override
Future<void> updateCircles(
CircleUpdates circleUpdates, {
@required int mapId,
}) {
assert(circleUpdates != null);
return channel(mapId).invokeMethod<void>(
'circles#update',
circleUpdates.toJson(),
);
}
/// Starts an animated change of the map camera position.
///
/// The returned [Future] completes after the change has been started on the
/// platform side.
@override
Future<void> animateCamera(
CameraUpdate cameraUpdate, {
@required int mapId,
}) {
return channel(mapId)
.invokeMethod<void>('camera#animate', <String, dynamic>{
'cameraUpdate': cameraUpdate.toJson(),
});
}
/// Changes the map camera position.
///
/// The returned [Future] completes after the change has been made on the
/// platform side.
@override
Future<void> moveCamera(
CameraUpdate cameraUpdate, {
@required int mapId,
}) {
return channel(mapId).invokeMethod<void>('camera#move', <String, dynamic>{
'cameraUpdate': cameraUpdate.toJson(),
});
}
/// Sets the styling of the base map.
///
/// Set to `null` to clear any previous custom styling.
///
/// If problems were detected with the [mapStyle], including un-parsable
/// styling JSON, unrecognized feature type, unrecognized element type, or
/// invalid styler keys: [MapStyleException] is thrown and the current
/// style is left unchanged.
///
/// The style string can be generated using [map style tool](https://mapstyle.withgoogle.com/).
/// Also, refer [iOS](https://developers.google.com/maps/documentation/ios-sdk/style-reference)
/// and [Android](https://developers.google.com/maps/documentation/android-sdk/style-reference)
/// style reference for more information regarding the supported styles.
@override
Future<void> setMapStyle(
String mapStyle, {
@required int mapId,
}) async {
final List<dynamic> successAndError = await channel(mapId)
.invokeMethod<List<dynamic>>('map#setStyle', mapStyle);
final bool success = successAndError[0];
if (!success) {
throw MapStyleException(successAndError[1]);
}
}
/// Return the region that is visible in a map.
@override
Future<LatLngBounds> getVisibleRegion({
@required int mapId,
}) async {
final Map<String, dynamic> latLngBounds = await channel(mapId)
.invokeMapMethod<String, dynamic>('map#getVisibleRegion');
final LatLng southwest = LatLng.fromJson(latLngBounds['southwest']);
final LatLng northeast = LatLng.fromJson(latLngBounds['northeast']);
return LatLngBounds(northeast: northeast, southwest: southwest);
}
/// Return point [Map<String, int>] of the [screenCoordinateInJson] in the current map view.
///
/// A projection is used to translate between on screen location and geographic coordinates.
/// Screen location is in screen pixels (not display pixels) with respect to the top left corner
/// of the map, not necessarily of the whole screen.
@override
Future<ScreenCoordinate> getScreenCoordinate(
LatLng latLng, {
@required int mapId,
}) async {
final Map<String, int> point = await channel(mapId)
.invokeMapMethod<String, int>(
'map#getScreenCoordinate', latLng.toJson());
return ScreenCoordinate(x: point['x'], y: point['y']);
}
/// Returns [LatLng] corresponding to the [ScreenCoordinate] in the current map view.
///
/// Returned [LatLng] corresponds to a screen location. The screen location is specified in screen
/// pixels (not display pixels) relative to the top left of the map, not top left of the whole screen.
@override
Future<LatLng> getLatLng(
ScreenCoordinate screenCoordinate, {
@required int mapId,
}) async {
final List<dynamic> latLng = await channel(mapId)
.invokeMethod<List<dynamic>>(
'map#getLatLng', screenCoordinate.toJson());
return LatLng(latLng[0], latLng[1]);
}
/// Programmatically show the Info Window for a [Marker].
///
/// The `markerId` must match one of the markers on the map.
/// An invalid `markerId` triggers an "Invalid markerId" error.
///
/// * See also:
/// * [hideMarkerInfoWindow] to hide the Info Window.
/// * [isMarkerInfoWindowShown] to check if the Info Window is showing.
@override
Future<void> showMarkerInfoWindow(
MarkerId markerId, {
@required int mapId,
}) {
assert(markerId != null);
return channel(mapId).invokeMethod<void>(
'markers#showInfoWindow', <String, String>{'markerId': markerId.value});
}
/// Programmatically hide the Info Window for a [Marker].
///
/// The `markerId` must match one of the markers on the map.
/// An invalid `markerId` triggers an "Invalid markerId" error.
///
/// * See also:
/// * [showMarkerInfoWindow] to show the Info Window.
/// * [isMarkerInfoWindowShown] to check if the Info Window is showing.
@override
Future<void> hideMarkerInfoWindow(
MarkerId markerId, {
@required int mapId,
}) {
assert(markerId != null);
return channel(mapId).invokeMethod<void>(
'markers#hideInfoWindow', <String, String>{'markerId': markerId.value});
}
/// Returns `true` when the [InfoWindow] is showing, `false` otherwise.
///
/// The `markerId` must match one of the markers on the map.
/// An invalid `markerId` triggers an "Invalid markerId" error.
///
/// * See also:
/// * [showMarkerInfoWindow] to show the Info Window.
/// * [hideMarkerInfoWindow] to hide the Info Window.
@override
Future<bool> isMarkerInfoWindowShown(
MarkerId markerId, {
@required int mapId,
}) {
assert(markerId != null);
return channel(mapId).invokeMethod<bool>('markers#isInfoWindowShown',
<String, String>{'markerId': markerId.value});
}
/// Returns the current zoom level of the map
@override
Future<double> getZoomLevel({
@required int mapId,
}) {
return channel(mapId).invokeMethod<double>('map#getZoomLevel');
}
/// Returns the image bytes of the map
@override
Future<Uint8List> takeSnapshot({
@required int mapId,
}) {
return channel(mapId).invokeMethod<Uint8List>('map#takeSnapshot');
}
/// This method builds the appropriate platform view where the map
/// can be rendered.
/// The `mapId` is passed as a parameter from the framework on the
/// `onPlatformViewCreated` callback.
@override
Widget buildView(
Map<String, dynamic> creationParams,
Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers,
PlatformViewCreatedCallback onPlatformViewCreated) {
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'plugins.flutter.io/google_maps',
onPlatformViewCreated: onPlatformViewCreated,
gestureRecognizers: gestureRecognizers,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return UiKitView(
viewType: 'plugins.flutter.io/google_maps',
onPlatformViewCreated: onPlatformViewCreated,
gestureRecognizers: gestureRecognizers,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
}
return Text(
'$defaultTargetPlatform is not yet supported by the maps plugin');
}
}