Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Add Option To Preload Tiles In An Area #1979

Open
RedHappyLlama opened this issue Oct 16, 2024 · 3 comments
Open

[FEATURE] Add Option To Preload Tiles In An Area #1979

RedHappyLlama opened this issue Oct 16, 2024 · 3 comments
Labels
feature This issue requests a new feature

Comments

@RedHappyLlama
Copy link

What do you want implemented?

Very similar to #1337 (comment), 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 RedHappyLlama added the feature This issue requests a new feature label Oct 16, 2024
@JaffaKetchup
Copy link
Member

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

@TesteurManiak
Copy link
Collaborator

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 🤔

@JaffaKetchup
Copy link
Member

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 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature This issue requests a new feature
Projects
Status: To do
Development

No branches or pull requests

3 participants