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