diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 12e9ab4b55f7..e0703f0c9010 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.8 + +* Add TileOverlay support. + ## 1.0.7 * Android: Handle deprecation & unchecked warning as error. diff --git a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle index a1d7da08a8d9..4a05b1929cfb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle @@ -21,6 +21,9 @@ rootProject.allprojects { apply plugin: 'com.android.library' +sourceCompatibility = JavaVersion.VERSION_1_8 +sourceCompatibility = JavaVersion.VERSION_1_8 + android { compileSdkVersion 29 @@ -31,6 +34,10 @@ android { lintOptions { disable 'InvalidPackage' } + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } dependencies { implementation "androidx.annotation:annotation:1.1.0" diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 7222511a2a3b..ecfa8d045c61 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -23,6 +23,7 @@ import com.google.android.gms.maps.model.PatternItem; import com.google.android.gms.maps.model.RoundCap; import com.google.android.gms.maps.model.SquareCap; +import com.google.android.gms.maps.model.Tile; import io.flutter.view.FlutterMain; import java.util.ArrayList; import java.util.Arrays; @@ -145,6 +146,13 @@ private static int toInt(Object o) { return ((Number) o).intValue(); } + private static byte[] toByteArray(Object o) { + if (o == null) { + return null; + } + return (byte[]) o; + } + static Object cameraPositionToJson(CameraPosition position) { if (position == null) { return null; @@ -200,6 +208,18 @@ static Object circleIdToJson(String circleId) { return data; } + static Object tileOverlayArgumentsToJson(String tileOverlayId, int x, int y, int zoom) { + if (tileOverlayId == null) { + return null; + } + final Map data = new HashMap<>(4); + data.put("tileOverlayId", tileOverlayId); + data.put("x", x); + data.put("y", y); + data.put("zoom", zoom); + return data; + } + static Object latLngToJson(LatLng latLng) { return Arrays.asList(latLng.latitude, latLng.longitude); } @@ -631,4 +651,38 @@ private static Cap toCap(Object o) { throw new IllegalArgumentException("Cannot interpret " + o + " as Cap"); } } + + static String interpretTileOverlayOptions(Object o, TileOverlaySink sink) { + final Map data = toMap(o); + final Object fadeIn = data.get("fadeIn"); + if (fadeIn != null) { + sink.setFadeIn(toBoolean(fadeIn)); + } + final Object transparency = data.get("transparency"); + if (transparency != null) { + sink.setTransparency(toFloat(transparency)); + } + final Object zIndex = data.get("zIndex"); + if (zIndex != null) { + sink.setZIndex(toFloat(zIndex)); + } + final Object visible = data.get("visible"); + if (visible != null) { + sink.setVisible(toBoolean(visible)); + } + final String tileOverlayId = (String) data.get("tileOverlayId"); + if (tileOverlayId == null) { + throw new IllegalArgumentException("tileOverlayId was null"); + } else { + return tileOverlayId; + } + } + + static Tile interpretTile(Object o) { + final Map data = toMap(o); + int width = toInt(data.get("width")); + int height = toInt(data.get("height")); + byte[] dataArray = toByteArray(data.get("data")); + return new Tile(width, height, dataArray); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java index 93a3c3ec9c28..b4ed7b8e7178 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java @@ -23,6 +23,7 @@ class GoogleMapBuilder implements GoogleMapOptionsSink { private Object initialPolygons; private Object initialPolylines; private Object initialCircles; + private Object initialTileOverlays; private Rect padding = new Rect(0, 0, 0, 0); GoogleMapController build( @@ -43,6 +44,7 @@ GoogleMapController build( controller.setInitialPolygons(initialPolygons); controller.setInitialPolylines(initialPolylines); controller.setInitialCircles(initialCircles); + controller.setInitialTileOverlays(initialTileOverlays); controller.setPadding(padding.top, padding.left, padding.bottom, padding.right); return controller; } @@ -165,4 +167,9 @@ public void setInitialPolylines(Object initialPolylines) { public void setInitialCircles(Object initialCircles) { this.initialCircles = initialCircles; } + + @Override + public void setInitialTileOverlays(Object initialTileOverlays) { + this.initialTileOverlays = initialTileOverlays; + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index f6b8c3fe1fd1..ea007118afc2 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -76,10 +76,12 @@ final class GoogleMapController private final PolygonsController polygonsController; private final PolylinesController polylinesController; private final CirclesController circlesController; + private final TileOverlaysController tileOverlayController; private List initialMarkers; private List initialPolygons; private List initialPolylines; private List initialCircles; + private List initialTileOverlays; GoogleMapController( int id, @@ -99,6 +101,7 @@ final class GoogleMapController this.polygonsController = new PolygonsController(methodChannel, density); this.polylinesController = new PolylinesController(methodChannel, density); this.circlesController = new CirclesController(methodChannel, density); + this.tileOverlayController = new TileOverlaysController(methodChannel); } @Override @@ -140,10 +143,12 @@ public void onMapReady(GoogleMap googleMap) { polygonsController.setGoogleMap(googleMap); polylinesController.setGoogleMap(googleMap); circlesController.setGoogleMap(googleMap); + tileOverlayController.setGoogleMap(googleMap); updateInitialMarkers(); updateInitialPolygons(); updateInitialPolylines(); updateInitialCircles(); + updateInitialTileOverlays(); } @Override @@ -385,6 +390,24 @@ public void onSnapshotReady(Bitmap bitmap) { result.success(mapStyleResult); break; } + case "tileOverlays#update": + { + Object tileOverlaysToAdd = call.argument("tileOverlaysToAdd"); + tileOverlayController.addTileOverlays((List) tileOverlaysToAdd); + Object tileOverlaysToChange = call.argument("tileOverlaysToChange"); + tileOverlayController.changeTileOverlays((List) tileOverlaysToChange); + Object tileOverlaysToRemove = call.argument("tileOverlayIdsToRemove"); + tileOverlayController.removeTileOverlays((List) tileOverlaysToRemove); + result.success(null); + break; + } + case "tileOverlays#clearTileCache": + { + Object rawTileOverlayId = call.argument("tileOverlayId"); + tileOverlayController.clearTileCache(rawTileOverlayId); + result.success(null); + break; + } default: result.notImplemented(); } @@ -732,6 +755,18 @@ private void updateInitialCircles() { circlesController.addCircles(initialCircles); } + @Override + public void setInitialTileOverlays(Object initialTileOverlays) { + this.initialTileOverlays = (List) initialTileOverlays; + if (googleMap != null) { + updateInitialTileOverlays(); + } + } + + private void updateInitialTileOverlays() { + tileOverlayController.addTileOverlays(initialTileOverlays); + } + @SuppressLint("MissingPermission") private void updateMyLocationSettings() { if (hasLocationPermission()) { diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java index e56adbbb987a..42afa3edb294 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java @@ -46,6 +46,9 @@ public PlatformView create(Context context, int id, Object args) { if (params.containsKey("circlesToAdd")) { builder.setInitialCircles(params.get("circlesToAdd")); } + if (params.containsKey("tileOverlaysToAdd")) { + builder.setInitialTileOverlays(params.get("tileOverlaysToAdd")); + } return builder.build(id, context, binaryMessenger, lifecycleProvider); } } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java index 9e6fa2a27236..b7f1ff3c9385 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java @@ -51,4 +51,6 @@ interface GoogleMapOptionsSink { void setInitialPolylines(Object initialPolylines); void setInitialCircles(Object initialCircles); + + void setInitialTileOverlays(Object initialTileOverlays); } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java new file mode 100644 index 000000000000..f363957d555e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java @@ -0,0 +1,41 @@ +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.TileOverlayOptions; +import com.google.android.gms.maps.model.TileProvider; + +public class TileOverlayBuilder implements TileOverlaySink { + private final TileOverlayOptions tileOverlayOptions; + + TileOverlayBuilder() { + this.tileOverlayOptions = new TileOverlayOptions(); + } + + TileOverlayOptions build() { + return tileOverlayOptions; + } + + @Override + public void setFadeIn(boolean fadeIn) { + tileOverlayOptions.fadeIn(fadeIn); + } + + @Override + public void setTransparency(float transparency) { + tileOverlayOptions.transparency(transparency); + } + + @Override + public void setZIndex(float zIndex) { + tileOverlayOptions.zIndex(zIndex); + } + + @Override + public void setVisible(boolean visible) { + tileOverlayOptions.visible(visible); + } + + @Override + public void setTileProvider(TileProvider tileProvider) { + tileOverlayOptions.tileProvider(tileProvider); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java new file mode 100644 index 000000000000..55db9e6a2d70 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java @@ -0,0 +1,46 @@ +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.TileOverlay; +import com.google.android.gms.maps.model.TileProvider; + +public class TileOverlayController implements TileOverlaySink { + + private final TileOverlay tileOverlay; + + public TileOverlayController(TileOverlay tileOverlay) { + this.tileOverlay = tileOverlay; + } + + void remove() { + tileOverlay.remove(); + } + + void clearTileCache() { + tileOverlay.clearTileCache(); + } + + @Override + public void setFadeIn(boolean fadeIn) { + tileOverlay.setFadeIn(fadeIn); + } + + @Override + public void setTransparency(float transparency) { + tileOverlay.setTransparency(transparency); + } + + @Override + public void setZIndex(float zIndex) { + tileOverlay.setZIndex(zIndex); + } + + @Override + public void setVisible(boolean visible) { + tileOverlay.setVisible(visible); + } + + @Override + public void setTileProvider(TileProvider tileProvider) { + // You can not change tile provider after creation + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java new file mode 100644 index 000000000000..bed0ae49df7d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java @@ -0,0 +1,16 @@ +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.TileProvider; + +/** Receiver of TileOverlayOptions configuration. */ +public interface TileOverlaySink { + void setFadeIn(boolean fadeIn); + + void setTransparency(float transparency); + + void setZIndex(float zIndex); + + void setVisible(boolean visible); + + void setTileProvider(TileProvider tileProvider); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java new file mode 100644 index 000000000000..fcbb6b83b865 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java @@ -0,0 +1,108 @@ +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.TileOverlay; +import com.google.android.gms.maps.model.TileOverlayOptions; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class TileOverlaysController { + + private final Map tileOverlayIdToController; + private final MethodChannel methodChannel; + private GoogleMap googleMap; + + TileOverlaysController(MethodChannel methodChannel) { + this.tileOverlayIdToController = new HashMap<>(); + this.methodChannel = methodChannel; + } + + void setGoogleMap(GoogleMap googleMap) { + this.googleMap = googleMap; + } + + void addTileOverlays(List tileOverlaysToAdd) { + if (tileOverlaysToAdd == null) { + return; + } + for (Object tileOverlayToAdd : tileOverlaysToAdd) { + addTileOverlay(tileOverlayToAdd); + } + } + + void changeTileOverlays(List tileOverlaysToChange) { + if (tileOverlaysToChange == null) { + return; + } + for (Object tileOverlayToChange : tileOverlaysToChange) { + changeTileOverlay(tileOverlayToChange); + } + } + + void removeTileOverlays(List tileOverlayIdsToRemove) { + if (tileOverlayIdsToRemove == null) { + return; + } + for (Object rawTileOverlayId : tileOverlayIdsToRemove) { + if (rawTileOverlayId == null) { + continue; + } + String tileOverlayId = (String) rawTileOverlayId; + removeTileOverlay(tileOverlayId); + } + } + + void clearTileCache(Object rawTileOverlayId) { + if (rawTileOverlayId == null) { + return; + } + String tileOverlayId = (String) rawTileOverlayId; + TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId); + if (tileOverlayController != null) { + tileOverlayController.clearTileCache(); + } + } + + private void addTileOverlay(Object tileOverlayOptions) { + if (tileOverlayOptions == null) { + return; + } + TileOverlayBuilder tileOverlayOptionsBuilder = new TileOverlayBuilder(); + String tileOverlayId = + Convert.interpretTileOverlayOptions(tileOverlayOptions, tileOverlayOptionsBuilder); + TileProviderController tileProviderController = + new TileProviderController(methodChannel, tileOverlayId); + tileOverlayOptionsBuilder.setTileProvider(tileProviderController); + TileOverlayOptions options = tileOverlayOptionsBuilder.build(); + TileOverlay tileOverlay = googleMap.addTileOverlay(options); + TileOverlayController tileOverlayController = new TileOverlayController(tileOverlay); + tileOverlayIdToController.put(tileOverlayId, tileOverlayController); + } + + private void changeTileOverlay(Object tileOverlayOptions) { + if (tileOverlayOptions == null) { + return; + } + String tileOverlayId = getTileOverlayId(tileOverlayOptions); + TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId); + if (tileOverlayController != null) { + Convert.interpretTileOverlayOptions(tileOverlayOptions, tileOverlayController); + } + } + + private void removeTileOverlay(String tileOverlayId) { + TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId); + if (tileOverlayController != null) { + tileOverlayController.remove(); + tileOverlayIdToController.remove(tileOverlayId); + } + } + + @SuppressWarnings("unchecked") + private static String getTileOverlayId(Object tileOverlay) { + Map tileOverlayMap = (Map) tileOverlay; + return (String) tileOverlayMap.get("tileOverlayId"); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java new file mode 100644 index 000000000000..215c84194973 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java @@ -0,0 +1,94 @@ +package io.flutter.plugins.googlemaps; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.android.gms.maps.model.Tile; +import com.google.android.gms.maps.model.TileProvider; +import io.flutter.plugin.common.MethodChannel; +import java.util.concurrent.CountDownLatch; + +public class TileProviderController implements TileProvider { + + private static final String TAG = "TileProviderController"; + + private final String tileOverlayId; + private final MethodChannel methodChannel; + private final Handler handler = new Handler(Looper.getMainLooper()); + + TileProviderController(MethodChannel methodChannel, String tileOverlayId) { + this.tileOverlayId = tileOverlayId; + this.methodChannel = methodChannel; + } + + @Override + public Tile getTile(final int x, final int y, final int zoom) { + Worker worker = new Worker(x, y, zoom); + return worker.getTile(); + } + + private final class Worker implements MethodChannel.Result { + + private final CountDownLatch countDownLatch = new CountDownLatch(1); + private final int x; + private final int y; + private final int zoom; + private Object result; + + Worker(int x, int y, int zoom) { + this.x = x; + this.y = y; + this.zoom = zoom; + } + + @NonNull + Tile getTile() { + handler.post( + () -> + methodChannel.invokeMethod( + "tileOverlay#getTile", + Convert.tileOverlayArgumentsToJson(tileOverlayId, x, y, zoom), + this)); + try { + countDownLatch.await(); + } catch (InterruptedException e) { + Log.e( + TAG, + String.format("countDownLatch: can't get tile: x = %d, y= %d, zoom = %d", x, y, zoom), + e); + return TileProvider.NO_TILE; + } + try { + return Convert.interpretTile(result); + } catch (Exception ex) { + Log.e(TAG, "Can't parse tile data", ex); + return TileProvider.NO_TILE; + } + } + + @Override + public void success(Object data) { + result = data; + countDownLatch.countDown(); + } + + @Override + public void error(String errorCode, String errorMessage, Object data) { + Log.e( + TAG, + String.format( + "Can't get tile: errorCode = %s, errorMessage = %s, date = %s", + errorCode, errorCode, data)); + result = null; + countDownLatch.countDown(); + } + + @Override + public void notImplemented() { + Log.e(TAG, "Can't get tile: notImplemented"); + result = null; + countDownLatch.countDown(); + } + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index 13763edb8216..b795040658e6 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart @@ -20,6 +20,7 @@ import 'place_polygon.dart'; import 'place_polyline.dart'; import 'scrolling_map.dart'; import 'snapshot.dart'; +import 'tile_overlay.dart'; final List _allPages = [ MapUiPage(), @@ -36,6 +37,7 @@ final List _allPages = [ PaddingPage(), SnapshotPage(), LiteModePage(), + TileOverlayPage(), ]; class MapsDemo extends StatelessWidget { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart new file mode 100644 index 000000000000..ebcc2c233249 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart @@ -0,0 +1,155 @@ +// 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. + +// ignore_for_file: public_member_api_docs + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'page.dart'; + +class TileOverlayPage extends GoogleMapExampleAppPage { + TileOverlayPage() : super(const Icon(Icons.map), 'Tile overlay'); + + @override + Widget build(BuildContext context) { + return const TileOverlayBody(); + } +} + +class TileOverlayBody extends StatefulWidget { + const TileOverlayBody(); + + @override + State createState() => TileOverlayBodyState(); +} + +class TileOverlayBodyState extends State { + TileOverlayBodyState(); + + GoogleMapController controller; + TileOverlay _tileOverlay; + + void _onMapCreated(GoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _removeTileOverlay() { + print("Remove tile overlay"); + setState(() { + _tileOverlay = null; + }); + } + + void _addTileOverlay() { + print("Add tile overlay"); + final TileOverlay tileOverlay = TileOverlay( + tileOverlayId: TileOverlayId("tile_overlay_1"), + tileProvider: DebugTileProvider(), + ); + setState(() { + _tileOverlay = tileOverlay; + }); + } + + void _clearTileCache() { + print("Clear tile cache"); + if (_tileOverlay != null && controller != null) { + controller.clearTileCache(_tileOverlay.tileOverlayId); + } + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: GoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(59.935460, 30.325177), + zoom: 7.0, + ), + tileOverlays: + _tileOverlay != null ? {_tileOverlay} : null, + onMapCreated: _onMapCreated, + ), + ), + ), + FlatButton( + child: const Text('Add tile overlay'), + onPressed: _addTileOverlay, + ), + FlatButton( + child: const Text('Remove tile overlay'), + onPressed: _removeTileOverlay, + ), + FlatButton( + child: const Text('Clear tile cache'), + onPressed: _clearTileCache, + ), + ], + ); + } +} + +class DebugTileProvider implements TileProvider { + DebugTileProvider() { + boxPaint.isAntiAlias = true; + boxPaint.color = Colors.blue; + boxPaint.strokeWidth = 2.0; + boxPaint.style = PaintingStyle.stroke; + } + + static const int width = 100; + static const int height = 100; + static final Paint boxPaint = Paint(); + static final TextStyle textStyle = TextStyle( + color: Colors.red, + fontSize: 20, + ); + + @override + Future getTile(int x, int y, int zoom) async { + print("TileProvider: getTile x = $x, y = $y, zoom = $zoom"); + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final TextSpan textSpan = TextSpan( + text: "$x,$y", + style: textStyle, + ); + final TextPainter textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + textPainter.layout( + minWidth: 0.0, + maxWidth: width.toDouble(), + ); + final Offset offset = const Offset(0, 0); + textPainter.paint(canvas, offset); + canvas.drawRect( + Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); + final ui.Picture picture = recorder.endRecording(); + final Uint8List byteData = await picture + .toImage(width, height) + .then((ui.Image image) => + image.toByteData(format: ui.ImageByteFormat.png)) + .then((ByteData byteData) => byteData.buffer.asUint8List()); + return Tile(width, height, byteData); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.h index 645ace34f9ed..780243f45682 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.h +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.h @@ -9,6 +9,7 @@ #import "GoogleMapMarkerController.h" #import "GoogleMapPolygonController.h" #import "GoogleMapPolylineController.h" +#import "GoogleMapTileOverlayController.h" @interface FLTGoogleMapsPlugin : NSObject @end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.h index 1bc8536f97d9..113d290d4d73 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.h +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.h @@ -8,6 +8,7 @@ #import "GoogleMapMarkerController.h" #import "GoogleMapPolygonController.h" #import "GoogleMapPolylineController.h" +#import "GoogleMapTileOverlayController.h" NS_ASSUME_NONNULL_BEGIN diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m index 321ddd318536..c28cd3a39e4f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -55,6 +55,7 @@ @implementation FLTGoogleMapController { FLTPolygonsController* _polygonsController; FLTPolylinesController* _polylinesController; FLTCirclesController* _circlesController; + FLTTileOverlaysController* _tileOverlaysController; } - (instancetype)initWithFrame:(CGRect)frame @@ -94,6 +95,9 @@ - (instancetype)initWithFrame:(CGRect)frame _circlesController = [[FLTCirclesController alloc] init:_channel mapView:_mapView registrar:registrar]; + _tileOverlaysController = [[FLTTileOverlaysController alloc] init:_channel + mapView:_mapView + registrar:registrar]; id markersToAdd = args[@"markersToAdd"]; if ([markersToAdd isKindOfClass:[NSArray class]]) { [_markersController addMarkers:markersToAdd]; @@ -110,6 +114,10 @@ - (instancetype)initWithFrame:(CGRect)frame if ([circlesToAdd isKindOfClass:[NSArray class]]) { [_circlesController addCircles:circlesToAdd]; } + id tileOverlaysToAdd = args[@"tileOverlaysToAdd"]; + if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) { + [_tileOverlaysController addTileOverlays:tileOverlaysToAdd]; + } } return self; } @@ -298,6 +306,24 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [_circlesController removeCircleIds:circleIdsToRemove]; } result(nil); + } else if ([call.method isEqualToString:@"tileOverlays#update"]) { + id tileOverlaysToAdd = call.arguments[@"tileOverlaysToAdd"]; + if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) { + [_tileOverlaysController addTileOverlays:tileOverlaysToAdd]; + } + id tileOverlaysToChange = call.arguments[@"tileOverlaysToChange"]; + if ([tileOverlaysToChange isKindOfClass:[NSArray class]]) { + [_tileOverlaysController changeTileOverlays:tileOverlaysToChange]; + } + id tileOverlayIdsToRemove = call.arguments[@"tileOverlayIdsToRemove"]; + if ([tileOverlayIdsToRemove isKindOfClass:[NSArray class]]) { + [_tileOverlaysController removeTileOverlayIds:tileOverlayIdsToRemove]; + } + result(nil); + } else if ([call.method isEqualToString:@"tileOverlays#clearTileCache"]) { + id rawTileOverlayId = call.arguments[@"tileOverlayId"]; + [_tileOverlaysController clearTileCache:rawTileOverlayId]; + result(nil); } else if ([call.method isEqualToString:@"map#isCompassEnabled"]) { NSNumber* isCompassEnabled = @(_mapView.settings.compassButton); result(isCompassEnabled); diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapTileOverlayController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapTileOverlayController.h new file mode 100644 index 000000000000..2e277fb8490a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapTileOverlayController.h @@ -0,0 +1,35 @@ +// 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 +#import + +@protocol FLTGoogleMapTileOverlayOptionsSink +- (void)setFadeIn:(BOOL)fadeIn; +- (void)setTransparency:(float)transparency; +- (void)setZIndex:(int)zIndex; +- (void)setVisible:(BOOL)visible; +- (void)setTileSize:(NSInteger)tileSize; +@end + +@interface FLTGoogleMapTileOverlayController : NSObject +- (instancetype)initWithTileLayer:(GMSTileLayer *)tileLayer mapView:(GMSMapView *)mapView; +- (void)removeTileOverlay; +- (void)clearTileCache; +@end + +@interface FLTTileProviderController : GMSTileLayer +@property(atomic, readonly) NSString *tileOverlayId; +- (instancetype)init:(FlutterMethodChannel *)methodChannel tileOverlayId:(NSString *)tileOverlayId; +@end + +@interface FLTTileOverlaysController : NSObject +- (instancetype)init:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar; +- (void)addTileOverlays:(NSArray *)tileOverlaysToAdd; +- (void)changeTileOverlays:(NSArray *)tileOverlaysToChange; +- (void)removeTileOverlayIds:(NSArray *)tileOverlayIdsToRemove; +- (void)clearTileCache:(NSString *)tileOverlayId; +@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapTileOverlayController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapTileOverlayController.m new file mode 100644 index 000000000000..54a6b42ec867 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapTileOverlayController.m @@ -0,0 +1,198 @@ +// 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 "GoogleMapTileOverlayController.h" +#import "JsonConversions.h" + +@implementation FLTGoogleMapTileOverlayController { + GMSTileLayer* _layer; + GMSMapView* _mapView; +} + +- (instancetype)initWithTileLayer:(GMSTileLayer*)tileLayer mapView:(GMSMapView*)mapView { + self = [super init]; + if (self) { + _layer = tileLayer; + _mapView = mapView; + } + return self; +} + +- (void)removeTileOverlay { + _layer.map = nil; +} + +- (void)clearTileCache { + [_layer clearTileCache]; +} + +#pragma mark - FLTGoogleMapTileOverlayOptionsSink methods + +- (void)setFadeIn:(BOOL)fadeIn { + _layer.fadeIn = fadeIn; +} + +- (void)setTransparency:(float)transparency { + float opacity = 1.0 - transparency; + _layer.opacity = opacity; +} + +- (void)setVisible:(BOOL)visible { + _layer.map = visible ? _mapView : nil; +} + +- (void)setZIndex:(int)zIndex { + _layer.zIndex = zIndex; +} + +- (void)setTileSize:(NSInteger)tileSize { + _layer.tileSize = tileSize; +} +@end + +@implementation FLTTileProviderController { + FlutterMethodChannel* _methodChannel; + NSString* _tileOverlayId; +} +- (instancetype)init:(FlutterMethodChannel*)methodChannel tileOverlayId:(NSString*)tileOverlayId { + self = [super init]; + if (self) { + _methodChannel = methodChannel; + _tileOverlayId = tileOverlayId; + } + return self; +} + +- (void)requestTileForX:(NSUInteger)x + y:(NSUInteger)y + zoom:(NSUInteger)zoom + receiver:(id)receiver { + NSNumber* xn = [NSNumber numberWithUnsignedInteger:x]; + NSNumber* yn = [NSNumber numberWithUnsignedInteger:y]; + NSNumber* zoomn = [NSNumber numberWithUnsignedInteger:zoom]; + + [_methodChannel + invokeMethod:@"tileOverlay#getTile" + arguments:@{@"tileOverlayId" : _tileOverlayId, @"x" : xn, @"y" : yn, @"zoom" : zoomn} + result:^(id _Nullable result) { + UIImage* tileImage; + if ([result isKindOfClass:[NSDictionary class]]) { + FlutterStandardTypedData* typedData = (FlutterStandardTypedData*)result[@"data"]; + tileImage = [UIImage imageWithData:typedData.data]; + } else { + if ([result isKindOfClass:[FlutterError class]]) { + FlutterError* error = (FlutterError*)result; + NSLog(@"Can't get tile: errorCode = %@, errorMessage = %@, date = %@", + [error code], [error message], [error details]); + } + if ([result isKindOfClass:[FlutterMethodNotImplemented class]]) { + NSLog(@"Can't get tile: notImplemented"); + } + tileImage = kGMSTileLayerNoTile; + } + + [receiver receiveTileWithX:x y:y zoom:zoom image:tileImage]; + }]; +} +@end + +static BOOL ToBool(NSNumber* data) { return [FLTGoogleMapJsonConversions toBool:data]; } + +static float ToFloat(NSNumber* data) { return [FLTGoogleMapJsonConversions toFloat:data]; } + +static int ToInt(NSNumber* data) { return [FLTGoogleMapJsonConversions toInt:data]; } + +static void InterpretTileOverlayOptions(NSDictionary* data, + id sink, + NSObject* registrar) { + NSNumber* visible = data[@"visible"]; + if (visible != nil) { + [sink setVisible:ToBool(visible)]; + } + + NSNumber* transparency = data[@"transparency"]; + if (transparency != nil) { + [sink setTransparency:ToFloat(transparency)]; + } + + NSNumber* zIndex = data[@"zIndex"]; + if (zIndex != nil) { + [sink setZIndex:ToInt(zIndex)]; + } + + NSNumber* fadeIn = data[@"fadeIn"]; + if (fadeIn != nil) { + [sink setFadeIn:ToBool(fadeIn)]; + } + + NSNumber* tileSize = data[@"tileSize"]; + if (tileSize != nil) { + [sink setTileSize:[tileSize integerValue]]; + } +} + +@implementation FLTTileOverlaysController { + NSMutableDictionary* _tileOverlayIdToController; + FlutterMethodChannel* _methodChannel; + NSObject* _registrar; + GMSMapView* _mapView; +} +- (instancetype)init:(FlutterMethodChannel*)methodChannel + mapView:(GMSMapView*)mapView + registrar:(NSObject*)registrar { + self = [super init]; + if (self) { + _methodChannel = methodChannel; + _mapView = mapView; + _tileOverlayIdToController = [NSMutableDictionary dictionaryWithCapacity:1]; + _registrar = registrar; + } + return self; +} + +- (void)addTileOverlays:(NSArray*)tileOverlaysToAdd { + for (NSDictionary* tileOverlay in tileOverlaysToAdd) { + NSString* tileOverlayId = [FLTTileOverlaysController getTileOverlayId:tileOverlay]; + FLTTileProviderController* tileProvider = + [[FLTTileProviderController alloc] init:_methodChannel tileOverlayId:tileOverlayId]; + FLTGoogleMapTileOverlayController* controller = + [[FLTGoogleMapTileOverlayController alloc] initWithTileLayer:tileProvider mapView:_mapView]; + InterpretTileOverlayOptions(tileOverlay, controller, _registrar); + _tileOverlayIdToController[tileOverlayId] = controller; + } +} + +- (void)changeTileOverlays:(NSArray*)tileOverlaysToChange { + for (NSDictionary* tileOverlay in tileOverlaysToChange) { + NSString* tileOverlayId = [FLTTileOverlaysController getTileOverlayId:tileOverlay]; + FLTGoogleMapTileOverlayController* controller = _tileOverlayIdToController[tileOverlayId]; + if (!controller) { + continue; + } + InterpretTileOverlayOptions(tileOverlay, controller, _registrar); + } +} +- (void)removeTileOverlayIds:(NSArray*)tileOverlayIdsToRemove { + for (NSString* tileOverlayId in tileOverlayIdsToRemove) { + FLTGoogleMapTileOverlayController* controller = _tileOverlayIdToController[tileOverlayId]; + if (!controller) { + continue; + } + [controller removeTileOverlay]; + [_tileOverlayIdToController removeObjectForKey:tileOverlayId]; + } +} + +- (void)clearTileCache:(NSString*)tileOverlayId { + FLTGoogleMapTileOverlayController* controller = _tileOverlayIdToController[tileOverlayId]; + if (!controller) { + return; + } + [controller clearTileCache]; +} + ++ (NSString*)getTileOverlayId:(NSDictionary*)tileOverlay { + return tileOverlay[@"tileOverlayId"]; +} +@end diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart index b879f3d302cf..e06f141c187b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart @@ -43,7 +43,11 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf PolygonId, Polyline, PolylineId, - ScreenCoordinate; + ScreenCoordinate, + Tile, + TileOverlayId, + TileOverlay, + TileProvider; part 'src/controller.dart'; part 'src/google_map.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index f47b8e57b049..764b484f756e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -18,6 +18,8 @@ class GoogleMapController { @required this.mapId, }) : assert(_googleMapsFlutterPlatform != null) { _connectStreams(mapId); + _googleMapsFlutterPlatform.setGetTileCallback( + mapId: mapId, callback: _googleMapState._onGetTile); } /// Initialize control of a [GoogleMap] with [id]. @@ -152,6 +154,29 @@ class GoogleMapController { mapId: mapId); } + /// Updates tile overlays 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 _updateTileOverlays(TileOverlayUpdates tileOverlayUpdates) { + assert(tileOverlayUpdates != null); + return _googleMapsFlutterPlatform.updateTileOverlays(tileOverlayUpdates, + mapId: mapId); + } + + /// Clears the tile cache so that all tiles will be requested again from the + /// [TileProvider]. The current tiles from this tile overlay will also be + /// cleared from the map after calling this method. The API maintains a small + /// in-memory cache of tiles. If you want to cache tiles for longer, you + /// should implement an on-disk cache. + Future clearTileCache(TileOverlayId tileOverlayId) async { + assert(tileOverlayId != null); + return _googleMapsFlutterPlatform.clearTileCache(tileOverlayId, + mapId: mapId); + } + /// Starts an animated change of the map camera position. /// /// The returned [Future] completes after the change has been started on the diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index d7f0f1a4e280..37571f56a5b0 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -49,6 +49,7 @@ class GoogleMap extends StatefulWidget { this.polygons, this.polylines, this.circles, + this.tileOverlays, this.onCameraMoveStarted, this.onCameraMove, this.onCameraIdle, @@ -120,6 +121,9 @@ class GoogleMap extends StatefulWidget { /// Circles to be placed on the map. final Set circles; + /// Tile overlays to be placed on the map. + final Set tileOverlays; + /// Called when the camera starts moving. /// /// This can be initiated by the following: @@ -220,6 +224,8 @@ class _GoogleMapState extends State { Map _polygons = {}; Map _polylines = {}; Map _circles = {}; + Map _tileOverlays = + {}; _GoogleMapOptions _googleMapOptions; @override @@ -231,6 +237,7 @@ class _GoogleMapState extends State { 'polygonsToAdd': serializePolygonSet(widget.polygons), 'polylinesToAdd': serializePolylineSet(widget.polylines), 'circlesToAdd': serializeCircleSet(widget.circles), + 'tileOverlaysToAdd': serializeTileOverlaySet(widget.tileOverlays), '_webOnlyMapCreationId': _webOnlyMapCreationId, }; @@ -249,6 +256,7 @@ class _GoogleMapState extends State { _polygons = keyByPolygonId(widget.polygons); _polylines = keyByPolylineId(widget.polylines); _circles = keyByCircleId(widget.circles); + _tileOverlays = keyTileOverlayId(widget.tileOverlays); } @override @@ -266,6 +274,7 @@ class _GoogleMapState extends State { _updatePolygons(); _updatePolylines(); _updateCircles(); + _updateTileOverlays(); } void _updateOptions() async { @@ -313,6 +322,14 @@ class _GoogleMapState extends State { _circles = keyByCircleId(widget.circles); } + void _updateTileOverlays() async { + final GoogleMapController controller = await _controller.future; + // ignore: unawaited_futures + controller._updateTileOverlays(TileOverlayUpdates.from( + _tileOverlays.values.toSet(), widget.tileOverlays)); + _tileOverlays = keyTileOverlayId(widget.tileOverlays); + } + Future onPlatformViewCreated(int id) async { final GoogleMapController controller = await GoogleMapController.init( id, @@ -376,6 +393,25 @@ class _GoogleMapState extends State { widget.onLongPress(position); } } + + // Returns the [Tile] from an added [TileOverlay]. + // + // If the TileOverlay or his TileProvider is not found, + // a [TileProvider.noTile] is returned. + Future _onGetTile( + String tileOverlayIdRaw, int x, int y, int zoom) async { + assert(tileOverlayIdRaw != null); + final TileOverlayId tileOverlayId = TileOverlayId(tileOverlayIdRaw); + final TileOverlay tileOverlay = _tileOverlays[tileOverlayId]; + Tile tile; + if (tileOverlay != null && tileOverlay.tileProvider != null) { + tile = await tileOverlay.tileProvider.getTile(x, y, zoom); + } + if (tile == null) { + tile = TileProvider.noTile; + } + return tile; + } } /// Configuration options for the GoogleMaps user interface. diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 06d5fb2f4257..6eeb12af30cd 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -1,13 +1,14 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter -version: 1.0.7 +version: 1.0.8 dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^1.0.0 - google_maps_flutter_platform_interface: ^1.0.4 + google_maps_flutter_platform_interface: + path: ../google_maps_flutter_platform_interface dev_dependencies: flutter_test: diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart index adca8b4c2a0e..7149df8a1d3a 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart @@ -19,6 +19,7 @@ class FakePlatformGoogleMap { updatePolygons(params); updatePolylines(params); updateCircles(params); + updateTileOverlays(params); } MethodChannel channel; @@ -83,6 +84,12 @@ class FakePlatformGoogleMap { Set circlesToChange; + Set tileOverlayIdsToRemove; + + Set tileOverlaysToAdd; + + Set tileOverlaysToChange; + Future onMethodCall(MethodCall call) { switch (call.method) { case 'map#update': @@ -100,6 +107,9 @@ class FakePlatformGoogleMap { case 'circles#update': updateCircles(call.arguments); return Future.sync(() {}); + case 'tileOverlays#update': + updateTileOverlays(call.arguments); + return Future.sync(() {}); default: return Future.sync(() {}); } @@ -282,6 +292,18 @@ class FakePlatformGoogleMap { circlesToChange = _deserializeCircles(circleUpdates['circlesToChange']); } + void updateTileOverlays(Map updateTileOverlayUpdates) { + if (updateTileOverlayUpdates == null) { + return; + } + tileOverlaysToAdd = + _deserializeTileOverlays(updateTileOverlayUpdates['tileOverlaysToAdd']); + tileOverlayIdsToRemove = _deserializeTileOverlayIds( + updateTileOverlayUpdates['tileOverlayIdsToRemove']); + tileOverlaysToChange = _deserializeTileOverlays( + updateTileOverlayUpdates['tileOverlaysToChange']); + } + Set _deserializeCircleIds(List circleIds) { if (circleIds == null) { // TODO(iskakaushik): Remove this when collection literals makes it to stable. @@ -319,6 +341,49 @@ class FakePlatformGoogleMap { return result; } + Set _deserializeTileOverlayIds(List tileOverlayIds) { + if (tileOverlayIds == null) { + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // https://github.com/flutter/flutter/issues/28312 + // ignore: prefer_collection_literals + return Set(); + } + return tileOverlayIds + .map((dynamic tileOverlayId) => TileOverlayId(tileOverlayId)) + .toSet(); + } + + Set _deserializeTileOverlays(dynamic tileOverlays) { + if (tileOverlays == null) { + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // https://github.com/flutter/flutter/issues/28312 + // ignore: prefer_collection_literals + return Set(); + } + final List tileOverlaysData = tileOverlays; + // TODO(iskakaushik): Remove this when collection literals makes it to stable. + // https://github.com/flutter/flutter/issues/28312 + // ignore: prefer_collection_literals + final Set result = Set(); + for (Map tileOverlayData in tileOverlaysData) { + final String tileOverlayId = tileOverlayData['tileOverlayId']; + final bool fadeIn = tileOverlayData['fadeIn']; + final double transparency = tileOverlayData['transparency']; + final int zIndex = tileOverlayData['zIndex']; + final bool visible = tileOverlayData['visible']; + + result.add(TileOverlay( + tileOverlayId: TileOverlayId(tileOverlayId), + fadeIn: fadeIn, + transparency: transparency, + zIndex: zIndex, + visible: visible, + )); + } + + return result; + } + void updateOptions(Map options) { if (options.containsKey('compassEnabled')) { compassEnabled = options['compassEnabled']; diff --git a/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart new file mode 100644 index 000000000000..b94d4906dec7 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart @@ -0,0 +1,210 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'fake_maps_controllers.dart'; + +Set _toSet({TileOverlay t1, TileOverlay t2, TileOverlay t3}) { + final Set res = Set.identity(); + if (t1 != null) { + res.add(t1); + } + if (t2 != null) { + res.add(t2); + } + if (t3 != null) { + res.add(t3); + } + return res; +} + +Widget _mapWithTileOverlays(Set tileOverlays) { + return Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + tileOverlays: tileOverlays, + ), + ); +} + +void main() { + final FakePlatformViewsController fakePlatformViewsController = + FakePlatformViewsController(); + + setUpAll(() { + SystemChannels.platform_views.setMockMethodCallHandler( + fakePlatformViewsController.fakePlatformViewsMethodHandler); + }); + + setUp(() { + fakePlatformViewsController.reset(); + }); + + testWidgets('Initializing a tile overlay', (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t1))); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.tileOverlaysToAdd.length, 1); + + final TileOverlay initializedTileOverlay = + platformGoogleMap.tileOverlaysToAdd.first; + expect(initializedTileOverlay, equals(t1)); + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + }); + + testWidgets("Adding a tile overlay", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + final TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + + await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t1))); + await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t1, t2: t2))); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.tileOverlaysToAdd.length, 1); + + final TileOverlay addedTileOverlay = + platformGoogleMap.tileOverlaysToAdd.first; + expect(addedTileOverlay, equals(t2)); + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + + expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + }); + + testWidgets("Removing a tile overlay", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + + await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t1))); + await tester.pumpWidget(_mapWithTileOverlays(null)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.tileOverlayIdsToRemove.length, 1); + expect(platformGoogleMap.tileOverlayIdsToRemove.first, + equals(t1.tileOverlayId)); + + expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + }); + + testWidgets("Updating a tile overlay", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + final TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"), zIndex: 10); + + await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t1))); + await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t2))); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.tileOverlaysToChange.length, 1); + expect(platformGoogleMap.tileOverlaysToChange.first, equals(t2)); + + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + }); + + testWidgets("Updating a tile overlay", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + final TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"), zIndex: 10); + + await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t1))); + await tester.pumpWidget(_mapWithTileOverlays(_toSet(t1: t2))); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + expect(platformGoogleMap.tileOverlaysToChange.length, 1); + + final TileOverlay update = platformGoogleMap.tileOverlaysToChange.first; + expect(update, equals(t2)); + expect(update.zIndex, 10); + }); + + testWidgets("Multi Update", (WidgetTester tester) async { + TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + final Set prev = _toSet(t1: t1, t2: t2); + t1 = TileOverlay( + tileOverlayId: TileOverlayId("tile_overlay_1"), visible: false); + t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"), zIndex: 10); + final Set cur = _toSet(t1: t1, t2: t2); + + await tester.pumpWidget(_mapWithTileOverlays(prev)); + await tester.pumpWidget(_mapWithTileOverlays(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.tileOverlaysToChange, cur); + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + }); + + testWidgets("Multi Update", (WidgetTester tester) async { + TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + final TileOverlay t3 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3")); + final Set prev = _toSet(t2: t2, t3: t3); + + // t1 is added, t2 is updated, t3 is removed. + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"), zIndex: 10); + final Set cur = _toSet(t1: t1, t2: t2); + + await tester.pumpWidget(_mapWithTileOverlays(prev)); + await tester.pumpWidget(_mapWithTileOverlays(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.tileOverlaysToChange.length, 1); + expect(platformGoogleMap.tileOverlaysToAdd.length, 1); + expect(platformGoogleMap.tileOverlayIdsToRemove.length, 1); + + expect(platformGoogleMap.tileOverlaysToChange.first, equals(t2)); + expect(platformGoogleMap.tileOverlaysToAdd.first, equals(t1)); + expect(platformGoogleMap.tileOverlayIdsToRemove.first, + equals(t3.tileOverlayId)); + }); + + testWidgets("Partial Update", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + final TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + TileOverlay t3 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3")); + final Set prev = _toSet(t1: t1, t2: t2, t3: t3); + t3 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3"), zIndex: 10); + final Set cur = _toSet(t1: t1, t2: t2, t3: t3); + + await tester.pumpWidget(_mapWithTileOverlays(prev)); + await tester.pumpWidget(_mapWithTileOverlays(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView; + + expect(platformGoogleMap.tileOverlaysToChange, _toSet(t3: t3)); + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + }); +} 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 index 0586ac414d97..a1c7c8547105 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.6 + +* Add TileOverlay support. + ## 1.0.5 * Temporarily add a `fromJson` constructor to `BitmapDescriptor` so serialized descriptors can be synchronously re-hydrated. This will be removed when a fix for [this issue](https://github.com/flutter/flutter/issues/70330) lands. 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 index 31392354d946..ba62d0e6ee77 100644 --- 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 @@ -33,6 +33,10 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { return _channels[mapId]; } + // Keep a collection of id -> GetTileCallback + // Every method call passes the int mapId + final Map _getTileCallbacks = {}; + /// Initializes the platform interface with [id]. /// /// This method is called when the plugin is first initialized. @@ -184,6 +188,15 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { LatLng.fromJson(call.arguments['position']), )); break; + case 'tileOverlay#getTile': + final getTileCallback = _getTileCallbacks[mapId]; + final Tile tile = await getTileCallback( + call.arguments['tileOverlayId'], + call.arguments['x'], + call.arguments['y'], + call.arguments['zoom'], + ); + return tile.toJson(); default: throw MissingPluginException(); } @@ -281,6 +294,40 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } + /// Updates tile overlay 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 updateTileOverlays( + TileOverlayUpdates tileOverlayUpdates, { + @required int mapId, + }) { + assert(tileOverlayUpdates != null); + return channel(mapId).invokeMethod( + 'tileOverlays#update', + tileOverlayUpdates.toJson(), + ); + } + + /// Clears the tile cache so that all tiles will be requested again from the + /// [TileProvider]. The current tiles from this tile overlay will also be + /// cleared from the map after calling this method. The API maintains a small + /// in-memory cache of tiles. If you want to cache tiles for longer, you + /// should implement an on-disk cache. + @override + Future clearTileCache( + TileOverlayId tileOverlayId, { + @required int mapId, + }) { + return channel(mapId) + .invokeMethod('tileOverlays#clearTileCache', { + 'tileOverlayId': tileOverlayId.value, + }); + } + /// Starts an animated change of the map camera position. /// /// The returned [Future] completes after the change has been started on the @@ -451,6 +498,11 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { return channel(mapId).invokeMethod('map#takeSnapshot'); } + @override + void setGetTileCallback({@required int mapId, MapGetTileCallback callback}) { + _getTileCallbacks[mapId] = callback; + } + /// 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 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 index a4f487740811..a5fd5d69807a 100644 --- 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 @@ -15,6 +15,10 @@ import 'package:google_maps_flutter_platform_interface/src/method_channel/method import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +/// Callback method for when a [Tile] is requested from a [TileProvider] +typedef Future MapGetTileCallback( + String tileOverlayId, int x, int y, int zoom); + /// The interface that platform-specific implementations of `google_maps_flutter` must extend. /// /// Avoid `implements` of this interface. Using `implements` makes adding any new @@ -115,6 +119,31 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('updateCircles() has not been implemented.'); } + /// Updates tile overlay 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 updateTileOverlays( + TileOverlayUpdates tileOverlayUpdates, { + @required int mapId, + }) { + throw UnimplementedError('updateTileOverlays() has not been implemented.'); + } + + /// Clears the tile cache so that all tiles will be requested again from the + /// [TileProvider]. The current tiles from this tile overlay will also be + /// cleared from the map after calling this method. The API maintains a small + /// in-memory cache of tiles. If you want to cache tiles for longer, you + /// should implement an on-disk cache. + Future clearTileCache( + TileOverlayId tileOverlayId, { + @required int mapId, + }) { + throw UnimplementedError('clearTileCache() 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 @@ -246,6 +275,12 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('takeSnapshot() has not been implemented.'); } + /// Set the [MapGetTileCallback] for the map, which will be called + /// when a [Tile] is requested for an added [TileProvider]. + void setGetTileCallback({@required int mapId, MapGetTileCallback callback}) { + throw UnimplementedError('onGetTile() has not been implemented.'); + } + // The following are the 11 possible streams of data from the native side // into the plugin diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart new file mode 100644 index 000000000000..3fe49fcbe1fb --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart @@ -0,0 +1,40 @@ +// 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:typed_data'; + +import 'package:meta/meta.dart' show immutable; + +/// Contains information about a Tile that is returned by a [TileProvider]. +@immutable +class Tile { + /// Creates an immutable representation of a [Tile] to draw by [TileProvider]. + const Tile(this.width, this.height, this.data); + + /// The width of the image encoded by data in pixels. + final int width; + + /// The height of the image encoded by data in pixels. + final int height; + + /// A byte array containing the image data. + final Uint8List data; + + /// 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('width', width); + addIfPresent('height', height); + addIfPresent('data', data); + + return json; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart new file mode 100644 index 000000000000..374dd19c20c3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart @@ -0,0 +1,158 @@ +// 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'; +import 'package:meta/meta.dart' show immutable, required; + +/// Uniquely identifies a [TileOverlay] among [GoogleMap] tile overlays. +/// +/// This does not have to be globally unique, only unique among the list. +@immutable +class TileOverlayId { + /// Creates an immutable identifier for a [TileOverlay]. + TileOverlayId(this.value) : assert(value != null); + + /// value of the [TileOverlayId]. + final String value; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final TileOverlayId typedOther = other; + return value == typedOther.value; + } + + @override + int get hashCode => value.hashCode; + + @override + String toString() { + return 'TileOverlay{value: $value}'; + } +} + +/// A Tile Overlay is a set of images which are displayed on top of the base map tiles. +/// These tiles may be transparent, allowing you to add features to existing maps. +/// +/// Tile Coordinates +/// +/// Note that the world is projected using the Mercator projection +/// (see [Wikipedia](https://en.wikipedia.org/wiki/Mercator_projection)) with the left (west) side +/// of the map corresponding to -180 degrees of longitude and the right (east) side of the map +/// corresponding to 180 degrees of longitude. To make the map square, the top (north) side of the +/// map corresponds to 85.0511 degrees of latitude and the bottom (south) side of the map +/// corresponds to -85.0511 degrees of latitude. Areas outside this latitude range are not rendered. +/// +/// At each zoom level, the map is divided into tiles and only the tiles that overlap the screen are +/// downloaded and rendered. Each tile is square and the map is divided into tiles as follows: +/// +/// * At zoom level 0, one tile represents the entire world. The coordinates of that tile are +/// (x, y) = (0, 0). +/// * At zoom level 1, the world is divided into 4 tiles arranged in a 2 x 2 grid. +/// * ... +/// * At zoom level N, the world is divided into 4N tiles arranged in a 2N x 2N grid. +/// +/// Note that the minimum zoom level that the camera supports (which can depend on various factors) +/// is GoogleMap.getMinZoomLevel and the maximum zoom level is GoogleMap.getMaxZoomLevel. +/// +/// The coordinates of the tiles are measured from the top left (northwest) corner of the map. +/// At zoom level N, the x values of the tile coordinates range from 0 to 2N - 1 and increase from +/// west to east and the y values range from 0 to 2N - 1 and increase from north to south. +class TileOverlay { + /// Creates an immutable representation of a [TileOverlay] to draw on [GoogleMap]. + const TileOverlay({ + @required this.tileOverlayId, + this.fadeIn = true, + this.tileProvider, + this.transparency = 0.0, + this.zIndex, + this.visible = true, + this.tileSize = 256, + }) : assert(transparency >= 0.0 && transparency <= 1.0); + + /// Uniquely identifies a [TileOverlay]. + final TileOverlayId tileOverlayId; + + /// Whether the tiles should fade in. The default is true. + final bool fadeIn; + + /// The tile provider to use for this tile overlay. + final TileProvider tileProvider; + + /// The transparency of the tile overlay. The default transparency is 0 (opaque). + final double transparency; + + /// The tile overlay's zIndex, i.e., the order in which it will be drawn where + /// overlays with larger values are drawn above those with lower values + final int zIndex; + + /// The visibility for the tile overlay. The default visibility is true. + final bool visible; + + /// Specifies the number of pixels (not points) that the returned tile images will prefer + /// to display as. iOS only. + /// + /// Defaults to 256, which is the traditional size of Google Maps tiles. + /// As an example, an application developer may wish to provide retina tiles (512 pixel edge length) + /// on retina devices, to keep the same number of tiles per view as the default value of 256 + /// would give on a non-retina device. + final int tileSize; + + /// Creates a new [Polygon] object whose values are the same as this instance, + /// unless overwritten by the specified parameters. + TileOverlay copyWith({ + TileOverlayId tileOverlayId, + bool fadeInParam, + double transparencyParam, + int zIndexParam, + bool visibleParam, + int tileSizeParam, + }) { + return TileOverlay( + tileOverlayId: tileOverlayId, + fadeIn: fadeInParam ?? fadeIn, + transparency: transparencyParam ?? transparency, + zIndex: zIndexParam ?? zIndex, + visible: visibleParam ?? visible, + tileSize: tileSizeParam ?? tileSize, + ); + } + + /// 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('tileOverlayId', tileOverlayId.value); + addIfPresent('fadeIn', fadeIn); + addIfPresent('transparency', transparency); + addIfPresent('zIndex', zIndex); + addIfPresent('visible', visible); + addIfPresent('tileSize', tileSize); + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final TileOverlay typedOther = other; + return tileOverlayId == typedOther.tileOverlayId && + fadeIn == typedOther.fadeIn && + transparency == typedOther.transparency && + zIndex == typedOther.zIndex && + visible == typedOther.visible && + tileSize == typedOther.tileSize; + } + + @override + int get hashCode => tileOverlayId.hashCode; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.dart new file mode 100644 index 000000000000..8ab1466eacad --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.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 'package:flutter/foundation.dart' show setEquals; + +import 'utils/tile_overlay.dart'; +import 'types.dart'; + +/// [TileProvider] update events to be applied to the [GoogleMap]. +/// +/// Used in [GoogleMapController] when the map is updated. +class TileOverlayUpdates { + /// Computes [TileOverlayUpdates] given previous and current [TileOverlay]s. + TileOverlayUpdates.from(Set previous, Set current) { + if (previous == null) { + previous = Set.identity(); + } + + if (current == null) { + current = Set.identity(); + } + + final Map previousTileOverlays = + keyTileOverlayId(previous); + final Map currentTileOverlays = + keyTileOverlayId(current); + + final Set prevTileOverlayIds = + previousTileOverlays.keys.toSet(); + final Set currentTileOverlayIds = + currentTileOverlays.keys.toSet(); + + TileOverlay idToCurrentTileOverlay(TileOverlayId id) { + return currentTileOverlays[id]; + } + + final Set _tileOverlayIdsToRemove = + prevTileOverlayIds.difference(currentTileOverlayIds); + + final Set _tileOverlaysToAdd = currentTileOverlayIds + .difference(prevTileOverlayIds) + .map(idToCurrentTileOverlay) + .toSet(); + + /// Returns `true` if [current] is not equals to previous one with the + /// same id. + bool hasChanged(TileOverlay current) { + final TileOverlay previous = previousTileOverlays[current.tileOverlayId]; + return current != previous; + } + + final Set _tileOverlaysToChange = currentTileOverlayIds + .intersection(prevTileOverlayIds) + .map(idToCurrentTileOverlay) + .where(hasChanged) + .toSet(); + + tileOverlaysToAdd = _tileOverlaysToAdd; + tileOverlayIdsToRemove = _tileOverlayIdsToRemove; + tileOverlaysToChange = _tileOverlaysToChange; + } + + /// Set of TileOverlays to be added in this update. + Set tileOverlaysToAdd; + + /// Set of TileOverlayIds to be removed in this update. + Set tileOverlayIdsToRemove; + + /// Set of TileOverlays to be changed in this update. + Set tileOverlaysToChange; + + /// 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( + 'tileOverlaysToAdd', serializeTileOverlaySet(tileOverlaysToAdd)); + addIfNonNull( + 'tileOverlaysToChange', serializeTileOverlaySet(tileOverlaysToChange)); + addIfNonNull( + 'tileOverlayIdsToRemove', + tileOverlayIdsToRemove + .map((TileOverlayId m) => m.value) + .toList()); + + return updateMap; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final TileOverlayUpdates typedOther = other; + return setEquals(tileOverlaysToAdd, typedOther.tileOverlaysToAdd) && + setEquals(tileOverlayIdsToRemove, typedOther.tileOverlayIdsToRemove) && + setEquals(tileOverlaysToChange, typedOther.tileOverlaysToChange); + } + + @override + int get hashCode => hashValues( + tileOverlaysToAdd, tileOverlayIdsToRemove, tileOverlaysToChange); + + @override + String toString() { + return 'TileOverlayUpdates{tileOverlaysToAdd: $tileOverlaysToAdd, ' + 'tileOverlayIdsToRemove: $tileOverlayIdsToRemove, ' + 'tileOverlaysToChange: $tileOverlaysToChange}'; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.dart new file mode 100644 index 000000000000..f0754159c941 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.dart @@ -0,0 +1,14 @@ +// 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'; + +/// An interface for a class that provides the tile images for a TileOverlay. +abstract class TileProvider { + /// Stub tile that is used to indicate that no tile exists for a specific tile coordinate. + static const Tile noTile = Tile(-1, -1, null); + + /// Returns the tile to be used for this tile coordinate. + Future getTile(int x, int y, int zoom); +} 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 index e56c3a5dd646..b872f2ff84ed 100644 --- 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 @@ -19,6 +19,10 @@ export 'polygon.dart'; export 'polyline_updates.dart'; export 'polyline.dart'; export 'screen_coordinate.dart'; +export 'tile.dart'; +export 'tile_overlay.dart'; +export 'tile_overlay_updates.dart'; +export 'tile_provider.dart'; export 'ui.dart'; // Export the utils, they're used by the Widget @@ -26,3 +30,4 @@ export 'utils/circle.dart'; export 'utils/marker.dart'; export 'utils/polygon.dart'; export 'utils/polyline.dart'; +export 'utils/tile_overlay.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.dart new file mode 100644 index 000000000000..0736c836481f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.dart @@ -0,0 +1,23 @@ +import '../types.dart'; + +/// Converts an [Iterable] of TileOverlay in a Map of TileOverlayId -> TileOverlay. +Map keyTileOverlayId( + Iterable tileOverlays) { + if (tileOverlays == null) { + return {}; + } + return Map.fromEntries(tileOverlays.map( + (TileOverlay tileOverlay) => MapEntry( + tileOverlay.tileOverlayId, tileOverlay))); +} + +/// Converts a Set of TileOverlays into something serializable in JSON. +List> serializeTileOverlaySet( + Set tileOverlays) { + if (tileOverlays == null) { + return null; + } + return tileOverlays + .map>((TileOverlay 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 index a2b5ff56fee1..ba5d09de54f0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ 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.5 +version: 1.0.6 dependencies: flutter: