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 1 year ago • 6 comments
trafficstars

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

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 ?).

JaffaKetchup avatar Nov 12 '24 20:11 JaffaKetchup

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 šŸ¤”

TesteurManiak avatar Nov 12 '24 20:11 TesteurManiak

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 šŸ‘

JaffaKetchup avatar Nov 12 '24 20:11 JaffaKetchup

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 avatar Dec 03 '24 19:12 mootw

@mootw That sounds like #1813?

JaffaKetchup avatar Dec 03 '24 19:12 JaffaKetchup

yep #1813 is exactly it! I am not entirely sure what is causing the regression, but it started sometime around v7

mootw avatar Dec 26 '24 04:12 mootw