mapbox / mapbox/mapbox-maps-flutter

Markers and clusters live thier own lives separately

Open
#346 3 comments 5 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Dart
Stars
380
Forks
204
PR merge metrics
No merged PRs in 30d

Description

Version mapbox_maps_flutter: ^0.5.1 is being used.
Flutter version 3.16.2

Is there any way to combine Markers and Clusters in the way that they know that each other exists on the map?

I render Markers by means of pointAnnotationManager and render Clusters as per your example file.
Both Markers and Clusters are displayed on the screen, but when some points are inside Cluster their markers are still visible.
In example YOU use unclusteredLayer instead of Markers. As for the Cluster I found the way how to style it, however as for unClusteredLayer I didn't find any way or example on how to set it as an Image but not a simple circle. Nevertheless, even if I find the way how to set unClusteredLayer as an Image I didn't find any way to listen to tap on cluster or on unClusteredLayer.

Please, provide a complete example, that includes both clusters and "custom" markers as an Image where these "custom" markers are not visible inside cluster and the way to listen tap on cluster or unClusteredLayer.

Here is the my code that is responsible for both Clusters and Markers.

...

void setMarkers(BuildContext context, List<PlaceEntity> places) async {
    mapboxMap.annotations.createPointAnnotationManager().then((pointAnnotationManager) async {
      var options = <PointAnnotationOptions>[];

      for (var i = 0; i < places.length; i++) {
        final point = points[i];
        final place = places[i];

        final widget = IEyeMarker(
          place: place,
          onDetailsTap: () {},
        );

        final Uint8List? image = await createImageFromWidget(context, widget);

        options.add(
          PointAnnotationOptions(
            geometry: Point(coordinates: Position(point.lng, point.lat)).toJson(),
            image: image,
            textField: place.id.toString(),
            textColor: 0x00000000,
            iconSize: 1,
          ),
        );
      }

      pointAnnotationManager.createMulti(options);
      pointAnnotationManager
          .addOnPointAnnotationClickListener(AnnotationClickListener(context: context));
    });

    List<Map<String, Object>> features = [];

    for (var i = 0; i < places.length; i++) {
      final point = points[i];
      final place = places[i];
      features.add({
        'type': 'Feature',
        'id': place.id,
        'properties': {
          'point_count_abbreviated': '1',
          'cluster_id': place.id,
          // 'cluster': true,
          // 'point_count': 1
        },
        'geometry': {
          'type': 'Point',
          'coordinates': [point.lng, point.lat, 0.0]
        }
      });
    }

    final data = {
      'type': 'FeatureCollection',
      'crs': {
        'type': 'name',
        'properties': {'name': 'urn:ogc:def:crs:OGC:1.3:CRS84'}
      },
      'features': features,
    };

    final layout = {
      'type': 'geojson',
      'data': data,
      'cluster': true,
      'clusterMaxZoom': 14,
      'clusterRadius': 50,
    };

    mapboxMap.style.styleSourceExists('places').then((value) async {
      if (!value) {
        var source = jsonEncode(layout);
        mapboxMap.style.addStyleSource('places', source.toString());
      }
    });

    mapboxMap.style.styleLayerExists('clusters').then((value) async {
      if (!value) {
        var layer = await rootBundle.loadString('assets/cluster/cluster_layer.json');
        mapboxMap.style.addStyleLayer(layer, null);

        var clusterCountLayer =
            await rootBundle.loadString('assets/cluster/cluster_count_layer.json');
        mapboxMap.style.addStyleLayer(clusterCountLayer, null);

        var unClusteredLayer =
            await rootBundle.loadString('assets/cluster/unclustered_point_layer.json');
        mapboxMap.style.addStyleLayer(unClusteredLayer, null);
      }
    });
  }

...

final points = [
  LatLng(lat: 51.5382123, lng: -0.1882464),
  LatLng(lat: 51.4382123, lng: -0.1982464),
  LatLng(lat: 51.5090229, lng: -0.2886548),
  LatLng(lat: 51.1090229, lng: -0.2786548),
  LatLng(lat: 51.5054563, lng: -0.0798412),
  LatLng(lat: 51.5090215, lng: -0.1959988),
  LatLng(lat: 51.5190215, lng: -0.1859988),
  LatLng(lat: 51.5577676, lng: -0.2008447),
];  

File "create_image_from_widget.dart":

import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

Future<Uint8List?> createImageFromWidget(BuildContext context, Widget widget,
    {Duration? wait, Size? logicalSize, Size? imageSize}) async {
  final repaintBoundary = RenderRepaintBoundary();

  logicalSize ??= View.of(context).physicalSize / View.of(context).devicePixelRatio;
  imageSize ??= View.of(context).physicalSize;

  assert(logicalSize.aspectRatio == imageSize.aspectRatio,
      'logicalSize and imageSize must not be the same');

  final renderView = RenderView(
      child: RenderPositionedBox(alignment: Alignment.center, child: repaintBoundary),
      configuration: ViewConfiguration(
        size: logicalSize,
        devicePixelRatio: 1,
      ),
      view: View.of(context) //PlatformDispatcher.instance.views.first,
      );

  final pipelineOwner = PipelineOwner();
  final buildOwner = BuildOwner(focusManager: FocusManager());

  pipelineOwner.rootNode = renderView;
  renderView.prepareInitialFrame();

  final rootElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: repaintBoundary,
      child: Directionality(
        textDirection: TextDirection.ltr,
        child: widget,
      )).attachToRenderTree(buildOwner);

  buildOwner.buildScope(rootElement);

  if (wait != null) {
    await Future.delayed(wait);
  }

  buildOwner
    ..buildScope(rootElement)
    ..finalizeTree();

  pipelineOwner
    ..flushLayout()
    ..flushCompositingBits()
    ..flushPaint();

  final image = await repaintBoundary.toImage(pixelRatio: imageSize.width / logicalSize.width);
  final byteData = await image.toByteData(format: ImageByteFormat.png);

  return byteData?.buffer.asUint8List();
}

Just for info here is my flutter doctor result:

Doctor summary (to see all details, run flutter doctor -v):
[!] Flutter (Channel stable, 3.16.2, on macOS 14.2.1 23C71 darwin-arm64, locale ru-RU)
    ! Warning: `dart` on your path resolves to /opt/homebrew/Cellar/dart-sdk/3.1.5/libexec/bin/dart, which is not inside your current Flutter SDK checkout at /Users/andreifufylev/fvm/versions/3.16.2. Consider
      adding /Users/andreifufylev/fvm/versions/3.16.2/bin to the front of your path.
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.1)
[✓] IntelliJ IDEA Community Edition (version 2023.2.5)
[✓] VS Code (version 1.85.1)
[✓] Connected device (4 available)
[✓] Network resources

! Doctor found issues in 1 category.

In order to make muself clear I also included the video and screenshots of my problem where rounded boxes with Image inside are my custom Markers converted from Widget to Uint8List:

Without my custom dynamic Markers:
Simulator Screenshot - iPhone 15 Pro Max - 2024-01-08 at 16 53 54

With my custom dynamic Markers:
Simulator Screenshot - iPhone 15 Pro Max - 2024-01-08 at 16 54 25

Here you can see that Markers are outside clusters:

https://github.com/mapbox/mapbox-maps-flutter/assets/35691656/98207a35-eaae-43b6-8021-2f225f8c8857

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with example/lib/cluster.dart and the assets/cluster/cluster_layer.json, cluster_count_layer.json, and unclustered_point_layer.json files, then compare them with setMarkers and the PointAnnotationManager usage shown here. Define how custom image markers should coexist with clustered points and how cluster and unclustered-layer taps should be handled; done means a complete working example covering those behaviors.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart, flutter
Domain
mobile-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.