flutter_map
flutter_map copied to clipboard
[FEATURE] Add Option To Preload Tiles In An Area
What do you want implemented?
Very similar to https://github.com/fleaflet/flutter_map/issues/1337#issuecomment-1332856263, when passed a centre latlng and zoom level, the tiles are preloaded before the FlutterMap is created and rendered, with some form of flag when preloading is complete. This would thereby instantly show a fully rendered map (excluding any issues) once loading is complete.
I feel part of this issue is due to where I'm fairly confident flutter_map 7.0.2 is displaying tiles slower than flutter_map 5.0.0. See code below. Any advice on how to improve this would be great, please let me know.
flutter_map 5.0.0
Code
Dependencies flutter_map: ^5.0.0 flutter_map_marker_cluster: ^1.2.0 flutter_map_location_marker: ^7.0.2 url_launcher: ^6.3.0 http: ^1.1.0
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:math' as math;
import 'package:http/http.dart' as http;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyMap(),
);
}
}
class MyMap extends StatefulWidget {
const MyMap({
Key? key,
}) : super(key: key);
@override
_MyMapState createState() => _MyMapState();
}
const String mapURL1 = "https://tile.openstreetmap.fr";
const String mapURL2 = "https://tile.openstreetmap.org";
class _MyMapState extends State<MyMap> with TickerProviderStateMixin {
double centerLat = 51.58862;
double centerLng = -1.427001;
bool _isLoading = true;
bool _isError = false;
String? errorMessage;
String? mapURL;
MapController mapController = MapController();
final PopupController _popupLayerController = PopupController();
List<Marker> pinList = [];
@override
void dispose() {
super.dispose();
}
@override
void initState() {
super.initState();
loadMapData();
}
Future loadMapData() async {
pinList = [
marker(51, -1.4),
marker(51, -2.4),
marker(52, -1.4),
marker(52, -2.4),
marker(53, -1.4),
marker(53, -2.4),
];
APIResponse response = await getStatusCode();
if (response.error) {
setState(() {
_isError = true;
_isLoading = false;
});
errorMessage = response.errorMessage;
} else {
mapURL = response.data;
setState(() {
_isError = false;
_isLoading = false;
});
}
}
Marker marker(double lat, double lng) {
return Marker(
height: 50.0,
width: 50.0,
rotate: true,
anchorPos: AnchorPos.align(AnchorAlign.top),
point: LatLng(lat, lng),
builder: (context) => Container(
color: Colors.pink,
height: 30,
width: 30,
),
);
}
Future<APIResponse> getStatusCode() async {
try {
http.Response response = await http.get(Uri.parse(mapURL1));
if (response.statusCode == 200) {
return APIResponse(
data: "$mapURL1/hot/{z}/{x}/{y}.png",
error: false,
);
} else {
response = await http.get(Uri.parse(mapURL2));
if (response.statusCode == 200) {
return APIResponse(
data: "$mapURL2/{z}/{x}/{y}.png",
error: false,
);
} else {
return APIResponse(
data: null,
error: true,
errorMessage:
'Looks like there is a problem at our 3rd party provider of map data, so the map can not load. Really sorry! Would you like to try again?',
);
}
}
} catch (e) {
return APIResponse(
data: null,
error: true,
errorMessage:
'Unable to connect. Please check your internet connection. Would you like to try again?',
);
}
}
TileLayer get myTileLayer => TileLayer(
urlTemplate: mapURL,
maxNativeZoom: 18,
minNativeZoom: 4,
maxZoom: 18,
minZoom: 4.0,
panBuffer: 0,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(' Map 2'),
),
body: Builder(
builder: (context) {
if (_isLoading) {
return const Material(
color: Colors.yellow,
child: Center(
child: CircularProgressIndicator(
color: Colors.blue,
),
),
);
}
if (_isError) {
return Material(
color: Colors.yellow,
child: Center(
child: AlertDialog(
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 14),
title: const Text(
'Map Error',
),
content: Text(
errorMessage!,
),
actionsAlignment: MainAxisAlignment.spaceAround,
actionsOverflowAlignment: OverflowBarAlignment.center,
actions: [
ElevatedButton(
onPressed: () async {
setState(() {
_isLoading = true;
_isError = false;
});
await loadMapData();
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(Colors.blue),
),
child: const Text(
'No',
),
),
ElevatedButton(
onPressed: () async {
setState(() {
_isLoading = true;
_isError = false;
});
await loadMapData();
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(Colors.green),
),
child: const Text(
'Yes',
),
),
],
),
),
);
}
return PopupScope(
popupController: _popupLayerController,
child: FlutterMap(
mapController: mapController,
options: MapOptions(
center: LatLng(centerLat, centerLng),
zoom: 16.0,
enableMultiFingerGestureRace: true,
maxZoom: 18,
minZoom: 4.0,
onPositionChanged: (MapPosition position, bool hasGesture) {
if (hasGesture) {
_popupLayerController.hideAllPopups();
}
},
),
children: [
myTileLayer,
MarkerClusterLayerWidget(
options: MarkerClusterLayerOptions(
onClusterTap: (markerClusterVoid) {
if (!_isLoading && mapController.rotation != 0) {
mapController.rotate(0);
}
},
popupOptions: PopupOptions(
popupSnap: PopupSnap.mapTop,
popupController: _popupLayerController,
popupBuilder: (BuildContext context, Marker marker) {
return Container(
padding: const EdgeInsets.only(top: 12.0),
width: MediaQuery.of(context).size.width,
height: 200,
color: Colors.black,
);
}),
disableClusteringAtZoom: 18,
maxClusterRadius: 120,
spiderfyCluster: true,
size: const Size(40, 40),
fitBoundsOptions:
const FitBoundsOptions(padding: EdgeInsets.symmetric(vertical: 200, horizontal: 20)),
markers: pinList,
showPolygon: false,
builder: (context, markers) {
return Transform.rotate(
angle: -mapController.rotation * math.pi / 180,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.green,
),
child: Center(
child: Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: Text(
markers.length.toString(),
textAlign: TextAlign.center,
textScaler: TextScaler.noScaling,
),
),
),
),
);
},
),
),
RichAttributionWidget(
animationConfig: const ScaleRAWA(), // Or `FadeRAWA` as is default
attributions: [
TextSourceAttribution(
'OpenStreetMap contributors',
onTap: () => launchUrl(Uri.parse('https://openstreetmap.org/copyright')),
),
],
),
],
),
);
},
),
);
}
}
class APIResponse<T> {
T? data;
bool error;
String? errorMessage;
APIResponse({
this.data,
required this.error,
this.errorMessage,
});
}
flutter_map 7.02
Code
**Dependencies** flutter_map: ^7.0.2 flutter_map_cancellable_tile_provider: ^3.0.2 flutter_map_marker_cluster_2: ^1.0.4 flutter_map_location_marker: ^9.1.1 url_launcher: ^6.3.0 http: ^1.1.0import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_cancellable_tile_provider/flutter_map_cancellable_tile_provider.dart';
import 'package:latlong2/latlong.dart';
import 'package:flutter_map_marker_cluster_2/flutter_map_marker_cluster.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:math' as math;
import 'package:http/http.dart' as http;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyMap(),
);
}
}
class MyMap extends StatefulWidget {
const MyMap({
Key? key,
}) : super(key: key);
@override
_MyMapState createState() => _MyMapState();
}
const String mapURL1 = "https://tile.openstreetmap.fr";
const String mapURL2 = "https://tile.openstreetmap.org";
class _MyMapState extends State<MyMap> with TickerProviderStateMixin {
double centerLat = 51.58862;
double centerLng = -1.427001;
bool _isLoading = true;
bool _isError = false;
String? errorMessage;
String? mapURL;
MapController mapController = MapController();
final PopupController _popupLayerController = PopupController();
List<Marker> pinList = [];
@override
void dispose() {
super.dispose();
}
@override
void initState() {
super.initState();
loadMapData();
}
Future loadMapData() async {
pinList = [
marker(51, -1.4),
marker(51, -2.4),
marker(52, -1.4),
marker(52, -2.4),
marker(53, -1.4),
marker(53, -2.4),
];
APIResponse response = await getStatusCode();
if (response.error) {
setState(() {
_isError = true;
_isLoading = false;
});
errorMessage = response.errorMessage;
} else {
mapURL = response.data;
setState(() {
_isError = false;
_isLoading = false;
});
}
}
Marker marker(double lat, double lng) {
return Marker(
height: 50.0,
width: 50.0,
rotate: true,
alignment: Alignment.topCenter,
point: LatLng(lat, lng),
child: Container(
color: Colors.pink,
height: 30,
width: 30,
),
);
}
Future<APIResponse> getStatusCode() async {
try {
http.Response response = await http.get(Uri.parse(mapURL1));
if (response.statusCode == 200) {
return APIResponse(
data: "$mapURL1/hot/{z}/{x}/{y}.png",
error: false,
);
} else {
response = await http.get(Uri.parse(mapURL2));
if (response.statusCode == 200) {
return APIResponse(
data: "$mapURL2/{z}/{x}/{y}.png",
error: false,
);
} else {
return APIResponse(
data: null,
error: true,
errorMessage:
'Looks like there is a problem at our 3rd party provider of map data, so the map can not load. Really sorry! Would you like to try again?',
);
}
}
} catch (e) {
return APIResponse(
data: null,
error: true,
errorMessage:
'Unable to connect. Please check your internet connection. Would you like to try again?',
);
}
}
TileLayer get myTileLayer => TileLayer(
urlTemplate: mapURL,
maxNativeZoom: 18,
minNativeZoom: 4,
maxZoom: 18,
minZoom: 4.0,
panBuffer: 0,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(' Map'),
),
body: Builder(
builder: (context) {
if (_isLoading) {
return const Material(
color: Colors.yellow,
child: Center(
child: CircularProgressIndicator(
color: Colors.blue,
),
),
);
}
if (_isError) {
return Material(
color: Colors.yellow,
child: Center(
child: AlertDialog(
contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 14),
title: const Text(
'Map Error',
),
content: Text(
errorMessage!,
),
actionsAlignment: MainAxisAlignment.spaceAround,
actionsOverflowAlignment: OverflowBarAlignment.center,
actions: [
ElevatedButton(
onPressed: () async {
setState(() {
_isLoading = true;
_isError = false;
});
await loadMapData();
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(Colors.blue),
),
child: const Text(
'No',
),
),
ElevatedButton(
onPressed: () async {
setState(() {
_isLoading = true;
_isError = false;
});
await loadMapData();
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(Colors.green),
),
child: const Text(
'Yes',
),
),
],
),
),
);
}
return PopupScope(
popupController: _popupLayerController,
child: FlutterMap(
mapController: mapController,
options: MapOptions(
initialCenter: LatLng(centerLat, centerLng),
initialZoom: 16.0,
interactionOptions: const InteractionOptions(
enableMultiFingerGestureRace: true,
),
maxZoom: 18,
minZoom: 4.0,
onPositionChanged: (MapCamera position, bool hasGesture) {
if (hasGesture) {
_popupLayerController.hideAllPopups();
}
},
),
children: [
myTileLayer,
MarkerClusterLayerWidget(
options: MarkerClusterLayerOptions(
onClusterTap: (markerClusterVoid) {
if (!_isLoading && mapController.camera.rotation != 0) {
mapController.rotate(0);
}
},
popupOptions: PopupOptions(
popupSnap: PopupSnap.mapTop,
popupController: _popupLayerController,
popupBuilder: (BuildContext context, Marker marker) {
return Container(
padding: const EdgeInsets.only(top: 12.0),
width: MediaQuery.of(context).size.width,
height: 200,
color: Colors.black,
);
}),
disableClusteringAtZoom: 18,
maxClusterRadius: 120,
spiderfyCluster: true,
size: const Size(40, 40),
padding: const EdgeInsets.symmetric(vertical: 200, horizontal: 20),
markers: pinList,
showPolygon: false,
builder: (context, markers) {
return Transform.rotate(
angle: -mapController.camera.rotation * math.pi / 180,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.green,
),
child: Center(
child: Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: Text(
markers.length.toString(),
textAlign: TextAlign.center,
textScaler: TextScaler.noScaling,
),
),
),
),
);
},
),
),
RichAttributionWidget(
animationConfig: const ScaleRAWA(), // Or `FadeRAWA` as is default
attributions: [
TextSourceAttribution(
'OpenStreetMap contributors',
onTap: () => launchUrl(Uri.parse('https://openstreetmap.org/copyright')),
),
],
),
],
),
);
},
),
);
}
}
class APIResponse<T> {
T? data;
bool error;
String? errorMessage;
APIResponse({
this.data,
required this.error,
this.errorMessage,
});
}
What other alternatives are available?
No response
Can you provide any other information?
No response
Severity
Annoying: Currently have to use workarounds
I feel part of this issue is due to where I'm fairly confident flutter_map 7.0.2 is displaying tiles slower than flutter_map 5.0.0.
This has been reported elsewhere, and it's also something I'm beginning to notice. I think this needs investigating.
An extension to this would be to preload tiles along an animation curve (maybe @TesteurManiak ?).
An extension to this would be to preload tiles along an animation curve (maybe @TesteurManiak ?).
Iād be glad to help! š But Iām not sure that animations would actually help in this case, troubleshooting potential performance issues might be more impactful š¤
Yep, wasn't suggesting animations might help, but if this feature were implemented (seperate to looking into why tile loading is slow), I'm sure it could be used in animations to make them look better š
There is an issue with tile loading, basically, if no move event happens after a quick zoom, then tiles of a higher zoom level stay active for some reason and do not render the lower tiles even though they are apparently? loaded in
@mootw That sounds like #1813?
yep #1813 is exactly it! I am not entirely sure what is causing the regression, but it started sometime around v7