flutter_map icon indicating copy to clipboard operation
flutter_map copied to clipboard

[FEATURE] Add Option To Preload Tiles In An Area

Open RedHappyLlama opened this issue 4 months ago • 0 comments

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.0
import '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

RedHappyLlama avatar Oct 16 '24 20:10 RedHappyLlama