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] Exception "MapCamera is no longer within the cameraConstraint after an option change" when using particular camera setups #1760

Open
dumabg opened this issue Dec 6, 2023 · 26 comments
Labels
bug This issue reports broken functionality or another error P: 1 (important) S: core Scoped to the core flutter_map functionality

Comments

@dumabg
Copy link

dumabg commented Dec 6, 2023

What is the bug?

A FlutterMap with a cameraConstraint CameraConstraint.contain(bounds: widget.bounds) gives the assert error "MapCamera is no longer within the cameraConstraint after an option change"

How can we reproduce it?

Put a ValueListenableBuilder and a FlutterMap with a cameraConstraint. Change the ValueListenableBuilder value to trigger a new repaint.

Do you have a potential solution?

I've comment the assert and seems that it works

Platforms

Android

Severity

Erroneous: Prevents normal functioning and causes errors in the console

@dumabg dumabg added bug This issue reports broken functionality or another error needs triage This new bug report needs reproducing and prioritizing labels Dec 6, 2023
@JaffaKetchup
Copy link
Member

Hi @dumabg,
Can I just double check what version you are running?

@JaffaKetchup JaffaKetchup added waiting for user response S: core Scoped to the core flutter_map functionality labels Dec 10, 2023
@JaffaKetchup JaffaKetchup changed the title MapCamera is no longer within the cameraConstraint after an option change [BUG] Exception: "MapCamera is no longer within the cameraConstraint after an option change" Dec 10, 2023
@thorito
Copy link

thorito commented Jan 9, 2024

It happens the same to me.

pubspect.yaml:

  flutter_map: ^6.1.0
  flutter_map_cancellable_tile_provider: ^2.0.0
  flutter_map_marker_cluster: ^1.3.4

My code:

 MapOptions _buildMapOptions() => MapOptions(
    initialCenter: widget._parametersMap.currentPosition.center,
    initialZoom: widget._parametersMap.initialZoom,
    maxZoom: widget._parametersMap.maxZoom,
    minZoom: widget._parametersMap.minZoom,
    initialRotation: widget._parametersMap.rotation,
    cameraConstraint: widget._parametersMap.cameraConstraint,
    initialCameraFit: markers.isNotEmpty &&
            UserPreferences.getInstance().currentPosition == null
        ? markers.getCameraFit(
            minZoom: widget._parametersMap.minZoom,
            maxZoom: widget._parametersMap.initialZoom,
          )
        : null,
    keepAlive: true,
    onMapReady: () {
      // logger.d('onMapReady !!');
    },
    onPositionChanged: (MapPosition position, bool hasGesture) {
      // logger.d('onPositionChanged hasGesture: $hasGesture,
      // position: $position');

      if (hasGesture) {
        ref.read(mapChangedProvider.notifier).changePosition();
      }
    },
    onTap: (_, __) => widget._popupController.hideAllPopups(),
  );

  @override
  Widget build(BuildContext context) {
    final markersAsync = ref.watch(markersProvider);

    return markersAsync.when(
      data: (values) {
        if (values.isNotEmpty) {
          setState(() {
            markers = List.from(values);
          });
        }

        return PopupScope(
          popupController: widget._popupController,
          child: Stack(
            children: [
              FlutterMap(
                mapController: widget._mapController,
                options: _buildMapOptions(),
                children: [
                     ...
				],
          ),
        );
      },
      loading: () => const CustomProgress(),
      error: (error, stackTrace) {
        context.showSnackbar(
          message: error.toString(),
          type: MessageType.error,
        );

        return const SizedBox.shrink();
      },
    );
  }

Error:

MapCamera is no longer within the cameraConstraint after an option change.
'package:flutter_map/src/map/controller/internal.dart':
Failed assertion: line 276 pos 7: 'newOptions.cameraConstraint.constrain(newCamera) == newCamera'

What is the reason for the error?

@JaffaKetchup
Copy link
Member

I can't tell exactly without a more minimal reproducible example, but there's a few things that could be causing issues:

  • Building the map options in a function
  • Not sure if initialCameraFit changes, but changing it will have no effect and could cause issues
  • Not sure what PopupScope is, but maybe that's causing issues?

We need an MRE to investigate this further (standalone, no state from other parts of app, no dependencies, etc.).

@iulian0512
Copy link

iulian0512 commented Jan 26, 2024

@JaffaKetchup @josxha here is a complete single file minimum reproducible example

// ignore_for_file: prefer_const_constructors, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';

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 MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int cnt = 0;

  @override
  void initState() {
    super.initState();
    Future(() async {
      await Future.delayed(Duration(seconds: 1));
      cnt++;
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    final camConstraint = cnt > 0
        ? CameraConstraint.contain(
            bounds: LatLngBounds(LatLng(43.6884447292, 20.2201924985),
                LatLng(48.2208812526, 29.62654341)))
        : null;
    return Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: Text(widget.title),
        ),
        body: FlutterMap(
            options: MapOptions(
                interactionOptions: InteractionOptions(
                    flags: InteractiveFlag.pinchZoom |
                        InteractiveFlag.drag |
                        InteractiveFlag.doubleTapZoom |
                        InteractiveFlag.scrollWheelZoom,
                    rotationWinGestures: MultiFingerGesture.none,
                    rotationThreshold: double.infinity,
                    cursorKeyboardRotationOptions:
                        CursorKeyboardRotationOptions.disabled()),
                minZoom: 1,
                maxZoom: 20,
                initialZoom: 5.9,
                initialCenter: LatLng(45.80565, 24.937853),
                cameraConstraint: camConstraint),
            children: []));
  }
}

pubspec.yaml

name: flutter_map_constraint_issue
description: "A new Flutter project."

publish_to: "none"

version: 1.0.0+1

environment:
  sdk: ">=3.2.5 <4.0.0"

dependencies:
  cupertino_icons: ^1.0.2
  flutter:
    sdk: flutter
  flutter_map: ^6.1.0

dev_dependencies:
  flutter_lints: ^2.0.0
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

the example above produces after 1 second the error

════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown building KeyedSubtree-[GlobalKey#de288]:
MapCamera is no longer within the cameraConstraint after an option change.
'package:flutter_map/src/map/controller/internal.dart':
Failed assertion: line 276 pos 7: 'newOptions.cameraConstraint.constrain(newCamera) == newCamera'

The relevant error-causing widget was:
    Scaffold Scaffold:file:///D:/temp/flutter_map_constraint_issue/lib/main.dart:56:12

let me know if i can assist or provide anything else.

@JaffaKetchup
Copy link
Member

Thanks @iulian0512, we'll look into this :)

@palicka
Copy link

palicka commented Feb 26, 2024

any update on this ? In my case it's not just error in console, but map is not displayed at all
(I'm using 6.1.0 as well)

EDIT:
using CameraConstraint.containCenter() instead of CameraConstraint.contain() fixed the issue for me (.containerCenter is slightly different than .contain, but in my case it doesn't matter)

@JaffaKetchup JaffaKetchup changed the title [BUG] Exception: "MapCamera is no longer within the cameraConstraint after an option change" [BUG] MapCamera is no longer within the cameraConstraint after an option change Mar 29, 2024
@JaffaKetchup JaffaKetchup self-assigned this May 23, 2024
@JaffaKetchup
Copy link
Member

JaffaKetchup commented May 23, 2024

The issue seems to occur because CameraConstraint.constrain returns null for some reason. Therefore, the new camera, and the constrained new camera don't match.

May return null if no appropriate camera could be generated by movement, for example because the camera was zoomed too far out.

Likely the issue is within the ContainCamera.constrain method, or we're setting options when we're not supposed to.

@JaffaKetchup
Copy link
Member

JaffaKetchup commented May 23, 2024

The issue can be tracked back to the initial implementation in 2c60d63/#1551. It only occurs with contain - although it seems to happen in multiple other MapOptions configurations.

Here's some debug output (when running the MRE here: #1760 (comment)). Not sure if I can fix this!

nePixel: Point(8901.449086445922, 5299.8690412382875)
swPixel: Point(8502.023744760078, 5576.9298207346355)
left...: 9526.023744760078
right...: 7877.449086445922
top...: 5876.269041238287
botOkCenter: 5000.529820734636

@JaffaKetchup JaffaKetchup added P: 1 (important) and removed needs triage This new bug report needs reproducing and prioritizing labels May 23, 2024
@JaffaKetchup
Copy link
Member

JaffaKetchup commented May 23, 2024

The issue also occurs in the following configuration, assuming the constraint is always constrained to the same bounds as the initial fit:
image

but doesn't occur when using insideBounds.

@rorystephenson Do you have any insight into what could be happening here?

@jetpeter
Copy link
Contributor

I think I found what is going on here. If you look at when you first create the camera when checking constraints it first looks for an existing camera, and if none then it creates a new camera based on the new options. The bug is that the initialCamera method uses the old initalCenter and initalZoom fields which have default data in them.

 final newCamera = value.camera?.withOptions(newOptions) ??
        MapCamera.initialCamera(newOptions);
  /// Initializes [MapCamera] from the given [options] and with the
  /// [nonRotatedSize] set to [kImpossibleSize].
  MapCamera.initialCamera(MapOptions options)
      : crs = options.crs,
        minZoom = options.minZoom,
        maxZoom = options.maxZoom,
        center = options.initialCenter,
        zoom = options.initialZoom,
        rotation = options.initialRotation,
        nonRotatedSize = kImpossibleSize;

@JaffaKetchup
Copy link
Member

Thanks for investigating @jetpeter!

@JaffaKetchup JaffaKetchup changed the title [BUG] MapCamera is no longer within the cameraConstraint after an option change [BUG] Exception "MapCamera is no longer within the cameraConstraint after an option change" when using particular camera setups May 30, 2024
@JaffaKetchup JaffaKetchup removed their assignment Jun 1, 2024
@JaffaKetchup
Copy link
Member

I think what @jetpeter has found is a part of a much larger issue in that we treat the initialCameraFit somewhat seperately to how we treat initialCenter and initialZoom, and, as you said, we have these two last properties always defined, even if they perhaps shouldn't be.

This is because we have to wait for Flutter to give us some constraints, so we can fit the camera, which can take some time: flutter/flutter#25827.

Therefore, what we try to do, is use the initial center and zoom, then re-fit once we get the constraints. However, this causes this issue indirectly - we re-set options which triggers this issue, as we try to fit the default center and zoom into the initialCameraFit the user actually wants. I believe that's whats causing this issue, although I'm not 100% sure.

Therefore, I propose that we change the way MapOptions works, and the way FlutterMap is built.
First, we make all the initial positioning options nullable, and use asserts to ensure that exactly one of the two 'modes' is used. Then, on build, if we aren't using initialCameraFit, we can build immediately.
Otherwise, we don't display any layers until we get the constraints from Flutter - we just display the background color (and perhaps a simple loading message). Although this does mean this is theoretically now visible to the user, it was happening before, they just couldn't see it, because the network time of the tile layer is larger than it takes to get constraints, so the map moves before tiles load in - I think (again, none of this is 100% certain).
This would mean that we don't have to continously workaround the constraints issue: we don't do anything until there are constraints.

@josxha wdyt?

@JaffaKetchup
Copy link
Member

JaffaKetchup commented Jun 1, 2024

I just realised that perhaps this is the intended behaviour. In @iulian0512's example above, his initial zoom lies outside the camera constraint - therefore the error is thrown, because the zoom needs to be higher to fit.
Equally, using CameraFit.bounds and CameraConstraint.contain with the same bounds would cause the same issue on many screen sizes: instead, the correct approach would be to use CameraFit.insideBounds.

Can anyone explain what else they would expect in this case?

@jetpeter
Copy link
Contributor

jetpeter commented Jun 1, 2024

@JaffaKetchup From your original thoughts, from what I can see the issue is the other way round:

"However, this causes this issue indirectly - we re-set options which triggers this issue,"

I think the issue is that the first time the options is set the exception is thrown. The map never gets an opportunity to use the initalCameraFit after sizing if the MapCamera.initailCamera is out of cameraConstraints due to initialZoom and initialCenter default values.

As it stands right now for usinger with dynamic constraints initialCameraFit is not usable, you must set initialZoom and initialCenter. To compute the initial values as a user just like the map you require widget size, so you have to use a LayoutBuilder to get the size then compute the center zoom from bounds. I have been doing this for a while since older versions of flutter map required this anyway. For new users it is really confusing that there is two different ways of doing initial placement with one being secretly required if constraints are provided, yet the documentation says that they are ignored if the initialCameraFit is provided.

It makes sense to throw an exception if the user requested bounds are outside of the initial position. The problem is if you just provided initalCameraFit and not initialCenter and initalZoom even if your initialCameraFit is inside the constraints you will always get an exception if your constraints are outside of the default center zoom of the map options. Flutter map should ideally have a single initalCamerFit field and not initialCenter initialZoom at all. initalCameraFit could have a centerZoom type instead. Having a few milliseconds of default background is probably acceptable, and honestly might not actually be any different than it is now since tiles should not be loaded until the correct position is computed. Hopefully for users providing initialCameraFit the map is not loading tiles for the default values while the widget is still sizing.

@JaffaKetchup
Copy link
Member

I think the issue is that the first time the options is set the exception is thrown. The map never gets an opportunity to use the initalCameraFit after sizing if the MapCamera.initailCamera is out of cameraConstraints due to initialZoom and initialCenter default values.

As far as I can see from my testing, it works perfectly fine, if CameraFit.insideBounds is used with a constraint, as it should.

For new users it is really confusing that there is two different ways of doing initial placement with one being secretly required if constraints are provided, yet the documentation says that they are ignored if the initialCameraFit is provided.

This shouldn't be the behaviour. Neither should be 'required'. I agree that having non-null defaults on the MapOptions initialCenter and initialZoom isn't the best style, but they SHOULD be ignored if the camera fit is provided.

It makes sense to throw an exception if the user requested bounds are outside of the initial position. The problem is if you just provided initalCameraFit and not initialCenter and initalZoom even if your initialCameraFit is inside the constraints you will always get an exception if your constraints are outside of the default center zoom of the map options.

I can certainly see this could be a possible issue. Can you post an MRE of your exact situation (MapOptions), so I can confirm this? I have been testing using the fixes(?) in #1902, so it's possible I 'accidentally' fixed that issue along the way.

@jetpeter
Copy link
Contributor

jetpeter commented Jun 3, 2024

When making a test project I realized that the camera fit issue only happens when you provide your own MapController.
The below code works if you comment out the controller def. With the controller you get the exceiption:

======== Exception caught by widgets library =======================================================
The following assertion was thrown building KeyedSubtree-[GlobalKey#f3ab2]:
Assertion failed: file:///Users/user/.pub-cache/hosted/pub.dev/flutter_map-7.0.0/lib/src/map/controller/map_controller_impl.dart:350:7
newOptions.cameraConstraint.constrain(newCamera) == newCamera
"MapCamera is no longer within the cameraConstraint after an option change."

The relevant error-causing widget was: 
  Scaffold Scaffold:file:///Users/user/src/map_test/lib/main.dart:52:12
When the exception was thrown, this was the stack: 
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 296:3  throw_
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 29:3   assertFailed
packages/flutter_map/src/map/controller/map_controller_impl.dart 350:68      set options
packages/flutter_map/src/map/widget.dart 188:7                               [_setMapController]
packages/flutter_map/src/map/widget.dart 61:5                                initState
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Map Bounds Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  late final MapController controller;

  @override
  void initState() {
    controller = MapController();
    super.initState();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final bounds = LatLngBounds(const LatLng(46.028731, -123.896535), const LatLng(42.004317, -117.057532));
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Center(
        child: FlutterMap(
          // Map works without controller
          mapController: controller,
          options: MapOptions(
            // If not using a controller you don't actually need initial camera fit.
            // The map will build to the default bounds, but if you try to move it
            // the map will jump to your bonds with no exception. 
            initialCameraFit: CameraFit.insideBounds(bounds: bounds),
            cameraConstraint: CameraConstraint.contain(bounds: bounds)
          ),
          children: [
            TileLayer(
              urlTemplate: "tile_provider_url",
              userAgentPackageName: 'com.example.bounds_test',
              tileProvider: NetworkTileProvider(),
            )
          ],
        )
    ));
  }
}

pubspec.yaml

  flutter_map: ^7.0.0
  latlong2: ^0.9.1

@JaffaKetchup
Copy link
Member

#1920 suggests it could be fixed by also providing an initialCenter inside the specified coordinates.

@jamesho86
Copy link

#1920 suggests it could be fixed by also providing an initialCenter inside the specified coordinates.

The bug still exists even if the initialCenter is inside the specified camera constraint with the latest versions below.

flutter_map: ^7.0.2
latlong2: ^0.9.1

error msg:
MapCamera is no longer within the cameraConstraint after an option change.
'package:flutter_map/src/map/controller/map_controller_impl.dart':
Failed assertion: line 345 pos 7: 'newOptions.cameraConstraint.constrain(newCamera) == newCamera'

@nathanwang-comp
Copy link

The bug still exists even if the initialCenter is inside the specified camera constraint and not init MapController with the latest versions below.
flutter_map: ^7.0.2
latlong2: ^0.9.1

@MatKershaw3708
Copy link

MatKershaw3708 commented Jul 22, 2024

I've been experimenting with the issue when crashes occur even if initialCenter is within the constraint bounds.

If you are zoomed out enough, the constraints may mean that the camera's initial center actually ends up outside the constraint bounds and the crash will occur.

The map will try to use the specified initialCenter, but may not be able to do so if this means exposing areas outside the cameraConstraint settings given the zoom level. It will instead center itself the best it can, triggering the crash if the resulting centre of the map is outside the constraint.

This is probably the same issue described by jetpeter above.

@MatKershaw3708
Copy link

I've worked around the issue to setting initialCameraFit to the same bounds as cameraConstraint. This is much safer than setting an initialZoom and initialCenter, as you know the map's resulting centre will be within the same bounds.

@JaffaKetchup
Copy link
Member

We would appriciate any PRs! This will require some level of rewrite of the camera system, particularly initialisation. It has outgrown its initial requirements, which I believe is why this is happening. A combinination of race conditions and complicated initialisation/setup.

@DevTello
Copy link

DevTello commented Jul 25, 2024

@MatKershaw3708

Seemingly doing what you said, but still having the crash
image

@IcyTempest
Copy link

IcyTempest commented Jul 26, 2024

#1920 suggests it could be fixed by also providing an initialCenter inside the specified coordinates.

The bug still exists even if the initialCenter is inside the specified camera constraint with the latest versions below.

flutter_map: ^7.0.2 latlong2: ^0.9.1

error msg: MapCamera is no longer within the cameraConstraint after an option change. 'package:flutter_map/src/map/controller/map_controller_impl.dart': Failed assertion: line 345 pos 7: 'newOptions.cameraConstraint.constrain(newCamera) == newCamera'

Have you tried it with an actual device? I'm developing my application with a real device instead of an emulator and I haven't had that problem occur since applying the initialCenter. And Yes, if I use an emulator the problem would still occur.

Actually, the bound stop working when I started using the emulator. But I'm not sure if I'm using flutter_map 7.0.2 at the time.

@priyanshi-pandya
Copy link

I was getting the same exception, i had to adjust the initialCenter with respect to the cameraConstraints

Like,
initialZoom: 2.3, cameraConstraint: CameraConstraint.contain( bounds: LatLngBounds( const LatLng(-90, -180), const LatLng(90, 180), ), ),

For this above bounds it works perfectly with initialZoom of 2.3, if I go any less than this, its throws me again MapCamera, is not within cameraConstraint error.

Here is my full code

     FlutterMap(
            options: MapOptions(
              minZoom: 1,
              maxZoom: 19,
              initialCenter: const LatLng(0, 0),
              initialZoom: 2.3,
              cameraConstraint: CameraConstraint.contain(
                bounds: LatLngBounds(
                  const LatLng(-90, -180),
                  const LatLng(90, 180),
                ),
              ),
              onTap: (tapPosition, point)  {
                //Tap logic
              },
              onPointerHover: (event, point) {
                //Hover logic                  },
            ),
            mapController: mapProvider.mapController,
            children: [
              TileLayer(
                urlTemplate:
                    'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
                userAgentPackageName: 'com.example.app',
                tileProvider: CancellableNetworkTileProvider(),
              ),
              //other layers
              
            ]
         ), 

@MatKershaw3708
Copy link

@DevTello - here is the relevant part of my current options:

Screenshot 2024-08-16 134519

That's all I use, which avoids the crash. You may find the inclusion of the min zoom settings are causing the crash by making the resulting camera center end up outside the cameraConstraint bounds. The cameraConstraint by itself will cap the min zoom level to whatever is required to make the bounding box visible as a whole, while preventing you from being able to zoom out further. Setting max zoom should not be an issue.

I didn't know you could inset the initialCameraFit, thanks for that!

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 P: 1 (important) S: core Scoped to the core flutter_map functionality
Projects
Status: To do
Development

No branches or pull requests