blob: 03bc739bebf6cf816e4a6c15c1d80dc3dcd55839 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of google_maps_flutter_web;
/// Type used when passing an override to the _createMap function.
typedef DebugCreateMapFunction = gmaps.GMap Function(
HtmlElement div, gmaps.MapOptions options);
/// Encapsulates a [gmaps.GMap], its events, and where in the DOM it's rendered.
class GoogleMapController {
/// Initializes the GMap, and the sub-controllers related to it. Wires events.
required int mapId,
required StreamController<MapEvent<Object?>> streamController,
required MapWidgetConfiguration widgetConfiguration,
MapObjects mapObjects = const MapObjects(),
MapConfiguration mapConfiguration = const MapConfiguration(),
}) : _mapId = mapId,
_streamController = streamController,
_initialCameraPosition = widgetConfiguration.initialCameraPosition,
_markers = mapObjects.markers,
_polygons = mapObjects.polygons,
_polylines = mapObjects.polylines,
_circles = mapObjects.circles,
_tileOverlays = mapObjects.tileOverlays,
_lastMapConfiguration = mapConfiguration {
_circlesController = CirclesController(stream: _streamController);
_polygonsController = PolygonsController(stream: _streamController);
_polylinesController = PolylinesController(stream: _streamController);
_markersController = MarkersController(stream: _streamController);
_tileOverlaysController = TileOverlaysController();
// Register the view factory that will hold the `_div` that holds the map in the DOM.
// The `_div` needs to be created outside of the ViewFactory (and cached!) so we can
// use it to create the [gmaps.GMap] in the `init()` method of this class.
_div = DivElement() = _getViewType(mapId) = '100%' = '100%';
(int viewId) => _div,
// The internal ID of the map. Used to broadcast events, DOM IDs and everything where a unique ID is needed.
final int _mapId;
final CameraPosition _initialCameraPosition;
final Set<Marker> _markers;
final Set<Polygon> _polygons;
final Set<Polyline> _polylines;
final Set<Circle> _circles;
Set<TileOverlay> _tileOverlays;
// The configuration passed by the user, before converting to gmaps.
// Caching this allows us to re-create the map faithfully when needed.
MapConfiguration _lastMapConfiguration = const MapConfiguration();
List<gmaps.MapTypeStyle> _lastStyles = const <gmaps.MapTypeStyle>[];
/// Configuration accessor for the [GoogleMapsInspectorWeb].
/// Returns the latest value of [MapConfiguration] set by the programmer.
/// This should only be used by an inspector instance created when a test
/// calls [GoogleMapsPlugin.enableDebugInspection].
MapConfiguration get configuration => _lastMapConfiguration;
// Creates the 'viewType' for the _widget
String _getViewType(int mapId) => '$mapId';
// The Flutter widget that contains the rendered Map.
HtmlElementView? _widget;
late HtmlElement _div;
/// The Flutter widget that will contain the rendered Map. Used for caching.
Widget? get widget {
if (_widget == null && !_streamController.isClosed) {
_widget = HtmlElementView(
viewType: _getViewType(_mapId),
return _widget;
// The currently-enabled traffic layer.
gmaps.TrafficLayer? _trafficLayer;
/// A getter for the current traffic layer. Only for tests.
gmaps.TrafficLayer? get trafficLayer => _trafficLayer;
// The underlying GMap instance. This is the interface with the JS SDK.
gmaps.GMap? _googleMap;
// The StreamController used by this controller and the geometry ones.
final StreamController<MapEvent<Object?>> _streamController;
/// The StreamController for the events of this Map. Only for integration testing.
StreamController<MapEvent<Object?>> get stream => _streamController;
/// The Stream over which this controller broadcasts events.
Stream<MapEvent<Object?>> get events =>;
// Geometry controllers, for different features of the map.
CirclesController? _circlesController;
PolygonsController? _polygonsController;
PolylinesController? _polylinesController;
MarkersController? _markersController;
TileOverlaysController? _tileOverlaysController;
// Keeps track if _attachGeometryControllers has been called or not.
bool _controllersBoundToMap = false;
// Keeps track if the map is moving or not.
bool _mapIsMoving = false;
/// Overrides certain properties to install mocks defined during testing.
void debugSetOverrides({
DebugCreateMapFunction? createMap,
MarkersController? markers,
CirclesController? circles,
PolygonsController? polygons,
PolylinesController? polylines,
TileOverlaysController? tileOverlays,
}) {
_overrideCreateMap = createMap;
_markersController = markers ?? _markersController;
_circlesController = circles ?? _circlesController;
_polygonsController = polygons ?? _polygonsController;
_polylinesController = polylines ?? _polylinesController;
_tileOverlaysController = tileOverlays ?? _tileOverlaysController;
DebugCreateMapFunction? _overrideCreateMap;
gmaps.GMap _createMap(HtmlElement div, gmaps.MapOptions options) {
if (_overrideCreateMap != null) {
return _overrideCreateMap!(div, options);
return gmaps.GMap(div, options);
/// A flag that returns true if the controller has been initialized or not.
bool get isInitialized => _googleMap != null;
/// Starts the JS Maps SDK into the target [_div] with `rawOptions`.
/// (Also initializes the geometry/traffic layers.)
/// The first part of this method starts the rendering of a [gmaps.GMap] inside
/// of the target [_div], with configuration from `rawOptions`. It then stores
/// the created GMap in the [_googleMap] attribute.
/// Not *everything* is rendered with the initial `rawOptions` configuration,
/// geometry and traffic layers (and possibly others in the future) have their
/// own configuration and are rendered on top of a GMap instance later. This
/// happens in the second half of this method.
/// This method is eagerly called from the [GoogleMapsPlugin.buildView] method
/// so the internal [GoogleMapsController] of a Web Map initializes as soon as
/// possible. Check [_attachMapEvents] to see how this controller notifies the
/// plugin of it being fully ready (through the `onTilesloaded.first` event).
/// Failure to call this method would result in the GMap not rendering at all,
/// and most of the public methods on this class no-op'ing.
void init() {
gmaps.MapOptions options = _configurationAndStyleToGmapsOptions(
_lastMapConfiguration, _lastStyles);
// Initial position can only to be set here!
options = _applyInitialPosition(_initialCameraPosition, options);
// Fully disable 45 degree imagery if desired
if (options.rotateControl == false) {
options.tilt = 0;
// Create the map...
final gmaps.GMap map = _createMap(_div, options);
_googleMap = map;
// Now attach the geometry, traffic and any other layers...
_setTrafficLayer(map, _lastMapConfiguration.trafficEnabled ?? false);
// Funnels map gmap events into the plugin's stream controller.
void _attachMapEvents(gmaps.GMap map) {
map.onTilesloaded.first.then((void _) {
// Report the map as ready to go the first time the tiles load
map.onClick.listen((gmaps.IconMouseEvent event) {
assert(event.latLng != null);
MapTapEvent(_mapId, _gmLatLngToLatLng(event.latLng!)),
map.onRightclick.listen((gmaps.MapMouseEvent event) {
assert(event.latLng != null);
MapLongPressEvent(_mapId, _gmLatLngToLatLng(event.latLng!)),
map.onBoundsChanged.listen((void _) {
if (!_mapIsMoving) {
_mapIsMoving = true;
CameraMoveEvent(_mapId, _gmViewportToCameraPosition(map)),
map.onIdle.listen((void _) {
_mapIsMoving = false;
// Binds the Geometry controllers to a map instance
void _attachGeometryControllers(gmaps.GMap map) {
// Now we can add the initial geometry.
// And bind the (ready) map instance to the other geometry controllers.
// These controllers are either created in the constructor of this class, or
// overriden (for testing) by the [debugSetOverrides] method. They can't be
// null.
assert(_circlesController != null,
'Cannot attach a map to a null CirclesController instance.');
assert(_polygonsController != null,
'Cannot attach a map to a null PolygonsController instance.');
assert(_polylinesController != null,
'Cannot attach a map to a null PolylinesController instance.');
assert(_markersController != null,
'Cannot attach a map to a null MarkersController instance.');
assert(_tileOverlaysController != null,
'Cannot attach a map to a null TileOverlaysController instance.');
_circlesController!.bindToMap(_mapId, map);
_polygonsController!.bindToMap(_mapId, map);
_polylinesController!.bindToMap(_mapId, map);
_markersController!.bindToMap(_mapId, map);
_tileOverlaysController!.bindToMap(_mapId, map);
_controllersBoundToMap = true;
// Renders the initial sets of geometry.
void _renderInitialGeometry() {
'Geometry controllers must be bound to a map before any geometry can '
'be added to them. Ensure _attachGeometryControllers is called first.');
// The above assert will only succeed if the controllers have been bound to a map
// in the [_attachGeometryControllers] method, which ensures that all these
// controllers below are *not* null.
// Merges new options coming from the plugin into _lastConfiguration.
// Returns the updated _lastConfiguration object.
MapConfiguration _mergeConfigurations(MapConfiguration update) {
_lastMapConfiguration = _lastMapConfiguration.applyDiff(update);
return _lastMapConfiguration;
/// Updates the map options from a [MapConfiguration].
/// This method converts the map into the proper [gmaps.MapOptions].
void updateMapConfiguration(MapConfiguration update) {
assert(_googleMap != null, 'Cannot update options on a null map.');
final MapConfiguration newConfiguration = _mergeConfigurations(update);
final gmaps.MapOptions newOptions =
_configurationAndStyleToGmapsOptions(newConfiguration, _lastStyles);
_setTrafficLayer(_googleMap!, newConfiguration.trafficEnabled ?? false);
/// Updates the map options with a new list of [styles].
void updateStyles(List<gmaps.MapTypeStyle> styles) {
_lastStyles = styles;
_configurationAndStyleToGmapsOptions(_lastMapConfiguration, styles));
// Sets new [gmaps.MapOptions] on the wrapped map.
// ignore: use_setters_to_change_properties
void _setOptions(gmaps.MapOptions options) {
_googleMap?.options = options;
// Attaches/detaches a Traffic Layer on the passed `map` if `attach` is true/false.
void _setTrafficLayer(gmaps.GMap map, bool attach) {
if (attach && _trafficLayer == null) {
_trafficLayer = gmaps.TrafficLayer()..set('map', map);
if (!attach && _trafficLayer != null) {
_trafficLayer!.set('map', null);
_trafficLayer = null;
// _googleMap manipulation
// Viewport
/// Returns the [LatLngBounds] of the current viewport.
Future<LatLngBounds> getVisibleRegion() async {
assert(_googleMap != null, 'Cannot get the visible region of a null map.');
final gmaps.LatLngBounds bounds =
await Future<gmaps.LatLngBounds?>.value(_googleMap!.bounds) ??
return _gmLatLngBoundsTolatLngBounds(bounds);
/// Returns the [ScreenCoordinate] for a given viewport [LatLng].
Future<ScreenCoordinate> getScreenCoordinate(LatLng latLng) async {
assert(_googleMap != null,
'Cannot get the screen coordinates with a null map.');
final gmaps.Point point =
toScreenLocation(_googleMap!, _latLngToGmLatLng(latLng));
return ScreenCoordinate(x: point.x!.toInt(), y: point.y!.toInt());
/// Returns the [LatLng] for a `screenCoordinate` (in pixels) of the viewport.
Future<LatLng> getLatLng(ScreenCoordinate screenCoordinate) async {
assert(_googleMap != null,
'Cannot get the lat, lng of a screen coordinate with a null map.');
final gmaps.LatLng latLng =
_pixelToLatLng(_googleMap!, screenCoordinate.x, screenCoordinate.y);
return _gmLatLngToLatLng(latLng);
/// Applies a `cameraUpdate` to the current viewport.
Future<void> moveCamera(CameraUpdate cameraUpdate) async {
assert(_googleMap != null, 'Cannot update the camera of a null map.');
return _applyCameraUpdate(_googleMap!, cameraUpdate);
/// Returns the zoom level of the current viewport.
Future<double> getZoomLevel() async {
assert(_googleMap != null, 'Cannot get zoom level of a null map.');
assert(_googleMap!.zoom != null,
'Zoom level should not be null. Is the map correctly initialized?');
return _googleMap!.zoom!.toDouble();
// Geometry manipulation
/// Applies [CircleUpdates] to the currently managed circles.
void updateCircles(CircleUpdates updates) {
_circlesController != null, 'Cannot update circles after dispose().');
/// Applies [PolygonUpdates] to the currently managed polygons.
void updatePolygons(PolygonUpdates updates) {
_polygonsController != null, 'Cannot update polygons after dispose().');
/// Applies [PolylineUpdates] to the currently managed lines.
void updatePolylines(PolylineUpdates updates) {
assert(_polylinesController != null,
'Cannot update polylines after dispose().');
/// Applies [MarkerUpdates] to the currently managed markers.
void updateMarkers(MarkerUpdates updates) {
_markersController != null, 'Cannot update markers after dispose().');
/// Updates the set of [TileOverlay]s.
void updateTileOverlays(Set<TileOverlay> newOverlays) {
final MapsObjectUpdates<TileOverlay> updates =
MapsObjectUpdates<TileOverlay>.from(_tileOverlays, newOverlays,
objectName: 'tileOverlay');
assert(_tileOverlaysController != null,
'Cannot update tile overlays after dispose().');
_tileOverlays = newOverlays;
/// Clears the tile cache associated with the given [TileOverlayId].
void clearTileCache(TileOverlayId id) {
/// Shows the [InfoWindow] of the marker identified by its [MarkerId].
void showInfoWindow(MarkerId markerId) {
assert(_markersController != null,
'Cannot show infowindow of marker [${markerId.value}] after dispose().');
/// Hides the [InfoWindow] of the marker identified by its [MarkerId].
void hideInfoWindow(MarkerId markerId) {
assert(_markersController != null,
'Cannot hide infowindow of marker [${markerId.value}] after dispose().');
/// Returns true if the [InfoWindow] of the marker identified by [MarkerId] is shown.
bool isInfoWindowShown(MarkerId markerId) {
return _markersController?.isInfoWindowShown(markerId) ?? false;
// Cleanup
/// Disposes of this controller and its resources.
/// You won't be able to call many of the methods on this controller after
/// calling `dispose`!
void dispose() {
_widget = null;
_googleMap = null;
_circlesController = null;
_polygonsController = null;
_polylinesController = null;
_markersController = null;
_tileOverlaysController = null;
/// A MapEvent event fired when a [mapId] on web is interactive.
class WebMapReadyEvent extends MapEvent<Object?> {
/// Build a WebMapReady Event for the map represented by `mapId`.
WebMapReadyEvent(int mapId) : super(mapId, null);