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

[BUG] TileLayer fails to load available tile when zoom is too high #1968

Open
ChristopheLyonnaz opened this issue Sep 20, 2024 · 8 comments
Open
Labels
bug This issue reports broken functionality or another error needs triage This new bug report needs reproducing and prioritizing

Comments

@ChristopheLyonnaz
Copy link

ChristopheLyonnaz commented Sep 20, 2024

What is the bug?

TileLayer should be able to load tile of lower resolution if the tile provider cannot provide the tile at required zoom.

When TileLayer loads a tile at zoom Z, if the user zooms in, TileLayer gets the tile at zoom Z+1. if the tile provider cannot deliver this tile, or if the maxNativeZoom is defined as Z, TileLayer scales up the latest available tile (at zoom Z) and displays it correctly.
However, in this situation (where a tile is scaled up), if the user scrolls to adjacent tiles, TileLayer fails to recover the tile at zoom Z to scale it up and display it at the new position. It returns an error and displays a grey area, or the fallback strategy.

How can we reproduce it?

Create a map layer with a maxNativeZoom greater than the maximum zoom available in your tile provider.
Zoom on a area to display a tile with the maximum zoom. Continue zooming to have this tile scaled up.
Scroll to adjacent tile.

TileLayer fails to get the adjacent tile and returns an exception:

======== Exception caught by image resource service ================================================
The following ClientException was thrown resolving an image codec:
Request to https://tile.openstreetmap.org/20/521267/378404.png failed with status 400: Bad Request., uri=https://tile.openstreetmap.org/20/521267/378404.png

When the exception was thrown, this was the stack: 
#0      BaseClient._checkResponseSuccess (package:http/src/base_client.dart:103:5)
#1      BaseClient.readBytes (package:http/src/base_client.dart:59:5)
<asynchronous suspension>
#2      NetworkTileProvider.getImage.<anonymous closure> (package:flutter_map/src/layer/tile_layer/tile_provider/network_tile_provider.dart:68:31)
<asynchronous suspension>
#3      ImmutableBuffer.fromUint8List (dart:ui/painting.dart:6667:3)
<asynchronous suspension>
#4      PaintingBinding.instantiateImageCodecWithSize (package:flutter/src/painting/binding.dart:137:3)
<asynchronous suspension>
#5      MultiFrameImageStreamCompleter._handleCodecReady (package:flutter/src/painting/image_stream.dart:1005:3)
<asynchronous suspension>
URL: https://tile.openstreetmap.org/20/521267/378404.png
Fallback URL: null
Current provider: MapNetworkImageProvider()
====================================================================================================


Here is an example of code (inspired from 'Sliding Map' flutter_map example):

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart';
import 'package:latlong2/latlong.dart';

class SlidingMapPage extends StatelessWidget {
  static const String route = '/sliding_map';
  static const northEast = LatLng(56.7378, 11.6644);
  static const southWest = LatLng(56.6877, 11.5089);

  const SlidingMapPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sliding Map')),
      drawer: const MenuDrawer(route),
      body: Column(
        children: [
          const Padding(
            padding: EdgeInsets.all(8),
            child: Text(
              'This is a map that can be panned smoothly when the '
              'boundaries are reached.',
            ),
          ),
          Flexible(
            child: FlutterMap(
              options: const MapOptions(
                initialCenter: LatLng(44.704173, -1.043808),
                minZoom: 1,
              ),
              children: [
                TileLayer(
                  urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
                  maxNativeZoom: 30,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Do you have a potential solution?

Not yet a solution, but I suggest that _onTileUpdateEvent() function may be able to check for max available zoom from provider and updates tileZoom accordingly.

Currently, it only clamps zoom with maxNativeZoom, but this clam can be adjusted according to server capability.

Platforms

All Android and iOS plateforms

Severity

Erroneous: Prevents normal functioning and causes errors in the console

@ChristopheLyonnaz ChristopheLyonnaz added bug This issue reports broken functionality or another error needs triage This new bug report needs reproducing and prioritizing labels Sep 20, 2024
@ibrierley
Copy link
Collaborator

What happens if you set the maxZoom to 20 as well (or try 19 out of interest as well) ?

@ChristopheLyonnaz
Copy link
Author

Tile is loaded and displayed correctly.
But it assumes you know what the max available zoom is, which may not be always true

@ibrierley
Copy link
Collaborator

I'm a bit out of touch on the recovering parent tile code etc, but I suspect this would be quite fiddly to resolve. Lets say you want zoom 30, but only have access to max zoom 20, and are panning, so potentially have no historic higher zooms "cached"...I think you would need to keep lots of old tiles about which would hurt performance, and you may still not have cached tiles to use anyway, so would still hit the same problem...

It seems quite reasonable if adding a URL of a tile provider to know its valid tile range ?

@ChristopheLyonnaz
Copy link
Author

Yes, I agree, that would be easier.
But in my specific use case, the tiles are loaded by the end user to have offline maps, and I don't know in advance what max zoom is available for a specific area when building the TileLayer.

However, I don't think it is necessary to 'cache' a lot of tiles. Just like when you manage a supported zoom, you request the missing tile to the server. Here also, if tile at zoom Z+1 is not available, it seems feasible to load tile at zoom Z.

@ChristopheLyonnaz
Copy link
Author

To give more consistency to my latest comment, I did a small check.
Let assume my server only provides tiles with max zoom as 10.

If a change _onTileUpdateEvent() in tile_layer.dart as follows:

  void _onTileUpdateEvent(TileUpdateEvent event) {
    final tileZoom = _clampToNativeZoom(event.zoom);
    ....

to

void _onTileUpdateEvent(TileUpdateEvent event) {
    final tileZoom = _clampToNativeZoom(event.zoom).clamp(widget.minNativeZoom, 10);
    ...

All is working nicely.

The goal is now to be able to build dynamically the '10' from the server capabilities (or in my case, from the available tile list in the displayed area).
And either to delegate this to tile_layer, or to give a way for the TileLayer user to give this information in TileProvider or TileBuilder functions.

@ibrierley
Copy link
Collaborator

Hacky, but could you do something like the following before starting flutter_map to figure the max zoom from the given tiles..

pseudocode

sub figureMaxZoomAvailable {
for x 9..30 {
ok, error = getUrl ( "https://tile.openstreetmap.org/{$x}/1/1.png")
if error, return x-1
}

@ChristopheLyonnaz
Copy link
Author

Yes, I thought of such a solution, but unfortunalely, this is valid only if I have the same zoom level for all the area I want to cover.

If I have tiles up to zoom 10 over France, but up to 9 over US, I will start flutter_map with a TileLayer with maxNativeZoom = 9 if I visit the US, and start a new flutter_map if I visit France.

However, you gave me an idea.
I may be able to create 1 TileLayer per area, and load them all in flutter_maps as children. Each TileLayer child would be defined with its maxNativeZoom and with its boundaries.

I'll keep you informed if this work around the issue.

@ChristopheLyonnaz
Copy link
Author

A final word on this issue from my side.
I manage to work around this issue, by 'simply' stacking several TileLayers, with boundary limits and maxNativeZoom on each of them. Each TileLayer is defined when maps are downloaded from the server.

In order to make this works correctly, make sure to sort them by max zoom level (from the lowest to the highest).

    mapList.forEach((map) {
      layers.add(
        TileLayer(
          urlTemplate: tilesPath,
          tileProvider: FileTileProvider(),
          maxNativeZoom: map.zoom,
          tileBounds: LatLngBounds(map.northWest, map.southEast),
        ),
      );
    });

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug This issue reports broken functionality or another error needs triage This new bug report needs reproducing and prioritizing
Projects
Status: To do
Development

No branches or pull requests

2 participants