Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions example/lib/pages/multi_worlds.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,87 @@ class _MultiWorldsPageState extends State<MultiWorldsPage> {
..._customMarkers,
],
),
GestureDetector(
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_hitNotifier.value!.hitValues.join(', ')),
duration: const Duration(seconds: 1),
showCloseIcon: true,
),
),
child: PolygonLayer<String>(
hitNotifier: _hitNotifier,
simplificationTolerance: 0,
useAltRendering: true,
drawLabelsLast: false,
polygons: [
Polygon<String>(
label: 'Aloha!',
labelStyle:
const TextStyle(color: Colors.green, fontSize: 40),
labelPlacement:
PolygonLabelPlacement.centroidWithMultiWorld,
rotateLabel: false,
points: const [
LatLng(40, 149),
LatLng(45, 159),
LatLng(50, 169),
LatLng(55, 179),
LatLng(50, -170),
LatLng(45, -160),
LatLng(40, -150),
LatLng(35, -160),
LatLng(30, -170),
LatLng(25, -180),
LatLng(30, 169),
LatLng(35, 159),
],
holePointsList: const [
[
LatLng(45, 175),
LatLng(45, -175),
LatLng(35, -175),
LatLng(35, 175),
],
],
color: const Color(0xFFFF0000),
hitValue: 'Red Line, Across the universe...',
),
],
),
),
GestureDetector(
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_hitNotifier.value!.hitValues.join(', ')),
duration: const Duration(seconds: 1),
showCloseIcon: true,
),
),
child: PolylineLayer<String>(
hitNotifier: _hitNotifier,
simplificationTolerance: 0,
polylines: [
Polyline<String>(
points: const [
LatLng(-40, 150),
LatLng(-45, 160),
LatLng(-50, 170),
LatLng(-55, 180),
LatLng(-50, -170),
LatLng(-45, -160),
LatLng(-40, -150),
LatLng(-45, -140),
LatLng(-50, -130),
],
useStrokeWidthInMeter: true,
strokeWidth: 500000,
color: const Color(0xFF0000FF),
hitValue: 'Blue Line',
),
],
),
),
],
),
],
Expand Down
1 change: 1 addition & 0 deletions lib/src/layer/circle_layer/circle_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:ui';

import 'package:flutter/widgets.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/src/layer/shared/feature_layer_utils.dart';
import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart';
import 'package:latlong2/latlong.dart' hide Path;

Expand Down
106 changes: 31 additions & 75 deletions lib/src/layer/circle_layer/painter.dart
Original file line number Diff line number Diff line change
@@ -1,79 +1,56 @@
part of 'circle_layer.dart';

/// The [CustomPainter] used to draw [CircleMarker] for the [CircleLayer].
base class CirclePainter<R extends Object>
extends HitDetectablePainter<R, CircleMarker<R>> {
/// The [CustomPainter] used to draw [CircleMarker]s for the [CircleLayer].
class CirclePainter<R extends Object> extends CustomPainter
with HitDetectablePainter<R, CircleMarker<R>>, FeatureLayerUtils {
/// Reference to the list of [CircleMarker]s of the [CircleLayer].
final List<CircleMarker<R>> circles;

@override
final MapCamera camera;

@override
final LayerHitNotifier<R>? hitNotifier;

/// Create a [CirclePainter] instance by providing the required
/// reference objects.
CirclePainter({
required this.circles,
required super.camera,
required super.hitNotifier,
required this.camera,
required this.hitNotifier,
});

static const _distance = Distance();

@override
bool elementHitTest(
CircleMarker<R> element, {
required Offset point,
required LatLng coordinate,
}) {
final worldWidth = _getWorldWidth();
final radius = _getRadiusInPixel(element, withBorder: true);
final initialCenter = _getOffset(element.point);

/// Returns null if invisible, true if hit, false if not hit.
bool? checkIfHit(double shift) {
WorldWorkControl checkIfHit(double shift) {
final center = initialCenter + Offset(shift, 0);
if (!_isVisible(
screenRect: _screenRect,
center: center,
radiusInPixel: radius,
)) {
return null;
if (!_isVisible(center: center, radiusInPixel: radius)) {
return WorldWorkControl.invisible;
}

return pow(point.dx - center.dx, 2) + pow(point.dy - center.dy, 2) <=
radius * radius;
radius * radius
? WorldWorkControl.hit
: WorldWorkControl.visible;
}

if (checkIfHit(0) ?? false) {
return true;
}

// Repeat over all worlds (<--||-->) until culling determines that
// that element is out of view, and therefore all further elements in
// that direction will also be
if (worldWidth == 0) return false;
for (double shift = -worldWidth;; shift -= worldWidth) {
final isHit = checkIfHit(shift);
if (isHit == null) break;
if (isHit) return true;
}
for (double shift = worldWidth;; shift += worldWidth) {
final isHit = checkIfHit(shift);
if (isHit == null) break;
if (isHit) return true;
}

return false;
return workAcrossWorlds(checkIfHit);
}

@override
Iterable<CircleMarker<R>> get elements => circles;

late Rect _screenRect;

@override
void paint(Canvas canvas, Size size) {
_screenRect = Offset.zero & size;
canvas.clipRect(_screenRect);

final worldWidth = _getWorldWidth();
super.paint(canvas, size);
canvas.clipRect(viewportRect);

// Let's calculate all the points grouped by color and radius
final points = <Color, Map<double, List<Offset>>>{};
Expand All @@ -84,17 +61,15 @@ base class CirclePainter<R extends Object>
final radiusWithBorder = _getRadiusInPixel(circle, withBorder: true);
final initialCenter = _getOffset(circle.point);

bool checkIfVisible(double shift) {
bool result = false;
/// Draws on a "single-world"
WorldWorkControl drawIfVisible(double shift) {
WorldWorkControl result = WorldWorkControl.invisible;
final center = initialCenter + Offset(shift, 0);

bool isVisible(double radius) {
if (_isVisible(
screenRect: _screenRect,
center: center,
radiusInPixel: radius,
)) {
return result = true;
if (_isVisible(center: center, radiusInPixel: radius)) {
result = WorldWorkControl.visible;
return true;
}
return false;
}
Expand Down Expand Up @@ -123,23 +98,11 @@ base class CirclePainter<R extends Object>
.add(center);
}
}

return result;
}

checkIfVisible(0);

// Repeat over all worlds (<--||-->) until culling determines that
// that element is out of view, and therefore all further elements in
// that direction will also be
if (worldWidth == 0) continue;
for (double shift = -worldWidth;; shift -= worldWidth) {
final isVisible = checkIfVisible(shift);
if (!isVisible) break;
}
for (double shift = worldWidth;; shift += worldWidth) {
final isVisible = checkIfVisible(shift);
if (!isVisible) break;
}
workAcrossWorlds(drawIfVisible);
}

// Now that all the points are grouped, let's draw them
Expand Down Expand Up @@ -203,21 +166,14 @@ base class CirclePainter<R extends Object>
double _getRadiusInPixel(CircleMarker circle, {required bool withBorder}) =>
(withBorder ? circle.borderStrokeWidth / 2 : 0) +
(circle.useRadiusInMeter
? (_getOffset(circle.point) -
_getOffset(
_distance.offset(circle.point, circle.radius, 180)))
.distance
? metersToScreenPixels(circle.point, circle.radius)
: circle.radius);

/// Returns true if a centered circle with this radius is on the screen.
bool _isVisible({
required Rect screenRect,
required Offset center,
required double radiusInPixel,
}) =>
screenRect.overlaps(
Rect.fromCircle(center: center, radius: radiusInPixel),
);

double _getWorldWidth() => camera.getWorldWidthAtZoom();
viewportRect
.overlaps(Rect.fromCircle(center: center, radius: radiusInPixel));
}
42 changes: 42 additions & 0 deletions lib/src/layer/polygon_layer/label.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ LatLng _computeLabelPosition(
) {
return switch (labelPlacement) {
PolygonLabelPlacement.centroid => _computeCentroid(points),
PolygonLabelPlacement.centroidWithMultiWorld =>
_computeCentroidWithMultiWorld(points),
PolygonLabelPlacement.polylabel => _computePolylabel(points),
};
}
Expand All @@ -72,6 +74,29 @@ LatLng _computeCentroid(List<LatLng> points) {
);
}

/// Calculate the centroid of a given list of [LatLng] points with multiple worlds.
LatLng _computeCentroidWithMultiWorld(List<LatLng> points) {
if (points.isEmpty) return _computeCentroid(points);
const halfWorld = 180;
int count = 0;
double sum = 0;
late double lastLng;
for (final LatLng point in points) {
double lng = point.longitude;
count++;
if (count > 1) {
if (lng - lastLng > halfWorld) {
lng -= 2 * halfWorld;
} else if (lng - lastLng < -halfWorld) {
lng += 2 * halfWorld;
}
}
lastLng = lng;
sum += lastLng;
}
return LatLng(points.map((e) => e.latitude).average, sum / count);
}

/// Use the Maxbox Polylabel algorithm to calculate the [LatLng] position for
/// a given list of points.
LatLng _computePolylabel(List<LatLng> points) {
Expand All @@ -93,3 +118,20 @@ LatLng _computePolylabel(List<LatLng> points) {
labelPosition.point.x.toDouble(),
);
}

/// Defines the algorithm used to calculate the position of the [Polygon] label.
///
/// > [!IMPORTANT]
/// > If your project allows users to browse across multiple worlds, and your
/// > polygons may be over the anti-meridan boundary, [centroidWithMultiWorld]
/// > must be used - other algorithms will produce unexpected results.
enum PolygonLabelPlacement {
/// Use the centroid of the [Polygon] outline as position for the label.
centroid,

/// Use the centroid in a multi-world as position for the label.
centroidWithMultiWorld,

/// Use the Mapbox Polylabel algorithm as position for the label.
polylabel,
}
Loading