mapbox / mapbox-maps-flutter

Interactive, thoroughly customizable maps for Flutter powered by Mapbox Maps SDK
https://www.mapbox.com/mobile-maps-sdk
Other
278 stars 107 forks source link

Markers and clusters live thier own lives separately #346

Open fufylev opened 8 months ago

fufylev commented 8 months ago

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

Jamey-7 commented 4 months ago

I have this same exact problem has anyone figured this out yet? It seems like it should be so simple here is my code why is it so hard to change the circle and make it tappable? There has to be a way?

void _addGeoJsonSourceWithClustering() async {
  final mapClusterProvider = Provider.of<MapClusterProvider>(context, listen: false);
  final geoJson = mapClusterProvider.createGeoJson();

  String sourceId = "data";
  Map<String, dynamic> sourceData = {
    "type": "geojson",
    "data": json.decode(geoJson),
    "cluster": true,
    "clusterRadius": 50,
    "clusterMaxZoom": 14,
  };

  String encodedSourceData = jsonEncode(sourceData);

  if (!(await mapboxMap?.style.styleSourceExists(sourceId) ?? true)) {
    await mapboxMap?.style.addStyleSource(sourceId, encodedSourceData);
    print('"data" source added to the map style');
    _addClusteringLayers(sourceId);
  }
}
void _addClusteringLayers(String sourceId) async {
  // Add clusters layer
  var layer = '''
    {
      "id": "clusters",
      "type": "circle",
      "source": "$sourceId",
      "filter": ["has", "point_count"],
      "paint": {
        "circle-color": [
          "step",
          ["get", "point_count"],
          "#51bbd6",
          100,
          "#f1f075",
          750,
          "#f28cb1"
        ],
        "circle-radius": [
          "step",
          ["get", "point_count"],
          20,
          100,
          30,
          750,
          40
        ]
      }
    }
  ''';
  await mapboxMap?.style.addStyleLayer(layer, null);
  print('"clusters" layer added to the map style');

  // Add cluster count layer
  var clusterCountLayer = '''
    {
      "id": "cluster-count",
      "type": "symbol",
      "source": "$sourceId",
      "filter": ["has", "point_count"],
      "layout": {
        "text-field": "{point_count_abbreviated}",
        "text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"],
        "text-size": 12
      }
    }
  ''';
  await mapboxMap?.style.addStyleLayer(clusterCountLayer, null);
  print('"cluster-count" layer added to the map style');

  // Add unclustered point layer
  var unclusteredLayer = '''
    {
      "id": "unclustered-point",
      "type": "circle",
      "source": "$sourceId",
      "filter": ["!", ["has", "point_count"]],
      "paint": {
        "circle-color": "#f62323",
        "circle-radius": 40,
        "circle-stroke-width": 1,
        "circle-stroke-color": "#fff"
      }
    }
  ''';
  await mapboxMap?.style.addStyleLayer(unclusteredLayer, null);
  print('"unclustered-point" layer added to the map style');
}
BishopSam commented 2 weeks ago

@fufylev did you find any work around for this? the api of this package is really becoming a pain in the *ss

fufylev commented 2 weeks ago

@fufylev did you find any work around for this? the api of this package is really becoming a pain in the *ss

Nope, spent a lot of time, still at the same trubble point.