diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..0d8803f93540 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +* Initial release. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/LICENSE b/packages/google_maps_flutter/google_maps_flutter_platform_interface/LICENSE new file mode 100644 index 000000000000..8940a4be1b58 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/README.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/README.md new file mode 100644 index 000000000000..6489ba39cbd8 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/README.md @@ -0,0 +1,26 @@ +# google_maps_flutter_platform_interface + +A common platform interface for the [`google_maps_flutter`][1] plugin. + +This interface allows platform-specific implementations of the `google_maps_flutter` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `google_maps_flutter`, extend +[`GoogleMapsFlutterPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`GoogleMapsFlutterPlatform` by calling +`GoogleMapsFlutterPlatform.instance = MyPlatformGoogleMapsFlutter()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../google_maps_flutter +[2]: lib/google_maps_flutter_platform_interface.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart new file mode 100644 index 000000000000..cb28b40470fd --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart @@ -0,0 +1,7 @@ +// Copyright 2018 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. + +export 'src/platform_interface/google_maps_flutter_platform.dart'; +export 'src/types/types.dart'; +export 'src/events/map_event.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart new file mode 100644 index 000000000000..c462b4b182e2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart @@ -0,0 +1,153 @@ +// Copyright 2018 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 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; + +/// Generic Event coming from the native side of Maps. +/// +/// All MapEvents contain the `mapId` that originated the event. This should +/// never be `null`. +/// +/// The `` on this event represents the type of the `value` that is +/// contained within the event. +/// +/// This class is used as a base class for all the events that might be +/// triggered from a Map, but it is never used directly as an event type. +/// +/// Do NOT instantiate new events like `MapEvent(mapId, val)` directly, +/// use a specific class instead: +/// +/// Do `class NewEvent extend MapEvent` when creating your own events. +/// See below for examples: `CameraMoveStartedEvent`, `MarkerDragEndEvent`... +/// These events are more semantic and pleasant to use than raw generics. They +/// can be (and in fact, are) filtered by the `instanceof`-operator. +/// +/// (See [MethodChannelGoogleMapsFlutter.onCameraMoveStarted], for example) +/// +/// If your event needs a `position`, alongside the `value`, do +/// `extends _PositionedMapEvent` instead. This adds a `LatLng position` +/// attribute. +/// +/// If your event *only* needs a `position`, do `extend _PositionedMapEvent` +/// do NOT `extend MapEvent`. The former lets consumers of these +/// events to access the `.position` property, rather than the more generic `.value` +/// yielded from the latter. +class MapEvent { + /// The ID of the Map this event is associated to. + final int mapId; + + /// The value wrapped by this event + final T value; + + /// Build a Map Event, that relates a mapId with a given value. + /// + /// The `mapId` is the id of the map that triggered the event. + /// `value` may be `null` in events that don't transport any meaningful data. + MapEvent(this.mapId, this.value); +} + +/// A `MapEvent` associated to a `position`. +class _PositionedMapEvent extends MapEvent { + /// The position where this event happened. + final LatLng position; + + /// Build a Positioned MapEvent, that relates a mapId and a position with a value. + /// + /// The `mapId` is the id of the map that triggered the event. + /// `value` may be `null` in events that don't transport any meaningful data. + _PositionedMapEvent(int mapId, this.position, T value) : super(mapId, value); +} + +// The following events are the ones exposed to the end user. They are semantic extensions +// of the two base classes above. +// +// These events are used to create the appropriate [Stream] objects, with information +// coming from the native side. + +/// An event fired when the Camera of a [mapId] starts moving. +class CameraMoveStartedEvent extends MapEvent { + /// Build a CameraMoveStarted Event triggered from the map represented by `mapId`. + CameraMoveStartedEvent(int mapId) : super(mapId, null); +} + +/// An event fired while the Camera of a [mapId] moves. +class CameraMoveEvent extends MapEvent { + /// Build a CameraMove Event triggered from the map represented by `mapId`. + /// + /// The `value` of this event is a [CameraPosition] object with the current position of the Camera. + CameraMoveEvent(int mapId, CameraPosition position) : super(mapId, position); +} + +/// An event fired when the Camera of a [mapId] becomes idle. +class CameraIdleEvent extends MapEvent { + /// Build a CameraIdle Event triggered from the map represented by `mapId`. + CameraIdleEvent(int mapId) : super(mapId, null); +} + +/// An event fired when a [Marker] is tapped. +class MarkerTapEvent extends MapEvent { + /// Build a MarkerTap Event triggered from the map represented by `mapId`. + /// + /// The `value` of this event is a [MarkerId] object that represents the tapped Marker. + MarkerTapEvent(int mapId, MarkerId markerId) : super(mapId, markerId); +} + +/// An event fired when an [InfoWindow] is tapped. +class InfoWindowTapEvent extends MapEvent { + /// Build an InfoWindowTap Event triggered from the map represented by `mapId`. + /// + /// The `value` of this event is a [MarkerId] object that represents the tapped InfoWindow. + InfoWindowTapEvent(int mapId, MarkerId markerId) : super(mapId, markerId); +} + +/// An event fired when a [Marker] is dragged to a new [LatLng]. +class MarkerDragEndEvent extends _PositionedMapEvent { + /// Build a MarkerDragEnd Event triggered from the map represented by `mapId`. + /// + /// The `position` on this event is the [LatLng] on which the Marker was dropped. + /// The `value` of this event is a [MarkerId] object that represents the moved Marker. + MarkerDragEndEvent(int mapId, LatLng position, MarkerId markerId) + : super(mapId, position, markerId); +} + +/// An event fired when a [Polyline] is tapped. +class PolylineTapEvent extends MapEvent { + /// Build an PolylineTap Event triggered from the map represented by `mapId`. + /// + /// The `value` of this event is a [PolylineId] object that represents the tapped Polyline. + PolylineTapEvent(int mapId, PolylineId polylineId) : super(mapId, polylineId); +} + +/// An event fired when a [Polygon] is tapped. +class PolygonTapEvent extends MapEvent { + /// Build an PolygonTap Event triggered from the map represented by `mapId`. + /// + /// The `value` of this event is a [PolygonId] object that represents the tapped Polygon. + PolygonTapEvent(int mapId, PolygonId polygonId) : super(mapId, polygonId); +} + +/// An event fired when a [Circle] is tapped. +class CircleTapEvent extends MapEvent { + /// Build an CircleTap Event triggered from the map represented by `mapId`. + /// + /// The `value` of this event is a [CircleId] object that represents the tapped Circle. + CircleTapEvent(int mapId, CircleId circleId) : super(mapId, circleId); +} + +/// An event fired when a Map is tapped. +class MapTapEvent extends _PositionedMapEvent { + /// Build an MapTap Event triggered from the map represented by `mapId`. + /// + /// The `position` of this event is the LatLng where the Map was tapped. + MapTapEvent(int mapId, LatLng position) : super(mapId, position, null); +} + +/// An event fired when a Map is long pressed. +class MapLongPressEvent extends _PositionedMapEvent { + /// Build an MapTap Event triggered from the map represented by `mapId`. + /// + /// The `position` of this event is the LatLng where the Map was long pressed. + MapLongPressEvent(int mapId, LatLng position) : super(mapId, position, null); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart new file mode 100644 index 000000000000..edbc51ab5afd --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -0,0 +1,477 @@ +// 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 _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 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('map#waitForMap'); + } + + // 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 _mapEventStreamController = + StreamController.broadcast(); + + // Returns a filtered view of the events in the _controller, by mapId. + Stream _events(int mapId) => + _mapEventStreamController.stream.where((event) => event.mapId == mapId); + + @override + Stream onCameraMoveStarted({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCameraMove({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCameraIdle({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerTap({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onInfoWindowTap({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDragEnd({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onPolylineTap({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onPolygonTap({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCircleTap({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onTap({@required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onLongPress({@required int mapId}) { + return _events(mapId).whereType(); + } + + Future _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 updateMapOptions( + Map optionsUpdate, { + @required int mapId, + }) { + assert(optionsUpdate != null); + return channel(mapId).invokeMethod( + 'map#update', + { + '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 updateMarkers( + MarkerUpdates markerUpdates, { + @required int mapId, + }) { + assert(markerUpdates != null); + return channel(mapId).invokeMethod( + '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 updatePolygons( + PolygonUpdates polygonUpdates, { + @required int mapId, + }) { + assert(polygonUpdates != null); + return channel(mapId).invokeMethod( + '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 updatePolylines( + PolylineUpdates polylineUpdates, { + @required int mapId, + }) { + assert(polylineUpdates != null); + return channel(mapId).invokeMethod( + '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 updateCircles( + CircleUpdates circleUpdates, { + @required int mapId, + }) { + assert(circleUpdates != null); + return channel(mapId).invokeMethod( + '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 animateCamera( + CameraUpdate cameraUpdate, { + @required int mapId, + }) { + return channel(mapId) + .invokeMethod('camera#animate', { + 'cameraUpdate': cameraUpdate.toJson(), + }); + } + + /// Changes the map camera position. + /// + /// The returned [Future] completes after the change has been made on the + /// platform side. + @override + Future moveCamera( + CameraUpdate cameraUpdate, { + @required int mapId, + }) { + return channel(mapId).invokeMethod('camera#move', { + '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 setMapStyle( + String mapStyle, { + @required int mapId, + }) async { + final List successAndError = await channel(mapId) + .invokeMethod>('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 getVisibleRegion({ + @required int mapId, + }) async { + final Map latLngBounds = await channel(mapId) + .invokeMapMethod('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] 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 getScreenCoordinate( + LatLng latLng, { + @required int mapId, + }) async { + final Map point = await channel(mapId) + .invokeMapMethod( + '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 getLatLng( + ScreenCoordinate screenCoordinate, { + @required int mapId, + }) async { + final List latLng = await channel(mapId) + .invokeMethod>( + '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 showMarkerInfoWindow( + MarkerId markerId, { + @required int mapId, + }) { + assert(markerId != null); + return channel(mapId).invokeMethod( + 'markers#showInfoWindow', {'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 hideMarkerInfoWindow( + MarkerId markerId, { + @required int mapId, + }) { + assert(markerId != null); + return channel(mapId).invokeMethod( + 'markers#hideInfoWindow', {'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 isMarkerInfoWindowShown( + MarkerId markerId, { + @required int mapId, + }) { + assert(markerId != null); + return channel(mapId).invokeMethod('markers#isInfoWindowShown', + {'markerId': markerId.value}); + } + + /// Returns the current zoom level of the map + @override + Future getZoomLevel({ + @required int mapId, + }) { + return channel(mapId).invokeMethod('map#getZoomLevel'); + } + + /// Returns the image bytes of the map + @override + Future takeSnapshot({ + @required int mapId, + }) { + return channel(mapId).invokeMethod('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 creationParams, + Set> 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'); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart new file mode 100644 index 000000000000..b89d3420c68e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart @@ -0,0 +1,314 @@ +// 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/widgets.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; + +import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// The interface that platform-specific implementations of `google_maps_flutter` must extend. +/// +/// Avoid `implements` of this interface. Using `implements` makes adding any new +/// methods here a breaking change for end users of your platform! +/// +/// Do `extends GoogleMapsFlutterPlatform` instead, so new methods added here are +/// inherited in your code with the default implementation (that throws at runtime), +/// rather than breaking your users at compile time. +abstract class GoogleMapsFlutterPlatform extends PlatformInterface { + /// Constructs a GoogleMapsFlutterPlatform. + GoogleMapsFlutterPlatform() : super(token: _token); + + static final Object _token = Object(); + + static GoogleMapsFlutterPlatform _instance = MethodChannelGoogleMapsFlutter(); + + /// The default instance of [GoogleMapsFlutterPlatform] to use. + /// + /// Defaults to [MethodChannelGoogleMapsFlutter]. + static GoogleMapsFlutterPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [GoogleMapsFlutterPlatform] when they register themselves. + static set instance(GoogleMapsFlutterPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// /// Initializes the platform interface with [id]. + /// + /// This method is called when the plugin is first initialized. + Future init(int mapId) { + throw UnimplementedError('init() has not been implemented.'); + } + + /// 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. + Future updateMapOptions( + Map optionsUpdate, { + @required int mapId, + }) { + throw UnimplementedError('updateMapOptions() has not been implemented.'); + } + + /// 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. + Future updateMarkers( + MarkerUpdates markerUpdates, { + @required int mapId, + }) { + throw UnimplementedError('updateMarkers() has not been implemented.'); + } + + /// 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. + Future updatePolygons( + PolygonUpdates polygonUpdates, { + @required int mapId, + }) { + throw UnimplementedError('updatePolygons() has not been implemented.'); + } + + /// 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. + Future updatePolylines( + PolylineUpdates polylineUpdates, { + @required int mapId, + }) { + throw UnimplementedError('updatePolylines() has not been implemented.'); + } + + /// 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. + Future updateCircles( + CircleUpdates circleUpdates, { + @required int mapId, + }) { + throw UnimplementedError('updateCircles() has not been implemented.'); + } + + /// Starts an animated change of the map camera position. + /// + /// The returned [Future] completes after the change has been started on the + /// platform side. + Future animateCamera( + CameraUpdate cameraUpdate, { + @required int mapId, + }) { + throw UnimplementedError('animateCamera() has not been implemented.'); + } + + /// Changes the map camera position. + /// + /// The returned [Future] completes after the change has been made on the + /// platform side. + Future moveCamera( + CameraUpdate cameraUpdate, { + @required int mapId, + }) { + throw UnimplementedError('moveCamera() has not been implemented.'); + } + + /// 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/). + Future setMapStyle( + String mapStyle, { + @required int mapId, + }) { + throw UnimplementedError('setMapStyle() has not been implemented.'); + } + + /// Return the region that is visible in a map. + Future getVisibleRegion({ + @required int mapId, + }) { + throw UnimplementedError('getVisibleRegion() has not been implemented.'); + } + + /// Return [ScreenCoordinate] of the [LatLng] 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. + Future getScreenCoordinate( + LatLng latLng, { + @required int mapId, + }) { + throw UnimplementedError('getScreenCoordinate() has not been implemented.'); + } + + /// Returns [LatLng] corresponding to the [ScreenCoordinate] 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. + Future getLatLng( + ScreenCoordinate screenCoordinate, { + @required int mapId, + }) { + throw UnimplementedError('getLatLng() has not been implemented.'); + } + + /// 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. + Future showMarkerInfoWindow( + MarkerId markerId, { + @required int mapId, + }) { + throw UnimplementedError( + 'showMarkerInfoWindow() has not been implemented.'); + } + + /// 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. + Future hideMarkerInfoWindow( + MarkerId markerId, { + @required int mapId, + }) { + throw UnimplementedError( + 'hideMarkerInfoWindow() has not been implemented.'); + } + + /// 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. + Future isMarkerInfoWindowShown( + MarkerId markerId, { + @required int mapId, + }) { + throw UnimplementedError('updateMapOptions() has not been implemented.'); + } + + /// Returns the current zoom level of the map + Future getZoomLevel({ + @required int mapId, + }) { + throw UnimplementedError('getZoomLevel() has not been implemented.'); + } + + /// Returns the image bytes of the map + Future takeSnapshot({ + @required int mapId, + }) { + throw UnimplementedError('takeSnapshot() has not been implemented.'); + } + + // The following are the 11 possible streams of data from the native side + // into the plugin + + /// The Camera started moving. + Stream onCameraMoveStarted({@required int mapId}) { + throw UnimplementedError('onCameraMoveStarted() has not been implemented.'); + } + + /// The Camera finished moving to a new [CameraPosition]. + Stream onCameraMove({@required int mapId}) { + throw UnimplementedError('onCameraMove() has not been implemented.'); + } + + /// The Camera is now idle. + Stream onCameraIdle({@required int mapId}) { + throw UnimplementedError('onCameraMove() has not been implemented.'); + } + + /// A [Marker] has been tapped. + Stream onMarkerTap({@required int mapId}) { + throw UnimplementedError('onMarkerTap() has not been implemented.'); + } + + /// An [InfoWindow] has been tapped. + Stream onInfoWindowTap({@required int mapId}) { + throw UnimplementedError('onInfoWindowTap() has not been implemented.'); + } + + /// A [Marker] has been dragged to a different [LatLng] position. + Stream onMarkerDragEnd({@required int mapId}) { + throw UnimplementedError('onMarkerDragEnd() has not been implemented.'); + } + + /// A [Polyline] has been tapped. + Stream onPolylineTap({@required int mapId}) { + throw UnimplementedError('onPolylineTap() has not been implemented.'); + } + + /// A [Polygon] has been tapped. + Stream onPolygonTap({@required int mapId}) { + throw UnimplementedError('onPolygonTap() has not been implemented.'); + } + + /// A [Circle] has been tapped. + Stream onCircleTap({@required int mapId}) { + throw UnimplementedError('onCircleTap() has not been implemented.'); + } + + /// A Map has been tapped at a certain [LatLng]. + Stream onTap({@required int mapId}) { + throw UnimplementedError('onTap() has not been implemented.'); + } + + /// A Map has been long-pressed at a certain [LatLng]. + Stream onLongPress({@required int mapId}) { + throw UnimplementedError('onLongPress() has not been implemented.'); + } + + /// Returns a widget displaying the map view + Widget buildView( + Map creationParams, + Set> gestureRecognizers, + PlatformViewCreatedCallback onPlatformViewCreated) { + throw UnimplementedError('buildView() has not been implemented.'); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart new file mode 100644 index 000000000000..40581b43e065 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart @@ -0,0 +1,116 @@ +// Copyright 2018 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' show Future; +import 'dart:typed_data' show Uint8List; + +import 'package:flutter/material.dart' + show ImageConfiguration, AssetImage, AssetBundleImageKey; +import 'package:flutter/services.dart' show AssetBundle; + +/// Defines a bitmap image. For a marker, this class can be used to set the +/// image of the marker icon. For a ground overlay, it can be used to set the +/// image to place on the surface of the earth. +class BitmapDescriptor { + const BitmapDescriptor._(this._json); + + /// Convenience hue value representing red. + static const double hueRed = 0.0; + + /// Convenience hue value representing orange. + static const double hueOrange = 30.0; + + /// Convenience hue value representing yellow. + static const double hueYellow = 60.0; + + /// Convenience hue value representing green. + static const double hueGreen = 120.0; + + /// Convenience hue value representing cyan. + static const double hueCyan = 180.0; + + /// Convenience hue value representing azure. + static const double hueAzure = 210.0; + + /// Convenience hue value representing blue. + static const double hueBlue = 240.0; + + /// Convenience hue value representing violet. + static const double hueViolet = 270.0; + + /// Convenience hue value representing magenta. + static const double hueMagenta = 300.0; + + /// Convenience hue value representing rose. + static const double hueRose = 330.0; + + /// Creates a BitmapDescriptor that refers to the default marker image. + static const BitmapDescriptor defaultMarker = + BitmapDescriptor._(['defaultMarker']); + + /// Creates a BitmapDescriptor that refers to a colorization of the default + /// marker image. For convenience, there is a predefined set of hue values. + /// See e.g. [hueYellow]. + static BitmapDescriptor defaultMarkerWithHue(double hue) { + assert(0.0 <= hue && hue < 360.0); + return BitmapDescriptor._(['defaultMarker', hue]); + } + + /// Creates a BitmapDescriptor using the name of a bitmap image in the assets + /// directory. + /// + /// Use [fromAssetImage]. This method does not respect the screen dpi when + /// picking an asset image. + @Deprecated("Use fromAssetImage instead") + static BitmapDescriptor fromAsset(String assetName, {String package}) { + if (package == null) { + return BitmapDescriptor._(['fromAsset', assetName]); + } else { + return BitmapDescriptor._(['fromAsset', assetName, package]); + } + } + + /// Creates a [BitmapDescriptor] from an asset image. + /// + /// Asset images in flutter are stored per: + /// https://flutter.dev/docs/development/ui/assets-and-images#declaring-resolution-aware-image-assets + /// This method takes into consideration various asset resolutions + /// and scales the images to the right resolution depending on the dpi. + /// Set `mipmaps` to false to load the exact dpi version of the image, `mipmap` is true by default. + static Future fromAssetImage( + ImageConfiguration configuration, + String assetName, { + AssetBundle bundle, + String package, + bool mipmaps = true, + }) async { + if (!mipmaps && configuration.devicePixelRatio != null) { + return BitmapDescriptor._([ + 'fromAssetImage', + assetName, + configuration.devicePixelRatio, + ]); + } + final AssetImage assetImage = + AssetImage(assetName, package: package, bundle: bundle); + final AssetBundleImageKey assetBundleImageKey = + await assetImage.obtainKey(configuration); + return BitmapDescriptor._([ + 'fromAssetImage', + assetBundleImageKey.name, + assetBundleImageKey.scale, + ]); + } + + /// Creates a BitmapDescriptor using an array of bytes that must be encoded + /// as PNG. + static BitmapDescriptor fromBytes(Uint8List byteData) { + return BitmapDescriptor._(['fromBytes', byteData]); + } + + final dynamic _json; + + /// Convert the object to a Json format. + dynamic toJson() => _json; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart new file mode 100644 index 000000000000..c20ece5d6c7c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart @@ -0,0 +1,63 @@ +// Copyright 2018 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 'types.dart'; + +/// Callback that receives updates to the camera position. +/// +/// This callback is triggered when the platform Google Map +/// registers a camera movement. +/// +/// This is used in [GoogleMap.onCameraMove]. +typedef void CameraPositionCallback(CameraPosition position); + +/// Callback function taking a single argument. +typedef void ArgumentCallback(T argument); + +/// Mutable collection of [ArgumentCallback] instances, itself an [ArgumentCallback]. +/// +/// Additions and removals happening during a single [call] invocation do not +/// change who gets a callback until the next such invocation. +/// +/// Optimized for the singleton case. +class ArgumentCallbacks { + final List> _callbacks = >[]; + + /// Callback method. Invokes the corresponding method on each callback + /// in this collection. + /// + /// The list of callbacks being invoked is computed at the start of the + /// method and is unaffected by any changes subsequently made to this + /// collection. + void call(T argument) { + final int length = _callbacks.length; + if (length == 1) { + _callbacks[0].call(argument); + } else if (0 < length) { + for (ArgumentCallback callback + in List>.from(_callbacks)) { + callback(argument); + } + } + } + + /// Adds a callback to this collection. + void add(ArgumentCallback callback) { + assert(callback != null); + _callbacks.add(callback); + } + + /// Removes a callback from this collection. + /// + /// Does nothing, if the callback was not present. + void remove(ArgumentCallback callback) { + _callbacks.remove(callback); + } + + /// Whether this collection is empty. + bool get isEmpty => _callbacks.isEmpty; + + /// Whether this collection is non-empty. + bool get isNotEmpty => _callbacks.isNotEmpty; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart new file mode 100644 index 000000000000..10ea1e98846a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart @@ -0,0 +1,197 @@ +// Copyright 2018 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:ui' show hashValues, Offset; + +import 'package:meta/meta.dart' show required; + +import 'types.dart'; + +/// The position of the map "camera", the view point from which the world is shown in the map view. +/// +/// Aggregates the camera's [target] geographical location, its [zoom] level, +/// [tilt] angle, and [bearing]. +class CameraPosition { + /// Creates a immutable representation of the [GoogleMap] camera. + /// + /// [AssertionError] is thrown if [bearing], [target], [tilt], or [zoom] are + /// null. + const CameraPosition({ + this.bearing = 0.0, + @required this.target, + this.tilt = 0.0, + this.zoom = 0.0, + }) : assert(bearing != null), + assert(target != null), + assert(tilt != null), + assert(zoom != null); + + /// The camera's bearing in degrees, measured clockwise from north. + /// + /// A bearing of 0.0, the default, means the camera points north. + /// A bearing of 90.0 means the camera points east. + final double bearing; + + /// The geographical location that the camera is pointing at. + final LatLng target; + + /// The angle, in degrees, of the camera angle from the nadir. + /// + /// A tilt of 0.0, the default and minimum supported value, means the camera + /// is directly facing the Earth. + /// + /// The maximum tilt value depends on the current zoom level. Values beyond + /// the supported range are allowed, but on applying them to a map they will + /// be silently clamped to the supported range. + final double tilt; + + /// The zoom level of the camera. + /// + /// A zoom of 0.0, the default, means the screen width of the world is 256. + /// Adding 1.0 to the zoom level doubles the screen width of the map. So at + /// zoom level 3.0, the screen width of the world is 2³x256=2048. + /// + /// Larger zoom levels thus means the camera is placed closer to the surface + /// of the Earth, revealing more detail in a narrower geographical region. + /// + /// The supported zoom level range depends on the map data and device. Values + /// beyond the supported range are allowed, but on applying them to a map they + /// will be silently clamped to the supported range. + final double zoom; + + /// Serializes [CameraPosition]. + /// + /// Mainly for internal use when calling [CameraUpdate.newCameraPosition]. + dynamic toMap() => { + 'bearing': bearing, + 'target': target.toJson(), + 'tilt': tilt, + 'zoom': zoom, + }; + + /// Deserializes [CameraPosition] from a map. + /// + /// Mainly for internal use. + static CameraPosition fromMap(dynamic json) { + if (json == null) { + return null; + } + return CameraPosition( + bearing: json['bearing'], + target: LatLng.fromJson(json['target']), + tilt: json['tilt'], + zoom: json['zoom'], + ); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (runtimeType != other.runtimeType) return false; + final CameraPosition typedOther = other; + return bearing == typedOther.bearing && + target == typedOther.target && + tilt == typedOther.tilt && + zoom == typedOther.zoom; + } + + @override + int get hashCode => hashValues(bearing, target, tilt, zoom); + + @override + String toString() => + 'CameraPosition(bearing: $bearing, target: $target, tilt: $tilt, zoom: $zoom)'; +} + +/// Defines a camera move, supporting absolute moves as well as moves relative +/// the current position. +class CameraUpdate { + CameraUpdate._(this._json); + + /// Returns a camera update that moves the camera to the specified position. + static CameraUpdate newCameraPosition(CameraPosition cameraPosition) { + return CameraUpdate._( + ['newCameraPosition', cameraPosition.toMap()], + ); + } + + /// Returns a camera update that moves the camera target to the specified + /// geographical location. + static CameraUpdate newLatLng(LatLng latLng) { + return CameraUpdate._(['newLatLng', latLng.toJson()]); + } + + /// Returns a camera update that transforms the camera so that the specified + /// geographical bounding box is centered in the map view at the greatest + /// possible zoom level. A non-zero [padding] insets the bounding box from the + /// map view's edges. The camera's new tilt and bearing will both be 0.0. + static CameraUpdate newLatLngBounds(LatLngBounds bounds, double padding) { + return CameraUpdate._([ + 'newLatLngBounds', + bounds.toJson(), + padding, + ]); + } + + /// Returns a camera update that moves the camera target to the specified + /// geographical location and zoom level. + static CameraUpdate newLatLngZoom(LatLng latLng, double zoom) { + return CameraUpdate._( + ['newLatLngZoom', latLng.toJson(), zoom], + ); + } + + /// Returns a camera update that moves the camera target the specified screen + /// distance. + /// + /// For a camera with bearing 0.0 (pointing north), scrolling by 50,75 moves + /// the camera's target to a geographical location that is 50 to the east and + /// 75 to the south of the current location, measured in screen coordinates. + static CameraUpdate scrollBy(double dx, double dy) { + return CameraUpdate._( + ['scrollBy', dx, dy], + ); + } + + /// Returns a camera update that modifies the camera zoom level by the + /// specified amount. The optional [focus] is a screen point whose underlying + /// geographical location should be invariant, if possible, by the movement. + static CameraUpdate zoomBy(double amount, [Offset focus]) { + if (focus == null) { + return CameraUpdate._(['zoomBy', amount]); + } else { + return CameraUpdate._([ + 'zoomBy', + amount, + [focus.dx, focus.dy], + ]); + } + } + + /// Returns a camera update that zooms the camera in, bringing the camera + /// closer to the surface of the Earth. + /// + /// Equivalent to the result of calling `zoomBy(1.0)`. + static CameraUpdate zoomIn() { + return CameraUpdate._(['zoomIn']); + } + + /// Returns a camera update that zooms the camera out, bringing the camera + /// further away from the surface of the Earth. + /// + /// Equivalent to the result of calling `zoomBy(-1.0)`. + static CameraUpdate zoomOut() { + return CameraUpdate._(['zoomOut']); + } + + /// Returns a camera update that sets the camera zoom level. + static CameraUpdate zoomTo(double zoom) { + return CameraUpdate._(['zoomTo', zoom]); + } + + final dynamic _json; + + /// Converts this object to something serializable in JSON. + dynamic toJson() => _json; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart new file mode 100644 index 000000000000..68bf14c36408 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart @@ -0,0 +1,55 @@ +// Copyright 2019 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 'package:meta/meta.dart' show immutable; + +import 'types.dart'; + +/// Cap that can be applied at the start or end vertex of a [Polyline]. +@immutable +class Cap { + const Cap._(this._json); + + /// Cap that is squared off exactly at the start or end vertex of a [Polyline] + /// with solid stroke pattern, equivalent to having no additional cap beyond + /// the start or end vertex. + /// + /// This is the default cap type at start and end vertices of Polylines with + /// solid stroke pattern. + static const Cap buttCap = Cap._(['buttCap']); + + /// Cap that is a semicircle with radius equal to half the stroke width, + /// centered at the start or end vertex of a [Polyline] with solid stroke + /// pattern. + static const Cap roundCap = Cap._(['roundCap']); + + /// Cap that is squared off after extending half the stroke width beyond the + /// start or end vertex of a [Polyline] with solid stroke pattern. + static const Cap squareCap = Cap._(['squareCap']); + + /// Constructs a new CustomCap with a bitmap overlay centered at the start or + /// end vertex of a [Polyline], orientated according to the direction of the line's + /// first or last edge and scaled with respect to the line's stroke width. + /// + /// CustomCap can be applied to [Polyline] with any stroke pattern. + /// + /// [bitmapDescriptor] must not be null. + /// + /// [refWidth] is the reference stroke width (in pixels) - the stroke width for which + /// the cap bitmap at its native dimension is designed. Must be positive. Default value + /// is 10 pixels. + static Cap customCapFromBitmap( + BitmapDescriptor bitmapDescriptor, { + double refWidth = 10, + }) { + assert(bitmapDescriptor != null); + assert(refWidth > 0.0); + return Cap._(['customCap', bitmapDescriptor.toJson(), refWidth]); + } + + final dynamic _json; + + /// Converts this object to something serializable in JSON. + dynamic toJson() => _json; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart new file mode 100644 index 000000000000..d1418a4c30b1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart @@ -0,0 +1,167 @@ +// Copyright 2019 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 'package:flutter/foundation.dart' show VoidCallback; +import 'package:flutter/material.dart' show Color, Colors; +import 'package:meta/meta.dart' show immutable, required; + +import 'types.dart'; + +/// Uniquely identifies a [Circle] among [GoogleMap] circles. +/// +/// This does not have to be globally unique, only unique among the list. +@immutable +class CircleId { + /// Creates an immutable identifier for a [Circle]. + CircleId(this.value) : assert(value != null); + + /// value of the [CircleId]. + final String value; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final CircleId typedOther = other; + return value == typedOther.value; + } + + @override + int get hashCode => value.hashCode; + + @override + String toString() { + return 'CircleId{value: $value}'; + } +} + +/// Draws a circle on the map. +@immutable +class Circle { + /// Creates an immutable representation of a [Circle] to draw on [GoogleMap]. + const Circle({ + @required this.circleId, + this.consumeTapEvents = false, + this.fillColor = Colors.transparent, + this.center = const LatLng(0.0, 0.0), + this.radius = 0, + this.strokeColor = Colors.black, + this.strokeWidth = 10, + this.visible = true, + this.zIndex = 0, + this.onTap, + }); + + /// Uniquely identifies a [Circle]. + final CircleId circleId; + + /// True if the [Circle] consumes tap events. + /// + /// If this is false, [onTap] callback will not be triggered. + final bool consumeTapEvents; + + /// Fill color in ARGB format, the same format used by Color. The default value is transparent (0x00000000). + final Color fillColor; + + /// Geographical location of the circle center. + final LatLng center; + + /// Radius of the circle in meters; must be positive. The default value is 0. + final double radius; + + /// Fill color in ARGB format, the same format used by Color. The default value is black (0xff000000). + final Color strokeColor; + + /// The width of the circle's outline in screen points. + /// + /// The width is constant and independent of the camera's zoom level. + /// The default value is 10. + /// Setting strokeWidth to 0 results in no stroke. + final int strokeWidth; + + /// True if the circle is visible. + final bool visible; + + /// The z-index of the circle, used to determine relative drawing order of + /// map overlays. + /// + /// Overlays are drawn in order of z-index, so that lower values means drawn + /// earlier, and thus appearing to be closer to the surface of the Earth. + final int zIndex; + + /// Callbacks to receive tap events for circle placed on this map. + final VoidCallback onTap; + + /// Creates a new [Circle] object whose values are the same as this instance, + /// unless overwritten by the specified parameters. + Circle copyWith({ + bool consumeTapEventsParam, + Color fillColorParam, + LatLng centerParam, + double radiusParam, + Color strokeColorParam, + int strokeWidthParam, + bool visibleParam, + int zIndexParam, + VoidCallback onTapParam, + }) { + return Circle( + circleId: circleId, + consumeTapEvents: consumeTapEventsParam ?? consumeTapEvents, + fillColor: fillColorParam ?? fillColor, + center: centerParam ?? center, + radius: radiusParam ?? radius, + strokeColor: strokeColorParam ?? strokeColor, + strokeWidth: strokeWidthParam ?? strokeWidth, + visible: visibleParam ?? visible, + zIndex: zIndexParam ?? zIndex, + onTap: onTapParam ?? onTap, + ); + } + + /// Creates a new [Circle] object whose values are the same as this instance. + Circle clone() => copyWith(); + + /// Converts this object to something serializable in JSON. + dynamic toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('circleId', circleId.value); + addIfPresent('consumeTapEvents', consumeTapEvents); + addIfPresent('fillColor', fillColor.value); + addIfPresent('center', center.toJson()); + addIfPresent('radius', radius); + addIfPresent('strokeColor', strokeColor.value); + addIfPresent('strokeWidth', strokeWidth); + addIfPresent('visible', visible); + addIfPresent('zIndex', zIndex); + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final Circle typedOther = other; + return circleId == typedOther.circleId && + consumeTapEvents == typedOther.consumeTapEvents && + fillColor == typedOther.fillColor && + center == typedOther.center && + radius == typedOther.radius && + strokeColor == typedOther.strokeColor && + strokeWidth == typedOther.strokeWidth && + visible == typedOther.visible && + zIndex == typedOther.zIndex; + } + + @override + int get hashCode => circleId.hashCode; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart new file mode 100644 index 000000000000..6f494423a38f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart @@ -0,0 +1,110 @@ +// Copyright 2018 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:ui' show hashValues; + +import 'package:flutter/foundation.dart' show setEquals; + +import 'types.dart'; +import 'utils/circle.dart'; + +/// [Circle] update events to be applied to the [GoogleMap]. +/// +/// Used in [GoogleMapController] when the map is updated. +// (Do not re-export) +class CircleUpdates { + /// Computes [CircleUpdates] given previous and current [Circle]s. + CircleUpdates.from(Set previous, Set current) { + if (previous == null) { + previous = Set.identity(); + } + + if (current == null) { + current = Set.identity(); + } + + final Map previousCircles = keyByCircleId(previous); + final Map currentCircles = keyByCircleId(current); + + final Set prevCircleIds = previousCircles.keys.toSet(); + final Set currentCircleIds = currentCircles.keys.toSet(); + + Circle idToCurrentCircle(CircleId id) { + return currentCircles[id]; + } + + final Set _circleIdsToRemove = + prevCircleIds.difference(currentCircleIds); + + final Set _circlesToAdd = currentCircleIds + .difference(prevCircleIds) + .map(idToCurrentCircle) + .toSet(); + + /// Returns `true` if [current] is not equals to previous one with the + /// same id. + bool hasChanged(Circle current) { + final Circle previous = previousCircles[current.circleId]; + return current != previous; + } + + final Set _circlesToChange = currentCircleIds + .intersection(prevCircleIds) + .map(idToCurrentCircle) + .where(hasChanged) + .toSet(); + + circlesToAdd = _circlesToAdd; + circleIdsToRemove = _circleIdsToRemove; + circlesToChange = _circlesToChange; + } + + /// Set of Circles to be added in this update. + Set circlesToAdd; + + /// Set of CircleIds to be removed in this update. + Set circleIdsToRemove; + + /// Set of Circles to be changed in this update. + Set circlesToChange; + + /// Converts this object to something serializable in JSON. + Map toJson() { + final Map updateMap = {}; + + void addIfNonNull(String fieldName, dynamic value) { + if (value != null) { + updateMap[fieldName] = value; + } + } + + addIfNonNull('circlesToAdd', serializeCircleSet(circlesToAdd)); + addIfNonNull('circlesToChange', serializeCircleSet(circlesToChange)); + addIfNonNull('circleIdsToRemove', + circleIdsToRemove.map((CircleId m) => m.value).toList()); + + return updateMap; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final CircleUpdates typedOther = other; + return setEquals(circlesToAdd, typedOther.circlesToAdd) && + setEquals(circleIdsToRemove, typedOther.circleIdsToRemove) && + setEquals(circlesToChange, typedOther.circlesToChange); + } + + @override + int get hashCode => + hashValues(circlesToAdd, circleIdsToRemove, circlesToChange); + + @override + String toString() { + return '_CircleUpdates{circlesToAdd: $circlesToAdd, ' + 'circleIdsToRemove: $circleIdsToRemove, ' + 'circlesToChange: $circlesToChange}'; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/joint_type.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/joint_type.dart new file mode 100644 index 000000000000..c7df0b298624 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/joint_type.dart @@ -0,0 +1,29 @@ +// Copyright 2019 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 'package:meta/meta.dart' show immutable; + +/// Joint types for [Polyline]. +@immutable +class JointType { + const JointType._(this.value); + + /// The value representing the [JointType] on the sdk. + final int value; + + /// Mitered joint, with fixed pointed extrusion equal to half the stroke width on the outside of the joint. + /// + /// Constant Value: 0 + static const JointType mitered = JointType._(0); + + /// Flat bevel on the outside of the joint. + /// + /// Constant Value: 1 + static const JointType bevel = JointType._(1); + + /// Rounded on the outside of the joint by an arc of radius equal to half the stroke width, centered at the vertex. + /// + /// Constant Value: 2 + static const JointType round = JointType._(2); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart new file mode 100644 index 000000000000..6b76a6d496ac --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart @@ -0,0 +1,129 @@ +// Copyright 2018 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:ui' show hashValues; + +import 'package:meta/meta.dart'; + +/// A pair of latitude and longitude coordinates, stored as degrees. +class LatLng { + /// Creates a geographical location specified in degrees [latitude] and + /// [longitude]. + /// + /// The latitude is clamped to the inclusive interval from -90.0 to +90.0. + /// + /// The longitude is normalized to the half-open interval from -180.0 + /// (inclusive) to +180.0 (exclusive) + const LatLng(double latitude, double longitude) + : assert(latitude != null), + assert(longitude != null), + latitude = + (latitude < -90.0 ? -90.0 : (90.0 < latitude ? 90.0 : latitude)), + longitude = (longitude + 180.0) % 360.0 - 180.0; + + /// The latitude in degrees between -90.0 and 90.0, both inclusive. + final double latitude; + + /// The longitude in degrees between -180.0 (inclusive) and 180.0 (exclusive). + final double longitude; + + /// Converts this object to something serializable in JSON. + dynamic toJson() { + return [latitude, longitude]; + } + + /// Initialize a LatLng from an \[lat, lng\] array. + static LatLng fromJson(dynamic json) { + if (json == null) { + return null; + } + return LatLng(json[0], json[1]); + } + + @override + String toString() => '$runtimeType($latitude, $longitude)'; + + @override + bool operator ==(Object o) { + return o is LatLng && o.latitude == latitude && o.longitude == longitude; + } + + @override + int get hashCode => hashValues(latitude, longitude); +} + +/// A latitude/longitude aligned rectangle. +/// +/// The rectangle conceptually includes all points (lat, lng) where +/// * lat ∈ [`southwest.latitude`, `northeast.latitude`] +/// * lng ∈ [`southwest.longitude`, `northeast.longitude`], +/// if `southwest.longitude` ≤ `northeast.longitude`, +/// * lng ∈ [-180, `northeast.longitude`] ∪ [`southwest.longitude`, 180], +/// if `northeast.longitude` < `southwest.longitude` +class LatLngBounds { + /// Creates geographical bounding box with the specified corners. + /// + /// The latitude of the southwest corner cannot be larger than the + /// latitude of the northeast corner. + LatLngBounds({@required this.southwest, @required this.northeast}) + : assert(southwest != null), + assert(northeast != null), + assert(southwest.latitude <= northeast.latitude); + + /// The southwest corner of the rectangle. + final LatLng southwest; + + /// The northeast corner of the rectangle. + final LatLng northeast; + + /// Converts this object to something serializable in JSON. + dynamic toJson() { + return [southwest.toJson(), northeast.toJson()]; + } + + /// Returns whether this rectangle contains the given [LatLng]. + bool contains(LatLng point) { + return _containsLatitude(point.latitude) && + _containsLongitude(point.longitude); + } + + bool _containsLatitude(double lat) { + return (southwest.latitude <= lat) && (lat <= northeast.latitude); + } + + bool _containsLongitude(double lng) { + if (southwest.longitude <= northeast.longitude) { + return southwest.longitude <= lng && lng <= northeast.longitude; + } else { + return southwest.longitude <= lng || lng <= northeast.longitude; + } + } + + /// Converts a list to [LatLngBounds]. + @visibleForTesting + static LatLngBounds fromList(dynamic json) { + if (json == null) { + return null; + } + return LatLngBounds( + southwest: LatLng.fromJson(json[0]), + northeast: LatLng.fromJson(json[1]), + ); + } + + @override + String toString() { + return '$runtimeType($southwest, $northeast)'; + } + + @override + bool operator ==(Object o) { + return o is LatLngBounds && + o.southwest == southwest && + o.northeast == northeast; + } + + @override + int get hashCode => hashValues(southwest, northeast); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart new file mode 100644 index 000000000000..9b57f9676334 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart @@ -0,0 +1,324 @@ +// Copyright 2018 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:ui' show hashValues, Offset; + +import 'package:flutter/foundation.dart' show ValueChanged, VoidCallback; +import 'package:meta/meta.dart' show immutable, required; + +import 'types.dart'; + +dynamic _offsetToJson(Offset offset) { + if (offset == null) { + return null; + } + return [offset.dx, offset.dy]; +} + +/// Text labels for a [Marker] info window. +class InfoWindow { + /// Creates an immutable representation of a label on for [Marker]. + const InfoWindow({ + this.title, + this.snippet, + this.anchor = const Offset(0.5, 0.0), + this.onTap, + }); + + /// Text labels specifying that no text is to be displayed. + static const InfoWindow noText = InfoWindow(); + + /// Text displayed in an info window when the user taps the marker. + /// + /// A null value means no title. + final String title; + + /// Additional text displayed below the [title]. + /// + /// A null value means no additional text. + final String snippet; + + /// The icon image point that will be the anchor of the info window when + /// displayed. + /// + /// The image point is specified in normalized coordinates: An anchor of + /// (0.0, 0.0) means the top left corner of the image. An anchor + /// of (1.0, 1.0) means the bottom right corner of the image. + final Offset anchor; + + /// onTap callback for this [InfoWindow]. + final VoidCallback onTap; + + /// Creates a new [InfoWindow] object whose values are the same as this instance, + /// unless overwritten by the specified parameters. + InfoWindow copyWith({ + String titleParam, + String snippetParam, + Offset anchorParam, + VoidCallback onTapParam, + }) { + return InfoWindow( + title: titleParam ?? title, + snippet: snippetParam ?? snippet, + anchor: anchorParam ?? anchor, + onTap: onTapParam ?? onTap, + ); + } + + dynamic _toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('title', title); + addIfPresent('snippet', snippet); + addIfPresent('anchor', _offsetToJson(anchor)); + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final InfoWindow typedOther = other; + return title == typedOther.title && + snippet == typedOther.snippet && + anchor == typedOther.anchor; + } + + @override + int get hashCode => hashValues(title.hashCode, snippet, anchor); + + @override + String toString() { + return 'InfoWindow{title: $title, snippet: $snippet, anchor: $anchor}'; + } +} + +/// Uniquely identifies a [Marker] among [GoogleMap] markers. +/// +/// This does not have to be globally unique, only unique among the list. +@immutable +class MarkerId { + /// Creates an immutable identifier for a [Marker]. + MarkerId(this.value) : assert(value != null); + + /// value of the [MarkerId]. + final String value; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final MarkerId typedOther = other; + return value == typedOther.value; + } + + @override + int get hashCode => value.hashCode; + + @override + String toString() { + return 'MarkerId{value: $value}'; + } +} + +/// Marks a geographical location on the map. +/// +/// A marker icon is drawn oriented against the device's screen rather than +/// the map's surface; that is, it will not necessarily change orientation +/// due to map rotations, tilting, or zooming. +@immutable +class Marker { + /// Creates a set of marker configuration options. + /// + /// Default marker options. + /// + /// Specifies a marker that + /// * is fully opaque; [alpha] is 1.0 + /// * uses icon bottom center to indicate map position; [anchor] is (0.5, 1.0) + /// * has default tap handling; [consumeTapEvents] is false + /// * is stationary; [draggable] is false + /// * is drawn against the screen, not the map; [flat] is false + /// * has a default icon; [icon] is `BitmapDescriptor.defaultMarker` + /// * anchors the info window at top center; [infoWindowAnchor] is (0.5, 0.0) + /// * has no info window text; [infoWindowText] is `InfoWindowText.noText` + /// * is positioned at 0, 0; [position] is `LatLng(0.0, 0.0)` + /// * has an axis-aligned icon; [rotation] is 0.0 + /// * is visible; [visible] is true + /// * is placed at the base of the drawing order; [zIndex] is 0.0 + /// * reports [onTap] events + /// * reports [onDragEnd] events + const Marker({ + @required this.markerId, + this.alpha = 1.0, + this.anchor = const Offset(0.5, 1.0), + this.consumeTapEvents = false, + this.draggable = false, + this.flat = false, + this.icon = BitmapDescriptor.defaultMarker, + this.infoWindow = InfoWindow.noText, + this.position = const LatLng(0.0, 0.0), + this.rotation = 0.0, + this.visible = true, + this.zIndex = 0.0, + this.onTap, + this.onDragEnd, + }) : assert(alpha == null || (0.0 <= alpha && alpha <= 1.0)); + + /// Uniquely identifies a [Marker]. + final MarkerId markerId; + + /// The opacity of the marker, between 0.0 and 1.0 inclusive. + /// + /// 0.0 means fully transparent, 1.0 means fully opaque. + final double alpha; + + /// The icon image point that will be placed at the [position] of the marker. + /// + /// The image point is specified in normalized coordinates: An anchor of + /// (0.0, 0.0) means the top left corner of the image. An anchor + /// of (1.0, 1.0) means the bottom right corner of the image. + final Offset anchor; + + /// True if the marker icon consumes tap events. If not, the map will perform + /// default tap handling by centering the map on the marker and displaying its + /// info window. + final bool consumeTapEvents; + + /// True if the marker is draggable by user touch events. + final bool draggable; + + /// True if the marker is rendered flatly against the surface of the Earth, so + /// that it will rotate and tilt along with map camera movements. + final bool flat; + + /// A description of the bitmap used to draw the marker icon. + final BitmapDescriptor icon; + + /// A Google Maps InfoWindow. + /// + /// The window is displayed when the marker is tapped. + final InfoWindow infoWindow; + + /// Geographical location of the marker. + final LatLng position; + + /// Rotation of the marker image in degrees clockwise from the [anchor] point. + final double rotation; + + /// True if the marker is visible. + final bool visible; + + /// The z-index of the marker, used to determine relative drawing order of + /// map overlays. + /// + /// Overlays are drawn in order of z-index, so that lower values means drawn + /// earlier, and thus appearing to be closer to the surface of the Earth. + final double zIndex; + + /// Callbacks to receive tap events for markers placed on this map. + final VoidCallback onTap; + + /// Signature reporting the new [LatLng] at the end of a drag event. + final ValueChanged onDragEnd; + + /// Creates a new [Marker] object whose values are the same as this instance, + /// unless overwritten by the specified parameters. + Marker copyWith({ + double alphaParam, + Offset anchorParam, + bool consumeTapEventsParam, + bool draggableParam, + bool flatParam, + BitmapDescriptor iconParam, + InfoWindow infoWindowParam, + LatLng positionParam, + double rotationParam, + bool visibleParam, + double zIndexParam, + VoidCallback onTapParam, + ValueChanged onDragEndParam, + }) { + return Marker( + markerId: markerId, + alpha: alphaParam ?? alpha, + anchor: anchorParam ?? anchor, + consumeTapEvents: consumeTapEventsParam ?? consumeTapEvents, + draggable: draggableParam ?? draggable, + flat: flatParam ?? flat, + icon: iconParam ?? icon, + infoWindow: infoWindowParam ?? infoWindow, + position: positionParam ?? position, + rotation: rotationParam ?? rotation, + visible: visibleParam ?? visible, + zIndex: zIndexParam ?? zIndex, + onTap: onTapParam ?? onTap, + onDragEnd: onDragEndParam ?? onDragEnd, + ); + } + + /// Creates a new [Marker] object whose values are the same as this instance. + Marker clone() => copyWith(); + + /// Converts this object to something serializable in JSON. + Map toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('markerId', markerId.value); + addIfPresent('alpha', alpha); + addIfPresent('anchor', _offsetToJson(anchor)); + addIfPresent('consumeTapEvents', consumeTapEvents); + addIfPresent('draggable', draggable); + addIfPresent('flat', flat); + addIfPresent('icon', icon?.toJson()); + addIfPresent('infoWindow', infoWindow?._toJson()); + addIfPresent('position', position?.toJson()); + addIfPresent('rotation', rotation); + addIfPresent('visible', visible); + addIfPresent('zIndex', zIndex); + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final Marker typedOther = other; + return markerId == typedOther.markerId && + alpha == typedOther.alpha && + anchor == typedOther.anchor && + consumeTapEvents == typedOther.consumeTapEvents && + draggable == typedOther.draggable && + flat == typedOther.flat && + icon == typedOther.icon && + infoWindow == typedOther.infoWindow && + position == typedOther.position && + rotation == typedOther.rotation && + visible == typedOther.visible && + zIndex == typedOther.zIndex; + } + + @override + int get hashCode => markerId.hashCode; + + @override + String toString() { + return 'Marker{markerId: $markerId, alpha: $alpha, anchor: $anchor, ' + 'consumeTapEvents: $consumeTapEvents, draggable: $draggable, flat: $flat, ' + 'icon: $icon, infoWindow: $infoWindow, position: $position, rotation: $rotation, ' + 'visible: $visible, zIndex: $zIndex, onTap: $onTap}'; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart new file mode 100644 index 000000000000..bb6ea8813ea3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart @@ -0,0 +1,110 @@ +// Copyright 2018 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:ui' show hashValues; + +import 'package:flutter/foundation.dart' show setEquals; + +import 'types.dart'; +import 'utils/marker.dart'; + +/// [Marker] update events to be applied to the [GoogleMap]. +/// +/// Used in [GoogleMapController] when the map is updated. +// (Do not re-export) +class MarkerUpdates { + /// Computes [MarkerUpdates] given previous and current [Marker]s. + MarkerUpdates.from(Set previous, Set current) { + if (previous == null) { + previous = Set.identity(); + } + + if (current == null) { + current = Set.identity(); + } + + final Map previousMarkers = keyByMarkerId(previous); + final Map currentMarkers = keyByMarkerId(current); + + final Set prevMarkerIds = previousMarkers.keys.toSet(); + final Set currentMarkerIds = currentMarkers.keys.toSet(); + + Marker idToCurrentMarker(MarkerId id) { + return currentMarkers[id]; + } + + final Set _markerIdsToRemove = + prevMarkerIds.difference(currentMarkerIds); + + final Set _markersToAdd = currentMarkerIds + .difference(prevMarkerIds) + .map(idToCurrentMarker) + .toSet(); + + /// Returns `true` if [current] is not equals to previous one with the + /// same id. + bool hasChanged(Marker current) { + final Marker previous = previousMarkers[current.markerId]; + return current != previous; + } + + final Set _markersToChange = currentMarkerIds + .intersection(prevMarkerIds) + .map(idToCurrentMarker) + .where(hasChanged) + .toSet(); + + markersToAdd = _markersToAdd; + markerIdsToRemove = _markerIdsToRemove; + markersToChange = _markersToChange; + } + + /// Set of Markers to be added in this update. + Set markersToAdd; + + /// Set of MarkerIds to be removed in this update. + Set markerIdsToRemove; + + /// Set of Markers to be changed in this update. + Set markersToChange; + + /// Converts this object to something serializable in JSON. + Map toJson() { + final Map updateMap = {}; + + void addIfNonNull(String fieldName, dynamic value) { + if (value != null) { + updateMap[fieldName] = value; + } + } + + addIfNonNull('markersToAdd', serializeMarkerSet(markersToAdd)); + addIfNonNull('markersToChange', serializeMarkerSet(markersToChange)); + addIfNonNull('markerIdsToRemove', + markerIdsToRemove.map((MarkerId m) => m.value).toList()); + + return updateMap; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final MarkerUpdates typedOther = other; + return setEquals(markersToAdd, typedOther.markersToAdd) && + setEquals(markerIdsToRemove, typedOther.markerIdsToRemove) && + setEquals(markersToChange, typedOther.markersToChange); + } + + @override + int get hashCode => + hashValues(markersToAdd, markerIdsToRemove, markersToChange); + + @override + String toString() { + return '_MarkerUpdates{markersToAdd: $markersToAdd, ' + 'markerIdsToRemove: $markerIdsToRemove, ' + 'markersToChange: $markersToChange}'; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart new file mode 100644 index 000000000000..28c7ce9d33dd --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart @@ -0,0 +1,35 @@ +// Copyright 2019 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 'package:meta/meta.dart' show immutable; + +/// Item used in the stroke pattern for a Polyline. +@immutable +class PatternItem { + const PatternItem._(this._json); + + /// A dot used in the stroke pattern for a [Polyline]. + static const PatternItem dot = PatternItem._(['dot']); + + /// A dash used in the stroke pattern for a [Polyline]. + /// + /// [length] has to be non-negative. + static PatternItem dash(double length) { + assert(length >= 0.0); + return PatternItem._(['dash', length]); + } + + /// A gap used in the stroke pattern for a [Polyline]. + /// + /// [length] has to be non-negative. + static PatternItem gap(double length) { + assert(length >= 0.0); + return PatternItem._(['gap', length]); + } + + final dynamic _json; + + /// Converts this object to something serializable in JSON. + dynamic toJson() => _json; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart new file mode 100644 index 000000000000..3b5e25060faf --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart @@ -0,0 +1,186 @@ +// Copyright 2019 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 'package:flutter/foundation.dart' show listEquals, VoidCallback; +import 'package:flutter/material.dart' show Color, Colors; +import 'package:meta/meta.dart' show immutable, required; + +import 'types.dart'; + +/// Uniquely identifies a [Polygon] among [GoogleMap] polygons. +/// +/// This does not have to be globally unique, only unique among the list. +@immutable +class PolygonId { + /// Creates an immutable identifier for a [Polygon]. + PolygonId(this.value) : assert(value != null); + + /// value of the [PolygonId]. + final String value; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final PolygonId typedOther = other; + return value == typedOther.value; + } + + @override + int get hashCode => value.hashCode; + + @override + String toString() { + return 'PolygonId{value: $value}'; + } +} + +/// Draws a polygon through geographical locations on the map. +@immutable +class Polygon { + /// Creates an immutable representation of a polygon through geographical locations on the map. + const Polygon({ + @required this.polygonId, + this.consumeTapEvents = false, + this.fillColor = Colors.black, + this.geodesic = false, + this.points = const [], + this.strokeColor = Colors.black, + this.strokeWidth = 10, + this.visible = true, + this.zIndex = 0, + this.onTap, + }); + + /// Uniquely identifies a [Polygon]. + final PolygonId polygonId; + + /// True if the [Polygon] consumes tap events. + /// + /// If this is false, [onTap] callback will not be triggered. + final bool consumeTapEvents; + + /// Fill color in ARGB format, the same format used by Color. The default value is black (0xff000000). + final Color fillColor; + + /// Indicates whether the segments of the polygon should be drawn as geodesics, as opposed to straight lines + /// on the Mercator projection. + /// + /// A geodesic is the shortest path between two points on the Earth's surface. + /// The geodesic curve is constructed assuming the Earth is a sphere + final bool geodesic; + + /// The vertices of the polygon to be drawn. + /// + /// Line segments are drawn between consecutive points. A polygon is not closed by + /// default; to form a closed polygon, the start and end points must be the same. + final List points; + + /// True if the marker is visible. + final bool visible; + + /// Line color in ARGB format, the same format used by Color. The default value is black (0xff000000). + final Color strokeColor; + + /// Width of the polygon, used to define the width of the line to be drawn. + /// + /// The width is constant and independent of the camera's zoom level. + /// The default value is 10. + final int strokeWidth; + + /// The z-index of the polygon, used to determine relative drawing order of + /// map overlays. + /// + /// Overlays are drawn in order of z-index, so that lower values means drawn + /// earlier, and thus appearing to be closer to the surface of the Earth. + final int zIndex; + + /// Callbacks to receive tap events for polygon placed on this map. + final VoidCallback onTap; + + /// Creates a new [Polygon] object whose values are the same as this instance, + /// unless overwritten by the specified parameters. + Polygon copyWith({ + bool consumeTapEventsParam, + Color fillColorParam, + bool geodesicParam, + List pointsParam, + Color strokeColorParam, + int strokeWidthParam, + bool visibleParam, + int zIndexParam, + VoidCallback onTapParam, + }) { + return Polygon( + polygonId: polygonId, + consumeTapEvents: consumeTapEventsParam ?? consumeTapEvents, + fillColor: fillColorParam ?? fillColor, + geodesic: geodesicParam ?? geodesic, + points: pointsParam ?? points, + strokeColor: strokeColorParam ?? strokeColor, + strokeWidth: strokeWidthParam ?? strokeWidth, + visible: visibleParam ?? visible, + onTap: onTapParam ?? onTap, + zIndex: zIndexParam ?? zIndex, + ); + } + + /// Creates a new [Polygon] object whose values are the same as this instance. + Polygon clone() { + return copyWith(pointsParam: List.of(points)); + } + + /// Converts this object to something serializable in JSON. + dynamic toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('polygonId', polygonId.value); + addIfPresent('consumeTapEvents', consumeTapEvents); + addIfPresent('fillColor', fillColor.value); + addIfPresent('geodesic', geodesic); + addIfPresent('strokeColor', strokeColor.value); + addIfPresent('strokeWidth', strokeWidth); + addIfPresent('visible', visible); + addIfPresent('zIndex', zIndex); + + if (points != null) { + json['points'] = _pointsToJson(); + } + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final Polygon typedOther = other; + return polygonId == typedOther.polygonId && + consumeTapEvents == typedOther.consumeTapEvents && + fillColor == typedOther.fillColor && + geodesic == typedOther.geodesic && + listEquals(points, typedOther.points) && + visible == typedOther.visible && + strokeColor == typedOther.strokeColor && + strokeWidth == typedOther.strokeWidth && + zIndex == typedOther.zIndex; + } + + @override + int get hashCode => polygonId.hashCode; + + dynamic _pointsToJson() { + final List result = []; + for (final LatLng point in points) { + result.add(point.toJson()); + } + return result; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart new file mode 100644 index 000000000000..cc8b8e26c896 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart @@ -0,0 +1,110 @@ +// Copyright 2018 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:ui' show hashValues; + +import 'package:flutter/foundation.dart' show setEquals; + +import 'types.dart'; +import 'utils/polygon.dart'; + +/// [Polygon] update events to be applied to the [GoogleMap]. +/// +/// Used in [GoogleMapController] when the map is updated. +// (Do not re-export) +class PolygonUpdates { + /// Computes [PolygonUpdates] given previous and current [Polygon]s. + PolygonUpdates.from(Set previous, Set current) { + if (previous == null) { + previous = Set.identity(); + } + + if (current == null) { + current = Set.identity(); + } + + final Map previousPolygons = keyByPolygonId(previous); + final Map currentPolygons = keyByPolygonId(current); + + final Set prevPolygonIds = previousPolygons.keys.toSet(); + final Set currentPolygonIds = currentPolygons.keys.toSet(); + + Polygon idToCurrentPolygon(PolygonId id) { + return currentPolygons[id]; + } + + final Set _polygonIdsToRemove = + prevPolygonIds.difference(currentPolygonIds); + + final Set _polygonsToAdd = currentPolygonIds + .difference(prevPolygonIds) + .map(idToCurrentPolygon) + .toSet(); + + /// Returns `true` if [current] is not equals to previous one with the + /// same id. + bool hasChanged(Polygon current) { + final Polygon previous = previousPolygons[current.polygonId]; + return current != previous; + } + + final Set _polygonsToChange = currentPolygonIds + .intersection(prevPolygonIds) + .map(idToCurrentPolygon) + .where(hasChanged) + .toSet(); + + polygonsToAdd = _polygonsToAdd; + polygonIdsToRemove = _polygonIdsToRemove; + polygonsToChange = _polygonsToChange; + } + + /// Set of Polygons to be added in this update. + Set polygonsToAdd; + + /// Set of PolygonIds to be removed in this update. + Set polygonIdsToRemove; + + /// Set of Polygons to be changed in this update. + Set polygonsToChange; + + /// Converts this object to something serializable in JSON. + Map toJson() { + final Map updateMap = {}; + + void addIfNonNull(String fieldName, dynamic value) { + if (value != null) { + updateMap[fieldName] = value; + } + } + + addIfNonNull('polygonsToAdd', serializePolygonSet(polygonsToAdd)); + addIfNonNull('polygonsToChange', serializePolygonSet(polygonsToChange)); + addIfNonNull('polygonIdsToRemove', + polygonIdsToRemove.map((PolygonId m) => m.value).toList()); + + return updateMap; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final PolygonUpdates typedOther = other; + return setEquals(polygonsToAdd, typedOther.polygonsToAdd) && + setEquals(polygonIdsToRemove, typedOther.polygonIdsToRemove) && + setEquals(polygonsToChange, typedOther.polygonsToChange); + } + + @override + int get hashCode => + hashValues(polygonsToAdd, polygonIdsToRemove, polygonsToChange); + + @override + String toString() { + return '_PolygonUpdates{polygonsToAdd: $polygonsToAdd, ' + 'polygonIdsToRemove: $polygonIdsToRemove, ' + 'polygonsToChange: $polygonsToChange}'; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart new file mode 100644 index 000000000000..ae5c3b976352 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart @@ -0,0 +1,247 @@ +// Copyright 2019 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 'package:flutter/foundation.dart' show listEquals, VoidCallback; +import 'package:flutter/material.dart' show Color, Colors; +import 'package:meta/meta.dart' show immutable, required; + +import 'types.dart'; + +/// Uniquely identifies a [Polyline] among [GoogleMap] polylines. +/// +/// This does not have to be globally unique, only unique among the list. +@immutable +class PolylineId { + /// Creates an immutable object representing a [PolylineId] among [GoogleMap] polylines. + /// + /// An [AssertionError] will be thrown if [value] is null. + PolylineId(this.value) : assert(value != null); + + /// value of the [PolylineId]. + final String value; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final PolylineId typedOther = other; + return value == typedOther.value; + } + + @override + int get hashCode => value.hashCode; + + @override + String toString() { + return 'PolylineId{value: $value}'; + } +} + +/// Draws a line through geographical locations on the map. +@immutable +class Polyline { + /// Creates an immutable object representing a line drawn through geographical locations on the map. + const Polyline({ + @required this.polylineId, + this.consumeTapEvents = false, + this.color = Colors.black, + this.endCap = Cap.buttCap, + this.geodesic = false, + this.jointType = JointType.mitered, + this.points = const [], + this.patterns = const [], + this.startCap = Cap.buttCap, + this.visible = true, + this.width = 10, + this.zIndex = 0, + this.onTap, + }); + + /// Uniquely identifies a [Polyline]. + final PolylineId polylineId; + + /// True if the [Polyline] consumes tap events. + /// + /// If this is false, [onTap] callback will not be triggered. + final bool consumeTapEvents; + + /// Line segment color in ARGB format, the same format used by Color. The default value is black (0xff000000). + final Color color; + + /// Indicates whether the segments of the polyline should be drawn as geodesics, as opposed to straight lines + /// on the Mercator projection. + /// + /// A geodesic is the shortest path between two points on the Earth's surface. + /// The geodesic curve is constructed assuming the Earth is a sphere + final bool geodesic; + + /// Joint type of the polyline line segments. + /// + /// The joint type defines the shape to be used when joining adjacent line segments at all vertices of the + /// polyline except the start and end vertices. See [JointType] for supported joint types. The default value is + /// mitered. + /// + /// Supported on Android only. + final JointType jointType; + + /// The stroke pattern for the polyline. + /// + /// Solid or a sequence of PatternItem objects to be repeated along the line. + /// Available PatternItem types: Gap (defined by gap length in pixels), Dash (defined by line width and dash + /// length in pixels) and Dot (circular, centered on the line, diameter defined by line width in pixels). + final List patterns; + + /// The vertices of the polyline to be drawn. + /// + /// Line segments are drawn between consecutive points. A polyline is not closed by + /// default; to form a closed polyline, the start and end points must be the same. + final List points; + + /// The cap at the start vertex of the polyline. + /// + /// The default start cap is ButtCap. + /// + /// Supported on Android only. + final Cap startCap; + + /// The cap at the end vertex of the polyline. + /// + /// The default end cap is ButtCap. + /// + /// Supported on Android only. + final Cap endCap; + + /// True if the marker is visible. + final bool visible; + + /// Width of the polyline, used to define the width of the line segment to be drawn. + /// + /// The width is constant and independent of the camera's zoom level. + /// The default value is 10. + final int width; + + /// The z-index of the polyline, used to determine relative drawing order of + /// map overlays. + /// + /// Overlays are drawn in order of z-index, so that lower values means drawn + /// earlier, and thus appearing to be closer to the surface of the Earth. + final int zIndex; + + /// Callbacks to receive tap events for polyline placed on this map. + final VoidCallback onTap; + + /// Creates a new [Polyline] object whose values are the same as this instance, + /// unless overwritten by the specified parameters. + Polyline copyWith({ + Color colorParam, + bool consumeTapEventsParam, + Cap endCapParam, + bool geodesicParam, + JointType jointTypeParam, + List patternsParam, + List pointsParam, + Cap startCapParam, + bool visibleParam, + int widthParam, + int zIndexParam, + VoidCallback onTapParam, + }) { + return Polyline( + polylineId: polylineId, + color: colorParam ?? color, + consumeTapEvents: consumeTapEventsParam ?? consumeTapEvents, + endCap: endCapParam ?? endCap, + geodesic: geodesicParam ?? geodesic, + jointType: jointTypeParam ?? jointType, + patterns: patternsParam ?? patterns, + points: pointsParam ?? points, + startCap: startCapParam ?? startCap, + visible: visibleParam ?? visible, + width: widthParam ?? width, + onTap: onTapParam ?? onTap, + zIndex: zIndexParam ?? zIndex, + ); + } + + /// Creates a new [Polyline] object whose values are the same as this + /// instance. + Polyline clone() { + return copyWith( + patternsParam: List.of(patterns), + pointsParam: List.of(points), + ); + } + + /// Converts this object to something serializable in JSON. + dynamic toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, dynamic value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('polylineId', polylineId.value); + addIfPresent('consumeTapEvents', consumeTapEvents); + addIfPresent('color', color.value); + addIfPresent('endCap', endCap?.toJson()); + addIfPresent('geodesic', geodesic); + addIfPresent('jointType', jointType?.value); + addIfPresent('startCap', startCap?.toJson()); + addIfPresent('visible', visible); + addIfPresent('width', width); + addIfPresent('zIndex', zIndex); + + if (points != null) { + json['points'] = _pointsToJson(); + } + + if (patterns != null) { + json['pattern'] = _patternToJson(); + } + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final Polyline typedOther = other; + return polylineId == typedOther.polylineId && + consumeTapEvents == typedOther.consumeTapEvents && + color == typedOther.color && + geodesic == typedOther.geodesic && + jointType == typedOther.jointType && + listEquals(patterns, typedOther.patterns) && + listEquals(points, typedOther.points) && + startCap == typedOther.startCap && + endCap == typedOther.endCap && + visible == typedOther.visible && + width == typedOther.width && + zIndex == typedOther.zIndex; + } + + @override + int get hashCode => polylineId.hashCode; + + dynamic _pointsToJson() { + final List result = []; + for (final LatLng point in points) { + result.add(point.toJson()); + } + return result; + } + + dynamic _patternToJson() { + final List result = []; + for (final PatternItem patternItem in patterns) { + if (patternItem != null) { + result.add(patternItem.toJson()); + } + } + return result; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart new file mode 100644 index 000000000000..f871928c0ac4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart @@ -0,0 +1,111 @@ +// Copyright 2018 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:ui' show hashValues; + +import 'package:flutter/foundation.dart' show setEquals; + +import 'utils/polyline.dart'; +import 'types.dart'; + +/// [Polyline] update events to be applied to the [GoogleMap]. +/// +/// Used in [GoogleMapController] when the map is updated. +// (Do not re-export) +class PolylineUpdates { + /// Computes [PolylineUpdates] given previous and current [Polyline]s. + PolylineUpdates.from(Set previous, Set current) { + if (previous == null) { + previous = Set.identity(); + } + + if (current == null) { + current = Set.identity(); + } + + final Map previousPolylines = + keyByPolylineId(previous); + final Map currentPolylines = keyByPolylineId(current); + + final Set prevPolylineIds = previousPolylines.keys.toSet(); + final Set currentPolylineIds = currentPolylines.keys.toSet(); + + Polyline idToCurrentPolyline(PolylineId id) { + return currentPolylines[id]; + } + + final Set _polylineIdsToRemove = + prevPolylineIds.difference(currentPolylineIds); + + final Set _polylinesToAdd = currentPolylineIds + .difference(prevPolylineIds) + .map(idToCurrentPolyline) + .toSet(); + + /// Returns `true` if [current] is not equals to previous one with the + /// same id. + bool hasChanged(Polyline current) { + final Polyline previous = previousPolylines[current.polylineId]; + return current != previous; + } + + final Set _polylinesToChange = currentPolylineIds + .intersection(prevPolylineIds) + .map(idToCurrentPolyline) + .where(hasChanged) + .toSet(); + + polylinesToAdd = _polylinesToAdd; + polylineIdsToRemove = _polylineIdsToRemove; + polylinesToChange = _polylinesToChange; + } + + /// Set of Polylines to be added in this update. + Set polylinesToAdd; + + /// Set of PolylineIds to be removed in this update. + Set polylineIdsToRemove; + + /// Set of Polylines to be changed in this update. + Set polylinesToChange; + + /// Converts this object to something serializable in JSON. + Map toJson() { + final Map updateMap = {}; + + void addIfNonNull(String fieldName, dynamic value) { + if (value != null) { + updateMap[fieldName] = value; + } + } + + addIfNonNull('polylinesToAdd', serializePolylineSet(polylinesToAdd)); + addIfNonNull('polylinesToChange', serializePolylineSet(polylinesToChange)); + addIfNonNull('polylineIdsToRemove', + polylineIdsToRemove.map((PolylineId m) => m.value).toList()); + + return updateMap; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final PolylineUpdates typedOther = other; + return setEquals(polylinesToAdd, typedOther.polylinesToAdd) && + setEquals(polylineIdsToRemove, typedOther.polylineIdsToRemove) && + setEquals(polylinesToChange, typedOther.polylinesToChange); + } + + @override + int get hashCode => + hashValues(polylinesToAdd, polylineIdsToRemove, polylinesToChange); + + @override + String toString() { + return '_PolylineUpdates{polylinesToAdd: $polylinesToAdd, ' + 'polylineIdsToRemove: $polylineIdsToRemove, ' + 'polylinesToChange: $polylinesToChange}'; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart new file mode 100644 index 000000000000..965db7969bc2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart @@ -0,0 +1,46 @@ +// Copyright 2019 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:ui' show hashValues; + +import 'package:meta/meta.dart' show immutable, required; + +/// Represents a point coordinate in the [GoogleMap]'s view. +/// +/// 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. (x, y) = (0, 0) +/// corresponds to top-left of the [GoogleMap] not the whole screen. +@immutable +class ScreenCoordinate { + /// Creates an immutable representation of a point coordinate in the [GoogleMap]'s view. + const ScreenCoordinate({ + @required this.x, + @required this.y, + }); + + /// Represents the number of pixels from the left of the [GoogleMap]. + final int x; + + /// Represents the number of pixels from the top of the [GoogleMap]. + final int y; + + /// Converts this object to something serializable in JSON. + dynamic toJson() { + return { + "x": x, + "y": y, + }; + } + + @override + String toString() => '$runtimeType($x, $y)'; + + @override + bool operator ==(Object o) { + return o is ScreenCoordinate && o.x == x && o.y == y; + } + + @override + int get hashCode => hashValues(x, y); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart new file mode 100644 index 000000000000..e56c3a5dd646 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart @@ -0,0 +1,28 @@ +// Copyright 2018 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. + +// All the public types exposed by this package. +export 'bitmap.dart'; +export 'callbacks.dart'; +export 'camera.dart'; +export 'cap.dart'; +export 'circle_updates.dart'; +export 'circle.dart'; +export 'joint_type.dart'; +export 'location.dart'; +export 'marker_updates.dart'; +export 'marker.dart'; +export 'pattern_item.dart'; +export 'polygon_updates.dart'; +export 'polygon.dart'; +export 'polyline_updates.dart'; +export 'polyline.dart'; +export 'screen_coordinate.dart'; +export 'ui.dart'; + +// Export the utils, they're used by the Widget +export 'utils/circle.dart'; +export 'utils/marker.dart'; +export 'utils/polygon.dart'; +export 'utils/polyline.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart new file mode 100644 index 000000000000..8d84171bac03 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart @@ -0,0 +1,118 @@ +// Copyright 2018 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:ui' show hashValues; + +import 'types.dart'; + +/// Type of map tiles to display. +// Enum constants must be indexed to match the corresponding int constants of +// the Android platform API, see +// +enum MapType { + /// Do not display map tiles. + none, + + /// Normal tiles (traffic and labels, subtle terrain information). + normal, + + /// Satellite imaging tiles (aerial photos) + satellite, + + /// Terrain tiles (indicates type and height of terrain) + terrain, + + /// Hybrid tiles (satellite images with some labels/overlays) + hybrid, +} + +/// Bounds for the map camera target. +// Used with [GoogleMapOptions] to wrap a [LatLngBounds] value. This allows +// distinguishing between specifying an unbounded target (null `LatLngBounds`) +// from not specifying anything (null `CameraTargetBounds`). +class CameraTargetBounds { + /// Creates a camera target bounds with the specified bounding box, or null + /// to indicate that the camera target is not bounded. + const CameraTargetBounds(this.bounds); + + /// The geographical bounding box for the map camera target. + /// + /// A null value means the camera target is unbounded. + final LatLngBounds bounds; + + /// Unbounded camera target. + static const CameraTargetBounds unbounded = CameraTargetBounds(null); + + /// Converts this object to something serializable in JSON. + dynamic toJson() => [bounds?.toJson()]; + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (runtimeType != other.runtimeType) return false; + final CameraTargetBounds typedOther = other; + return bounds == typedOther.bounds; + } + + @override + int get hashCode => bounds.hashCode; + + @override + String toString() { + return 'CameraTargetBounds(bounds: $bounds)'; + } +} + +/// Preferred bounds for map camera zoom level. +// Used with [GoogleMapOptions] to wrap min and max zoom. This allows +// distinguishing between specifying unbounded zooming (null `minZoom` and +// `maxZoom`) from not specifying anything (null `MinMaxZoomPreference`). +class MinMaxZoomPreference { + /// Creates a immutable representation of the preferred minimum and maximum zoom values for the map camera. + /// + /// [AssertionError] will be thrown if [minZoom] > [maxZoom]. + const MinMaxZoomPreference(this.minZoom, this.maxZoom) + : assert(minZoom == null || maxZoom == null || minZoom <= maxZoom); + + /// The preferred minimum zoom level or null, if unbounded from below. + final double minZoom; + + /// The preferred maximum zoom level or null, if unbounded from above. + final double maxZoom; + + /// Unbounded zooming. + static const MinMaxZoomPreference unbounded = + MinMaxZoomPreference(null, null); + + /// Converts this object to something serializable in JSON. + dynamic toJson() => [minZoom, maxZoom]; + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (runtimeType != other.runtimeType) return false; + final MinMaxZoomPreference typedOther = other; + return minZoom == typedOther.minZoom && maxZoom == typedOther.maxZoom; + } + + @override + int get hashCode => hashValues(minZoom, maxZoom); + + @override + String toString() { + return 'MinMaxZoomPreference(minZoom: $minZoom, maxZoom: $maxZoom)'; + } +} + +/// Exception when a map style is invalid or was unable to be set. +/// +/// See also: `setStyle` on [GoogleMapController] for why this exception +/// might be thrown. +class MapStyleException implements Exception { + /// Default constructor for [MapStyleException]. + const MapStyleException(this.cause); + + /// The reason `GoogleMapController.setStyle` would throw this exception. + final String cause; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart new file mode 100644 index 000000000000..5c3af96f8e02 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart @@ -0,0 +1,22 @@ +// Copyright 2019 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 '../types.dart'; + +/// Converts an [Iterable] of Circles in a Map of CircleId -> Circle. +Map keyByCircleId(Iterable circles) { + if (circles == null) { + return {}; + } + return Map.fromEntries(circles.map((Circle circle) => + MapEntry(circle.circleId, circle.clone()))); +} + +/// Converts a Set of Circles into something serializable in JSON. +List> serializeCircleSet(Set circles) { + if (circles == null) { + return null; + } + return circles.map>((Circle p) => p.toJson()).toList(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart new file mode 100644 index 000000000000..7a2c76d8055b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart @@ -0,0 +1,22 @@ +// Copyright 2019 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 '../types.dart'; + +/// Converts an [Iterable] of Markers in a Map of MarkerId -> Marker. +Map keyByMarkerId(Iterable markers) { + if (markers == null) { + return {}; + } + return Map.fromEntries(markers.map((Marker marker) => + MapEntry(marker.markerId, marker.clone()))); +} + +/// Converts a Set of Markers into something serializable in JSON. +List> serializeMarkerSet(Set markers) { + if (markers == null) { + return null; + } + return markers.map>((Marker m) => m.toJson()).toList(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart new file mode 100644 index 000000000000..9434ddaa077d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart @@ -0,0 +1,22 @@ +// Copyright 2019 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 '../types.dart'; + +/// Converts an [Iterable] of Polygons in a Map of PolygonId -> Polygon. +Map keyByPolygonId(Iterable polygons) { + if (polygons == null) { + return {}; + } + return Map.fromEntries(polygons.map((Polygon polygon) => + MapEntry(polygon.polygonId, polygon.clone()))); +} + +/// Converts a Set of Polygons into something serializable in JSON. +List> serializePolygonSet(Set polygons) { + if (polygons == null) { + return null; + } + return polygons.map>((Polygon p) => p.toJson()).toList(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart new file mode 100644 index 000000000000..9cef6319ddb5 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart @@ -0,0 +1,25 @@ +// Copyright 2019 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 '../types.dart'; + +/// Converts an [Iterable] of Polylines in a Map of PolylineId -> Polyline. +Map keyByPolylineId(Iterable polylines) { + if (polylines == null) { + return {}; + } + return Map.fromEntries(polylines.map( + (Polyline polyline) => MapEntry( + polyline.polylineId, polyline.clone()))); +} + +/// Converts a Set of Polylines into something serializable in JSON. +List> serializePolylineSet(Set polylines) { + if (polylines == null) { + return null; + } + return polylines + .map>((Polyline p) => p.toJson()) + .toList(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..2cd840b07e3a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -0,0 +1,23 @@ +name: google_maps_flutter_platform_interface +description: A common platform interface for the google_maps_flutter plugin. +homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_platform_interface +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 1.0.0 + +dependencies: + flutter: + sdk: flutter + meta: ^1.0.5 + plugin_platform_interface: ^1.0.1 + stream_transform: ^1.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^4.1.1 + pedantic: ^1.8.0 + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.9.1+hotfix.4 <2.0.0" diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart new file mode 100644 index 000000000000..a003b94d544c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart @@ -0,0 +1,71 @@ +// 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 'package:mockito/mockito.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$GoogleMapsFlutterPlatform', () { + test('$MethodChannelGoogleMapsFlutter() is the default instance', () { + expect(GoogleMapsFlutterPlatform.instance, + isInstanceOf()); + }); + + test('Cannot be implemented with `implements`', () { + expect(() { + GoogleMapsFlutterPlatform.instance = + ImplementsGoogleMapsFlutterPlatform(); + }, throwsA(isInstanceOf())); + }); + + test('Can be mocked with `implements`', () { + final GoogleMapsFlutterPlatformMock mock = + GoogleMapsFlutterPlatformMock(); + GoogleMapsFlutterPlatform.instance = mock; + }); + + test('Can be extended', () { + GoogleMapsFlutterPlatform.instance = ExtendsGoogleMapsFlutterPlatform(); + }); + }); + + group('$MethodChannelGoogleMapsFlutter', () { + const MethodChannel channel = + MethodChannel('plugins.flutter.io/google_maps_flutter'); + final List log = []; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + }); + +// final MethodChannelGoogleMapsFlutter map = MethodChannelGoogleMapsFlutter(0); + + tearDown(() { + log.clear(); + }); + + test('foo', () async { +// await map.foo(); + expect( + log, + [], + ); + }); + }); +} + +class GoogleMapsFlutterPlatformMock extends Mock + with MockPlatformInterfaceMixin + implements GoogleMapsFlutterPlatform {} + +class ImplementsGoogleMapsFlutterPlatform extends Mock + implements GoogleMapsFlutterPlatform {} + +class ExtendsGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform {} diff --git a/script/build_all_plugins_app.sh b/script/build_all_plugins_app.sh index 8b1d4b320f09..f21a2f84f88f 100755 --- a/script/build_all_plugins_app.sh +++ b/script/build_all_plugins_app.sh @@ -15,6 +15,8 @@ readonly EXCLUDED_PLUGINS_LIST=( "connectivity_platform_interface" "connectivity_web" "flutter_plugin_android_lifecycle" + "google_maps_flutter_platform_interface" + "google_maps_flutter_web" "google_sign_in_platform_interface" "google_sign_in_web" "instrumentation_adapter"