Ankit Agarwal
Ankit Agarwal

Reputation: 124

How to Create a Proportional Circle Layer with Radius in Kilometers using Flutter's mapbox_maps_flutter package?

I'm using Flutter's mapbox_maps_flutter package for displaying maps. I want to add a circle layer at a specific point with latitude, longitude, and radius specified in kilometers. My current implementation adds a circle layer, but I'm not sure how to make it proportional to the map's zoom level. In other words, the circle should cover the same area regardless of the zoom level.

Here is my code for creating the circle layer:

void _createCircle() {
  final point = Point(coordinates: Position(cameraCenterLongitude, cameraCenterLatitude));
  mapboxMap?.style.addSource(GeoJsonSource(id: "source", data: json.encode(point)));

  mapboxMap?.style.addLayer(CircleLayer(
    id: 'layer',
    sourceId: 'source',
    visibility: maps.Visibility.VISIBLE,
    circleSortKey: 1.0,
    circleColor: Colors.blue.value,
    circleOpacity: 0.2,
    circlePitchAlignment: CirclePitchAlignment.MAP,
    circlePitchScale: CirclePitchScale.MAP,
    circleRadius: AppConstants.defaultRadius / 1000.toDouble(),
    circleStrokeColor: Colors.blue.value,
    circleStrokeOpacity: 1.0,
    circleStrokeWidth: 1.0,
    circleTranslate: [0.0, 1.0],
    circleTranslateAnchor: CircleTranslateAnchor.MAP,
  ));

  mapboxMap?.style.setStyleLayerProperty("layer", 'circle-radius', [
    'interpolate',
    ['linear'],
    ['zoom'],
    5, 10, 10, 500
  ]);
}

I've attempted to use interpolation to handle the zooming, but I'm not confident that I've implemented it correctly. Specifically, I'm not sure how to adjust the circle-radius so that it remains consistent in terms of real-world distance (in kilometers) regardless of the zoom level.

Can anyone provide guidance or a solution for creating a proportional circle layer with a radius specified in kilometers?

Thanks in advance!

Upvotes: 0

Views: 353

Answers (1)

Anderson Arroyo
Anderson Arroyo

Reputation: 367

This took me some hours to figure out. Here is my implementation using circle layers.

/// A class responsible for drawing a circle on a Mapbox map.
///
/// This class provides functionality to draw a circular area on a Mapbox map
/// with specified properties such as center, radius, and color.
class MapCircleDrawer {
  /// Creates a new instance of [MapCircleDrawer].
  ///
  /// [mapboxMap]: The Mapbox map instance to draw on.
  /// [center]: The center point of the circle.
  /// [radiusInMeters]: The radius of the circle in meters.
  /// [circleColor]: The color of the circle.
  MapCircleDrawer({
    required this.mapboxMap,
    required this.center,
    required this.radiusInMeters,
    required this.circleColor,
  });

  /// The Mapbox map instance to draw on.
  final MapboxMap? mapboxMap;

  /// The center point of the circle.
  final Point center;

  /// The radius of the circle in meters.
  final double radiusInMeters;

  /// The color of the circle.
  final Color circleColor;

  /// Draws the circle on the map.
  ///
  /// This method creates a GeoJSON source and a circle layer to represent
  /// the circular area on the map. It also sets up the circle's appearance
  /// and size based on the zoom level.
  Future<void> drawCircle() async {
    final String geoJsonData = jsonEncode(<String, Object>{
      'type': 'Feature',
      'geometry': <String, Object>{
        'type': 'Point',
        'coordinates': <double>[
          center.coordinates.lng as double,
          center.coordinates.lat as double
        ],
      },
      'properties': <String, Object>{},
    });

    final GeoJsonSource circleSource = GeoJsonSource(
      id: 'circle-source',
      data: geoJsonData,
    );
    await mapboxMap?.style.addSource(circleSource);

    final CircleLayer circleLayer = CircleLayer(
      id: 'circle-layer',
      sourceId: 'circle-source',
      circleColor: circleColor.value,
      circleOpacity: 0.2,
      circleStrokeWidth: 0.5,
      circleStrokeColor: Colors.black.value,
    );
    await mapboxMap?.style.addLayer(circleLayer);

    await mapboxMap?.style
        .setStyleLayerProperty('circle-layer', 'circle-radius', <Object>[
      'interpolate',
      <Object>['exponential', 2],
      <String>['zoom'],
      0,
      0,
      20,
      _metersToPixels(center.coordinates.lat as double, radiusInMeters, 20)
    ]);
  }

  /// Converts meters to pixels at a given latitude and zoom level.
  ///
  /// This method is used to calculate the appropriate circle radius in pixels
  /// based on the map's zoom level and the circle's latitude.
  ///
  /// [latitude]: The latitude of the circle's center.
  /// [meters]: The radius in meters.
  /// [zoomLevel]: The current zoom level of the map.
  ///
  /// Returns the radius in pixels.
  double _metersToPixels(double latitude, double meters, double zoomLevel) {
    return meters /
        (78271.484 / math.pow(2, zoomLevel)) /
        math.cos((latitude * math.pi) / 180);
  }
}

Then you just need to call this class within your onStyleLoadedListener function like this:

final MapCircleDrawer circleDrawer = MapCircleDrawer(
      mapboxMap: mapboxMap,
      center: widget.location,
      radiusInMeters: 500,
      circleColor: _getCircleColor(),
    );
 await circleDrawer.drawCircle();

Upvotes: 0

Related Questions