[google_maps_flutter_web] Initial support for custom overlays (#3538)

This is a resubmission of https://github.com/flutter/plugins/pull/6982 from the now archived flutter plugins repo. I'm submitting the changes from the original author, @AsturaPhoenix. The original description is below.

--------

Saves tile bytes to blobs and uses img elements to decode and render. Does not implement opacity, perform caching, or serve placeholder images.

**Issue:** Fixes https://github.com/flutter/flutter/issues/98596

**Known issues:**

- https://github.com/flutter/flutter/issues/116132
- https://github.com/AsturaPhoenix/trip_planner_aquamarine/issues/22
This commit is contained in:
Eli Geller
2023-07-31 11:38:48 -04:00
committed by GitHub
parent 9074ea9e38
commit 9e21922163
15 changed files with 800 additions and 150 deletions

View File

@ -1,3 +1,7 @@
## 0.5.3
* Initial support for custom overlays. [#98596](https://github.com/flutter/flutter/issues/98596).
## 0.5.2
* Adds options for gesture handling and tilt controls.

View File

@ -14,14 +14,14 @@ import 'package:integration_test/integration_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'google_maps_controller_test.mocks.dart';
@GenerateMocks(<Type>[], customMocks: <MockSpec<dynamic>>[
MockSpec<CirclesController>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<PolygonsController>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<PolylinesController>(onMissingStub: OnMissingStub.returnDefault),
MockSpec<MarkersController>(onMissingStub: OnMissingStub.returnDefault),
@GenerateNiceMocks(<MockSpec<dynamic>>[
MockSpec<CirclesController>(),
MockSpec<PolygonsController>(),
MockSpec<PolylinesController>(),
MockSpec<MarkersController>(),
MockSpec<TileOverlaysController>(),
])
import 'google_maps_controller_test.mocks.dart';
/// Test Google Map Controller
void main() {
@ -194,6 +194,15 @@ void main() {
}, throwsAssertionError);
});
testWidgets('cannot updateTileOverlays after dispose',
(WidgetTester tester) async {
controller.dispose();
expect(() {
controller.updateTileOverlays(const <TileOverlay>{});
}, throwsAssertionError);
});
testWidgets('isInfoWindowShown defaults to false',
(WidgetTester tester) async {
controller.dispose();
@ -208,6 +217,7 @@ void main() {
late MockMarkersController markers;
late MockPolygonsController polygons;
late MockPolylinesController polylines;
late MockTileOverlaysController tileOverlays;
late gmaps.GMap map;
setUp(() {
@ -215,20 +225,20 @@ void main() {
markers = MockMarkersController();
polygons = MockPolygonsController();
polylines = MockPolylinesController();
tileOverlays = MockTileOverlaysController();
map = gmaps.GMap(html.DivElement());
});
testWidgets('listens to map events', (WidgetTester tester) async {
controller = createController();
controller.debugSetOverrides(
createMap: (_, __) => map,
circles: circles,
markers: markers,
polygons: polygons,
polylines: polylines,
);
controller.init();
controller = createController()
..debugSetOverrides(
createMap: (_, __) => map,
circles: circles,
markers: markers,
polygons: polygons,
polylines: polylines,
)
..init();
// Trigger events on the map, and verify they've been broadcast to the stream
final Stream<MapEvent<Object?>> capturedEvents = stream.stream.take(5);
@ -258,26 +268,26 @@ void main() {
testWidgets("binds geometry controllers to map's",
(WidgetTester tester) async {
controller = createController();
controller.debugSetOverrides(
createMap: (_, __) => map,
circles: circles,
markers: markers,
polygons: polygons,
polylines: polylines,
);
controller.init();
controller = createController()
..debugSetOverrides(
createMap: (_, __) => map,
circles: circles,
markers: markers,
polygons: polygons,
polylines: polylines,
tileOverlays: tileOverlays,
)
..init();
verify(circles.bindToMap(mapId, map));
verify(markers.bindToMap(mapId, map));
verify(polygons.bindToMap(mapId, map));
verify(polylines.bindToMap(mapId, map));
verify(tileOverlays.bindToMap(mapId, map));
});
testWidgets('renders initial geometry', (WidgetTester tester) async {
controller = createController(
mapObjects: MapObjects(circles: <Circle>{
final MapObjects mapObjects = MapObjects(circles: <Circle>{
const Circle(
circleId: CircleId('circle-1'),
zIndex: 1234,
@ -320,57 +330,25 @@ void main() {
LatLng(43.354469, -5.851318),
LatLng(43.354762, -5.850824),
])
}));
}, tileOverlays: <TileOverlay>{
const TileOverlay(tileOverlayId: TileOverlayId('overlay-1'))
});
controller.debugSetOverrides(
circles: circles,
markers: markers,
polygons: polygons,
polylines: polylines,
);
controller = createController(mapObjects: mapObjects)
..debugSetOverrides(
circles: circles,
markers: markers,
polygons: polygons,
polylines: polylines,
tileOverlays: tileOverlays,
)
..init();
controller.init();
final Set<Circle> capturedCircles =
verify(circles.addCircles(captureAny)).captured[0] as Set<Circle>;
final Set<Marker> capturedMarkers =
verify(markers.addMarkers(captureAny)).captured[0] as Set<Marker>;
final Set<Polygon> capturedPolygons =
verify(polygons.addPolygons(captureAny)).captured[0]
as Set<Polygon>;
final Set<Polyline> capturedPolylines =
verify(polylines.addPolylines(captureAny)).captured[0]
as Set<Polyline>;
expect(capturedCircles.first.circleId.value, 'circle-1');
expect(capturedCircles.first.zIndex, 1234);
expect(capturedMarkers.first.markerId.value, 'marker-1');
expect(capturedMarkers.first.infoWindow.snippet, 'snippet for test');
expect(capturedMarkers.first.infoWindow.title, 'title for test');
expect(capturedPolygons.first.polygonId.value, 'polygon-1');
expect(capturedPolygons.elementAt(1).polygonId.value,
'polygon-2-with-holes');
expect(capturedPolygons.elementAt(1).holes, isNot(null));
expect(capturedPolylines.first.polylineId.value, 'polyline-1');
});
testWidgets('empty infoWindow does not create InfoWindow instance.',
(WidgetTester tester) async {
controller = createController(
mapObjects: MapObjects(markers: <Marker>{
const Marker(markerId: MarkerId('marker-1')),
}));
controller.debugSetOverrides(
markers: markers,
);
controller.init();
final Set<Marker> capturedMarkers =
verify(markers.addMarkers(captureAny)).captured[0] as Set<Marker>;
expect(capturedMarkers.first.infoWindow, InfoWindow.noText);
verify(circles.addCircles(mapObjects.circles));
verify(markers.addMarkers(mapObjects.markers));
verify(polygons.addPolygons(mapObjects.polygons));
verify(polylines.addPolylines(mapObjects.polylines));
verify(tileOverlays.addTileOverlays(mapObjects.tileOverlays));
});
group('Initialization options', () {
@ -449,15 +427,12 @@ void main() {
target: LatLng(43.308, -5.6910),
zoom: 12,
),
);
controller.debugSetOverrides(
createMap: (_, gmaps.MapOptions options) {
capturedOptions = options;
return map;
});
controller.init();
)
..debugSetOverrides(createMap: (_, gmaps.MapOptions options) {
capturedOptions = options;
return map;
})
..init();
expect(capturedOptions, isNotNull);
expect(capturedOptions!.zoom, 12);
@ -467,8 +442,7 @@ void main() {
group('Traffic Layer', () {
testWidgets('by default is disabled', (WidgetTester tester) async {
controller = createController();
controller.init();
controller = createController()..init();
expect(controller.trafficLayer, isNull);
});
@ -477,9 +451,9 @@ void main() {
controller = createController(
mapConfiguration: const MapConfiguration(
trafficEnabled: true,
));
controller.debugSetOverrides(createMap: (_, __) => map);
controller.init();
))
..debugSetOverrides(createMap: (_, __) => map)
..init();
expect(controller.trafficLayer, isNotNull);
});
});
@ -496,9 +470,9 @@ void main() {
..zoom = 10
..center = gmaps.LatLng(0, 0),
);
controller = createController();
controller.debugSetOverrides(createMap: (_, __) => map);
controller.init();
controller = createController()
..debugSetOverrides(createMap: (_, __) => map)
..init();
});
group('updateRawOptions', () {
@ -556,13 +530,9 @@ void main() {
// These are the methods that get forwarded to other controllers, so we just verify calls.
group('Pass-through methods', () {
setUp(() {
controller = createController();
});
testWidgets('updateCircles', (WidgetTester tester) async {
final MockCirclesController mock = MockCirclesController();
controller.debugSetOverrides(circles: mock);
controller = createController()..debugSetOverrides(circles: mock);
final Set<Circle> previous = <Circle>{
const Circle(circleId: CircleId('to-be-updated')),
@ -589,7 +559,7 @@ void main() {
testWidgets('updateMarkers', (WidgetTester tester) async {
final MockMarkersController mock = MockMarkersController();
controller.debugSetOverrides(markers: mock);
controller = createController()..debugSetOverrides(markers: mock);
final Set<Marker> previous = <Marker>{
const Marker(markerId: MarkerId('to-be-updated')),
@ -616,7 +586,7 @@ void main() {
testWidgets('updatePolygons', (WidgetTester tester) async {
final MockPolygonsController mock = MockPolygonsController();
controller.debugSetOverrides(polygons: mock);
controller = createController()..debugSetOverrides(polygons: mock);
final Set<Polygon> previous = <Polygon>{
const Polygon(polygonId: PolygonId('to-be-updated')),
@ -643,7 +613,7 @@ void main() {
testWidgets('updatePolylines', (WidgetTester tester) async {
final MockPolylinesController mock = MockPolylinesController();
controller.debugSetOverrides(polylines: mock);
controller = createController()..debugSetOverrides(polylines: mock);
final Set<Polyline> previous = <Polyline>{
const Polyline(polylineId: PolylineId('to-be-updated')),
@ -674,11 +644,38 @@ void main() {
}));
});
testWidgets('updateTileOverlays', (WidgetTester tester) async {
final MockTileOverlaysController mock = MockTileOverlaysController();
controller = createController(
mapObjects: MapObjects(tileOverlays: <TileOverlay>{
const TileOverlay(tileOverlayId: TileOverlayId('to-be-updated')),
const TileOverlay(tileOverlayId: TileOverlayId('to-be-removed')),
}))
..debugSetOverrides(tileOverlays: mock);
controller.updateTileOverlays(<TileOverlay>{
const TileOverlay(
tileOverlayId: TileOverlayId('to-be-updated'), visible: false),
const TileOverlay(tileOverlayId: TileOverlayId('to-be-added')),
});
verify(mock.removeTileOverlays(<TileOverlayId>{
const TileOverlayId('to-be-removed'),
}));
verify(mock.addTileOverlays(<TileOverlay>{
const TileOverlay(tileOverlayId: TileOverlayId('to-be-added')),
}));
verify(mock.changeTileOverlays(<TileOverlay>{
const TileOverlay(
tileOverlayId: TileOverlayId('to-be-updated'), visible: false),
}));
});
testWidgets('infoWindow visibility', (WidgetTester tester) async {
final MockMarkersController mock = MockMarkersController();
const MarkerId markerId = MarkerId('marker-with-infowindow');
when(mock.isInfoWindowShown(markerId)).thenReturn(true);
controller.debugSetOverrides(markers: mock);
controller = createController()..debugSetOverrides(markers: mock);
controller.showInfoWindow(markerId);

View File

@ -403,3 +403,65 @@ class MockMarkersController extends _i1.Mock implements _i3.MarkersController {
returnValueForMissingStub: null,
);
}
/// A class which mocks [TileOverlaysController].
///
/// See the documentation for Mockito's code generation for more information.
class MockTileOverlaysController extends _i1.Mock
implements _i3.TileOverlaysController {
@override
_i2.GMap get googleMap => (super.noSuchMethod(
Invocation.getter(#googleMap),
returnValue: _FakeGMap_0(
this,
Invocation.getter(#googleMap),
),
returnValueForMissingStub: _FakeGMap_0(
this,
Invocation.getter(#googleMap),
),
) as _i2.GMap);
@override
set googleMap(_i2.GMap? _googleMap) => super.noSuchMethod(
Invocation.setter(
#googleMap,
_googleMap,
),
returnValueForMissingStub: null,
);
@override
void addTileOverlays(Set<_i4.TileOverlay>? tileOverlays) =>
super.noSuchMethod(
Invocation.method(
#addTileOverlays,
[tileOverlays],
),
returnValueForMissingStub: null,
);
@override
void changeTileOverlays(Set<_i4.TileOverlay>? tileOverlays) =>
super.noSuchMethod(
Invocation.method(
#changeTileOverlays,
[tileOverlays],
),
returnValueForMissingStub: null,
);
@override
void removeTileOverlays(Set<_i4.TileOverlayId>? tileOverlayIds) =>
super.noSuchMethod(
Invocation.method(
#removeTileOverlays,
[tileOverlayIds],
),
returnValueForMissingStub: null,
);
@override
void clearTileCache(_i4.TileOverlayId? tileOverlayId) => super.noSuchMethod(
Invocation.method(
#clearTileCache,
[tileOverlayId],
),
returnValueForMissingStub: null,
);
}

View File

@ -14,12 +14,9 @@ import 'package:integration_test/integration_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@GenerateNiceMocks(<MockSpec<dynamic>>[MockSpec<GoogleMapController>()])
import 'google_maps_plugin_test.mocks.dart';
@GenerateMocks(<Type>[], customMocks: <MockSpec<dynamic>>[
MockSpec<GoogleMapController>(onMissingStub: OnMissingStub.returnDefault),
])
/// Test GoogleMapsPlugin
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@ -208,28 +205,6 @@ void main() {
});
});
group('Noop methods:', () {
const int mapId = 0;
setUp(() {
plugin.debugSetMapById(<int, GoogleMapController>{mapId: controller});
});
// Options
testWidgets('updateTileOverlays', (WidgetTester tester) async {
final Future<void> update = plugin.updateTileOverlays(
mapId: mapId,
newTileOverlays: <TileOverlay>{},
);
expect(update, completion(null));
});
testWidgets('updateTileOverlays', (WidgetTester tester) async {
final Future<void> update = plugin.clearTileCache(
const TileOverlayId('any'),
mapId: mapId,
);
expect(update, completion(null));
});
});
// These methods only pass-through values from the plugin to the controller
// so we verify them all together here...
group('Pass-through methods:', () {
@ -287,6 +262,24 @@ void main() {
verify(controller.updateCircles(expectedUpdates));
});
// Tile Overlays
testWidgets('updateTileOverlays', (WidgetTester tester) async {
final Set<TileOverlay> expectedOverlays = <TileOverlay>{
const TileOverlay(tileOverlayId: TileOverlayId('overlay'))
};
await plugin.updateTileOverlays(
newTileOverlays: expectedOverlays, mapId: mapId);
verify(controller.updateTileOverlays(expectedOverlays));
});
testWidgets('clearTileCache', (WidgetTester tester) async {
const TileOverlayId tileOverlayId = TileOverlayId('Dory');
await plugin.clearTileCache(tileOverlayId, mapId: mapId);
verify(controller.clearTileCache(tileOverlayId));
});
// Camera
testWidgets('animateCamera', (WidgetTester tester) async {
final CameraUpdate expectedUpdates = CameraUpdate.newLatLng(

View File

@ -126,6 +126,7 @@ class MockGoogleMapController extends _i1.Mock
_i4.CirclesController? circles,
_i4.PolygonsController? polygons,
_i4.PolylinesController? polylines,
_i4.TileOverlaysController? tileOverlays,
}) =>
super.noSuchMethod(
Invocation.method(
@ -137,6 +138,7 @@ class MockGoogleMapController extends _i1.Mock
#circles: circles,
#polygons: polygons,
#polylines: polylines,
#tileOverlays: tileOverlays,
},
),
returnValueForMissingStub: null,
@ -286,6 +288,23 @@ class MockGoogleMapController extends _i1.Mock
returnValueForMissingStub: null,
);
@override
void updateTileOverlays(Set<_i2.TileOverlay>? newOverlays) =>
super.noSuchMethod(
Invocation.method(
#updateTileOverlays,
[newOverlays],
),
returnValueForMissingStub: null,
);
@override
void clearTileCache(_i2.TileOverlayId? id) => super.noSuchMethod(
Invocation.method(
#clearTileCache,
[id],
),
returnValueForMissingStub: null,
);
@override
void showInfoWindow(_i2.MarkerId? markerId) => super.noSuchMethod(
Invocation.method(
#showInfoWindow,

View File

@ -0,0 +1,119 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:html' as html;
import 'package:flutter_test/flutter_test.dart';
import 'package:google_maps/google_maps.dart' as gmaps;
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
import 'package:google_maps_flutter_web/google_maps_flutter_web.dart';
import 'package:integration_test/integration_test.dart';
import 'resources/tile16_base64.dart';
class NoTileProvider implements TileProvider {
const NoTileProvider();
@override
Future<Tile> getTile(int x, int y, int? zoom) async => TileProvider.noTile;
}
class TestTileProvider implements TileProvider {
const TestTileProvider();
@override
Future<Tile> getTile(int x, int y, int? zoom) async =>
Tile(16, 16, const Base64Decoder().convert(tile16Base64));
}
/// Test Overlays
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('TileOverlayController', () {
const TileOverlayId id = TileOverlayId('');
testWidgets('minimal initialization', (WidgetTester tester) async {
final TileOverlayController controller = TileOverlayController(
tileOverlay: const TileOverlay(tileOverlayId: id),
);
final gmaps.Size size = controller.gmMapType.tileSize!;
expect(size.width, TileOverlayController.logicalTileSize);
expect(size.height, TileOverlayController.logicalTileSize);
expect(controller.gmMapType.getTile!(gmaps.Point(0, 0), 0, html.document),
null);
});
testWidgets('produces image tiles', (WidgetTester tester) async {
final TileOverlayController controller = TileOverlayController(
tileOverlay: const TileOverlay(
tileOverlayId: id,
tileProvider: TestTileProvider(),
),
);
final html.ImageElement img =
controller.gmMapType.getTile!(gmaps.Point(0, 0), 0, html.document)!
as html.ImageElement;
expect(img.naturalWidth, 0);
expect(img.naturalHeight, 0);
expect(img.hidden, true);
// Wait until the image is fully loaded and decoded before re-reading its attributes.
await img.onLoad.first;
await img.decode();
expect(img.hidden, false);
expect(img.naturalWidth, 16);
expect(img.naturalHeight, 16);
});
testWidgets('update', (WidgetTester tester) async {
final TileOverlayController controller = TileOverlayController(
tileOverlay: const TileOverlay(
tileOverlayId: id,
tileProvider: NoTileProvider(),
),
);
{
final html.ImageElement img =
controller.gmMapType.getTile!(gmaps.Point(0, 0), 0, html.document)!
as html.ImageElement;
await null; // let `getTile` `then` complete
expect(
img.src,
isEmpty,
reason: 'The NoTileProvider never updates the img src',
);
}
controller.update(const TileOverlay(
tileOverlayId: id,
tileProvider: TestTileProvider(),
));
{
final html.ImageElement img =
controller.gmMapType.getTile!(gmaps.Point(0, 0), 0, html.document)!
as html.ImageElement;
await img.onLoad.first;
expect(
img.src,
isNotEmpty,
reason: 'The img `src` should eventually become the Blob URL.',
);
}
controller.update(const TileOverlay(tileOverlayId: id));
{
expect(
controller.gmMapType.getTile!(gmaps.Point(0, 0), 0, html.document),
null,
reason: 'Setting a null tileProvider should work.',
);
}
});
});
}

View File

@ -0,0 +1,172 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:html' as html;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:google_maps/google_maps.dart' as gmaps;
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'
hide GoogleMapController;
import 'package:integration_test/integration_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
@GenerateNiceMocks(<MockSpec<dynamic>>[MockSpec<TileProvider>()])
import 'overlays_test.mocks.dart';
MockTileProvider neverTileProvider() {
final MockTileProvider tileProvider = MockTileProvider();
when(tileProvider.getTile(any, any, any))
.thenAnswer((_) => Completer<Tile>().future);
return tileProvider;
}
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('TileOverlaysController', () {
late TileOverlaysController controller;
late gmaps.GMap map;
late List<MockTileProvider> tileProviders;
late List<TileOverlay> tileOverlays;
/// Queries the current overlay map types for tiles at x = 0, y = 0, zoom =
/// 0.
void probeTiles() {
for (final gmaps.MapType? mapType in map.overlayMapTypes!.array!) {
mapType?.getTile!(gmaps.Point(0, 0), 0, html.document);
}
}
setUp(() {
controller = TileOverlaysController();
map = gmaps.GMap(html.DivElement());
controller.googleMap = map;
tileProviders = <MockTileProvider>[
for (int i = 0; i < 3; ++i) neverTileProvider()
];
tileOverlays = <TileOverlay>[
for (int i = 0; i < 3; ++i)
TileOverlay(
tileOverlayId: TileOverlayId('$i'),
tileProvider: tileProviders[i],
zIndex: i)
];
});
testWidgets('addTileOverlays', (WidgetTester tester) async {
controller.addTileOverlays(<TileOverlay>{...tileOverlays});
probeTiles();
verifyInOrder(<dynamic>[
tileProviders[0].getTile(any, any, any),
tileProviders[1].getTile(any, any, any),
tileProviders[2].getTile(any, any, any),
]);
verifyNoMoreInteractions(tileProviders[0]);
verifyNoMoreInteractions(tileProviders[1]);
verifyNoMoreInteractions(tileProviders[2]);
});
testWidgets('changeTileOverlays', (WidgetTester tester) async {
controller.addTileOverlays(<TileOverlay>{...tileOverlays});
// Set overlay 0 visiblity to false; flip z ordering of 1 and 2, leaving 1
// unchanged.
controller.changeTileOverlays(<TileOverlay>{
tileOverlays[0].copyWith(visibleParam: false),
tileOverlays[2].copyWith(zIndexParam: 0),
});
probeTiles();
verifyInOrder(<dynamic>[
tileProviders[2].getTile(any, any, any),
tileProviders[1].getTile(any, any, any),
]);
verifyZeroInteractions(tileProviders[0]);
verifyNoMoreInteractions(tileProviders[1]);
verifyNoMoreInteractions(tileProviders[2]);
// Re-enable overlay 0.
controller.changeTileOverlays(
<TileOverlay>{tileOverlays[0].copyWith(visibleParam: true)});
probeTiles();
verify(tileProviders[2].getTile(any, any, any));
verifyInOrder(<dynamic>[
tileProviders[0].getTile(any, any, any),
tileProviders[1].getTile(any, any, any),
]);
verifyNoMoreInteractions(tileProviders[0]);
verifyNoMoreInteractions(tileProviders[1]);
verifyNoMoreInteractions(tileProviders[2]);
});
testWidgets(
'updating the z index of a hidden layer does not make it visible',
(WidgetTester tester) async {
controller.addTileOverlays(<TileOverlay>{...tileOverlays});
controller.changeTileOverlays(<TileOverlay>{
tileOverlays[0].copyWith(zIndexParam: -1, visibleParam: false),
});
probeTiles();
verifyZeroInteractions(tileProviders[0]);
});
testWidgets('removeTileOverlays', (WidgetTester tester) async {
controller.addTileOverlays(<TileOverlay>{...tileOverlays});
controller.removeTileOverlays(<TileOverlayId>{
tileOverlays[0].tileOverlayId,
tileOverlays[2].tileOverlayId,
});
probeTiles();
verify(tileProviders[1].getTile(any, any, any));
verifyZeroInteractions(tileProviders[0]);
verifyZeroInteractions(tileProviders[2]);
});
testWidgets('clearTileCache', (WidgetTester tester) async {
final Completer<GoogleMapController> controllerCompleter =
Completer<GoogleMapController>();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: GoogleMap(
initialCameraPosition: const CameraPosition(
target: LatLng(43.3078, -5.6958),
zoom: 14,
),
tileOverlays: <TileOverlay>{...tileOverlays.take(2)},
onMapCreated: (GoogleMapController value) {
controllerCompleter.complete(value);
addTearDown(() => value.dispose());
},
))));
// This is needed to kick-off the rendering of the JS Map flutter widget
await tester.pump();
final GoogleMapController controller = await controllerCompleter.future;
await tester.pump();
verify(tileProviders[0].getTile(any, any, any));
verify(tileProviders[1].getTile(any, any, any));
await controller.clearTileCache(tileOverlays[0].tileOverlayId);
await tester.pump();
verify(tileProviders[0].getTile(any, any, any));
verifyNoMoreInteractions(tileProviders[1]);
});
});
}

View File

@ -0,0 +1,77 @@
// Mocks generated by Mockito 5.4.1 from annotations
// in google_maps_flutter_web_integration_tests/integration_test/overlays_test.dart.
// Do not manually edit this file.
// @dart=2.19
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i3;
import 'package:google_maps_flutter_platform_interface/src/types/types.dart'
as _i2;
import 'package:mockito/mockito.dart' as _i1;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
class _FakeTile_0 extends _i1.SmartFake implements _i2.Tile {
_FakeTile_0(
Object parent,
Invocation parentInvocation,
) : super(
parent,
parentInvocation,
);
}
/// A class which mocks [TileProvider].
///
/// See the documentation for Mockito's code generation for more information.
class MockTileProvider extends _i1.Mock implements _i2.TileProvider {
@override
_i3.Future<_i2.Tile> getTile(
int? x,
int? y,
int? zoom,
) =>
(super.noSuchMethod(
Invocation.method(
#getTile,
[
x,
y,
zoom,
],
),
returnValue: _i3.Future<_i2.Tile>.value(_FakeTile_0(
this,
Invocation.method(
#getTile,
[
x,
y,
zoom,
],
),
)),
returnValueForMissingStub: _i3.Future<_i2.Tile>.value(_FakeTile_0(
this,
Invocation.method(
#getTile,
[
x,
y,
zoom,
],
),
)),
) as _i3.Future<_i2.Tile>);
}

View File

@ -0,0 +1,9 @@
// 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.
/// 16x16 transparent png.
const String tile16Base64 =
'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAA'
'Cxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAATSURBVDhPYxgFo2AUjAIwYGAAAAQQAAGn'
'RHxjAAAAAElFTkSuQmCC';

View File

@ -9,6 +9,7 @@ import 'dart:convert';
import 'dart:html';
import 'dart:js_util';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -31,6 +32,8 @@ part 'src/google_maps_controller.dart';
part 'src/google_maps_flutter_web.dart';
part 'src/marker.dart';
part 'src/markers.dart';
part 'src/overlay.dart';
part 'src/overlays.dart';
part 'src/polygon.dart';
part 'src/polygons.dart';
part 'src/polyline.dart';

View File

@ -25,11 +25,13 @@ class GoogleMapController {
_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
@ -53,6 +55,7 @@ class GoogleMapController {
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();
@ -108,6 +111,7 @@ class GoogleMapController {
PolygonsController? _polygonsController;
PolylinesController? _polylinesController;
MarkersController? _markersController;
TileOverlaysController? _tileOverlaysController;
// Keeps track if _attachGeometryControllers has been called or not.
bool _controllersBoundToMap = false;
@ -122,12 +126,14 @@ class GoogleMapController {
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;
@ -182,13 +188,7 @@ class GoogleMapController {
_attachGeometryControllers(map);
// Now attach the geometry, traffic and any other layers...
_renderInitialGeometry(
markers: _markers,
circles: _circles,
polygons: _polygons,
polylines: _polylines,
);
_renderInitialGeometry();
_setTrafficLayer(map, _lastMapConfiguration.trafficEnabled ?? false);
}
@ -241,22 +241,20 @@ class GoogleMapController {
'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({
Set<Marker> markers = const <Marker>{},
Set<Circle> circles = const <Circle>{},
Set<Polygon> polygons = const <Polygon>{},
Set<Polyline> polylines = const <Polyline>{},
}) {
void _renderInitialGeometry() {
assert(
_controllersBoundToMap,
'Geometry controllers must be bound to a map before any geometry can '
@ -266,10 +264,11 @@ class GoogleMapController {
// in the [_attachGeometryControllers] method, which ensures that all these
// controllers below are *not* null.
_markersController!.addMarkers(markers);
_circlesController!.addCircles(circles);
_polygonsController!.addPolygons(polygons);
_polylinesController!.addPolylines(polylines);
_markersController!.addMarkers(_markers);
_circlesController!.addCircles(_circles);
_polygonsController!.addPolygons(_polygons);
_polylinesController!.addPolylines(_polylines);
_tileOverlaysController!.addTileOverlays(_tileOverlays);
}
// Merges new options coming from the plugin into _lastConfiguration.
@ -407,6 +406,25 @@ class GoogleMapController {
_markersController?.removeMarkers(updates.markerIdsToRemove);
}
/// 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().');
_tileOverlaysController?.addTileOverlays(updates.objectsToAdd);
_tileOverlaysController?.changeTileOverlays(updates.objectsToChange);
_tileOverlaysController
?.removeTileOverlays(updates.objectIdsToRemove.cast<TileOverlayId>());
_tileOverlays = newOverlays;
}
/// Clears the tile cache associated with the given [TileOverlayId].
void clearTileCache(TileOverlayId id) {
_tileOverlaysController?.clearTileCache(id);
}
/// Shows the [InfoWindow] of the marker identified by its [MarkerId].
void showInfoWindow(MarkerId markerId) {
assert(_markersController != null,
@ -439,6 +457,7 @@ class GoogleMapController {
_polygonsController = null;
_polylinesController = null;
_markersController = null;
_tileOverlaysController = null;
_streamController.close();
}
}

View File

@ -95,7 +95,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform {
required Set<TileOverlay> newTileOverlays,
required int mapId,
}) async {
return; // Noop for now!
_map(mapId).updateTileOverlays(newTileOverlays);
}
@override
@ -103,7 +103,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform {
TileOverlayId tileOverlayId, {
required int mapId,
}) async {
return; // Noop for now!
_map(mapId).clearTileCache(tileOverlayId);
}
/// Applies the given `cameraUpdate` to the current viewport (with animation).

View File

@ -0,0 +1,76 @@
// 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;
/// This wraps a [TileOverlay] in a [gmaps.MapType].
class TileOverlayController {
/// Creates a `TileOverlayController` that wraps a [TileOverlay] object and its corresponding [gmaps.MapType].
TileOverlayController({
required TileOverlay tileOverlay,
}) {
update(tileOverlay);
}
/// The size in pixels of the (square) tiles passed to the Maps SDK.
///
/// Even though the web supports any size, and rectangular tiles, for
/// for consistency with mobile, this is not configurable on the web.
/// (Both Android and iOS prefer square 256px tiles @ 1x DPI)
///
/// For higher DPI screens, the Tile that is actually returned can be larger
/// than 256px square.
static const int logicalTileSize = 256;
/// Updates the [gmaps.MapType] and cached properties with an updated
/// [TileOverlay].
void update(TileOverlay tileOverlay) {
_tileOverlay = tileOverlay;
_gmMapType = gmaps.MapType()
..tileSize = gmaps.Size(logicalTileSize, logicalTileSize)
..getTile = _getTile;
}
/// Renders a Tile for gmaps; delegating to the configured [TileProvider].
HtmlElement? _getTile(
gmaps.Point? tileCoord,
num? zoom,
Document? ownerDocument,
) {
if (_tileOverlay.tileProvider == null) {
return null;
}
final ImageElement img =
ownerDocument!.createElement('img') as ImageElement;
img.width = img.height = logicalTileSize;
img.hidden = true;
img.setAttribute('decoding', 'async');
_tileOverlay.tileProvider!
.getTile(tileCoord!.x!.toInt(), tileCoord.y!.toInt(), zoom?.toInt())
.then((Tile tile) {
if (tile.data == null) {
return;
}
// Using img lets us take advantage of native decoding.
final String src = Url.createObjectUrl(Blob(<Object?>[tile.data]));
img.src = src;
img.addEventListener('load', (_) {
img.hidden = false;
Url.revokeObjectUrl(src);
});
});
return img;
}
/// The [gmaps.MapType] produced by this controller.
gmaps.MapType get gmMapType => _gmMapType;
late gmaps.MapType _gmMapType;
/// The [TileOverlay] providing data for this controller.
TileOverlay get tileOverlay => _tileOverlay;
late TileOverlay _tileOverlay;
}

View File

@ -0,0 +1,99 @@
// 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;
/// This class manages all the [TileOverlayController]s associated to a [GoogleMapController].
class TileOverlaysController extends GeometryController {
final Map<TileOverlayId, TileOverlayController> _tileOverlays =
<TileOverlayId, TileOverlayController>{};
final List<TileOverlayController> _visibleTileOverlays =
<TileOverlayController>[];
// Inserts `tileOverlayController` into the list of visible overlays, and the current [googleMap].
//
// After insertion, the arrays stay sorted by ascending z-index.
void _insertZSorted(TileOverlayController tileOverlayController) {
final int index = _visibleTileOverlays.lowerBoundBy<num>(
tileOverlayController,
(TileOverlayController c) => c.tileOverlay.zIndex);
googleMap.overlayMapTypes!.insertAt(index, tileOverlayController.gmMapType);
_visibleTileOverlays.insert(index, tileOverlayController);
}
// Removes `tileOverlayController` from the list of visible overlays.
void _remove(TileOverlayController tileOverlayController) {
final int index = _visibleTileOverlays.indexOf(tileOverlayController);
if (index < 0) {
return;
}
googleMap.overlayMapTypes!.removeAt(index);
_visibleTileOverlays.removeAt(index);
}
/// Adds new [TileOverlay]s to this controller.
///
/// Wraps the [TileOverlay]s in corresponding [TileOverlayController]s.
void addTileOverlays(Set<TileOverlay> tileOverlaysToAdd) {
tileOverlaysToAdd.forEach(_addTileOverlay);
}
void _addTileOverlay(TileOverlay tileOverlay) {
final TileOverlayController controller = TileOverlayController(
tileOverlay: tileOverlay,
);
_tileOverlays[tileOverlay.tileOverlayId] = controller;
if (tileOverlay.visible) {
_insertZSorted(controller);
}
}
/// Updates [TileOverlay]s with new options.
void changeTileOverlays(Set<TileOverlay> tileOverlays) {
tileOverlays.forEach(_changeTileOverlay);
}
void _changeTileOverlay(TileOverlay tileOverlay) {
final TileOverlayController controller =
_tileOverlays[tileOverlay.tileOverlayId]!;
final bool wasVisible = controller.tileOverlay.visible;
final bool isVisible = tileOverlay.visible;
controller.update(tileOverlay);
if (wasVisible) {
_remove(controller);
}
if (isVisible) {
_insertZSorted(controller);
}
}
/// Removes the tile overlays associated with the given [TileOverlayId]s.
void removeTileOverlays(Set<TileOverlayId> tileOverlayIds) {
tileOverlayIds.forEach(_removeTileOverlay);
}
void _removeTileOverlay(TileOverlayId tileOverlayId) {
final TileOverlayController? controller =
_tileOverlays.remove(tileOverlayId);
if (controller != null) {
_remove(controller);
}
}
/// Invalidates the tile overlay associated with the given [TileOverlayId].
void clearTileCache(TileOverlayId tileOverlayId) {
final TileOverlayController? controller = _tileOverlays[tileOverlayId];
if (controller != null && controller.tileOverlay.visible) {
final int i = _visibleTileOverlays.indexOf(controller);
// This causes the map to reload the overlay.
googleMap.overlayMapTypes!.setAt(i, controller.gmMapType);
}
}
}

View File

@ -2,7 +2,7 @@ name: google_maps_flutter_web
description: Web platform implementation of google_maps_flutter
repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter_web
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
version: 0.5.2
version: 0.5.3
environment:
sdk: ">=2.18.0 <4.0.0"
@ -17,6 +17,7 @@ flutter:
fileName: google_maps_flutter_web.dart
dependencies:
collection: ^1.16.0
flutter:
sdk: flutter
flutter_web_plugins: