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
18 changes: 12 additions & 6 deletions example/lib/pages/circle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ class _CirclePageState extends State<CirclePage> {
List<HitValue>? _prevHitValues;
List<CircleMarker<HitValue>>? _hoverCircles;

static const double _initialBorderStrokeWidth = 2;
static const double _hoverBorderStrokeWidth = 15;

final _circlesRaw = <CircleMarker<HitValue>>[
CircleMarker(
point: const LatLng(51.5, -0.09),
color: Colors.white.withAlpha(178),
borderColor: Colors.black,
borderStrokeWidth: 2,
borderStrokeWidth: _initialBorderStrokeWidth,
useRadiusInMeter: false,
radius: 100,
hitValue: (title: 'White', subtitle: 'Radius in logical pixels'),
Expand All @@ -35,7 +38,7 @@ class _CirclePageState extends State<CirclePage> {
point: const LatLng(51.5, -0.09),
color: Colors.black.withAlpha(178),
borderColor: Colors.black,
borderStrokeWidth: 2,
borderStrokeWidth: _initialBorderStrokeWidth,
useRadiusInMeter: false,
radius: 50,
hitValue: (
Expand All @@ -48,9 +51,10 @@ class _CirclePageState extends State<CirclePage> {
// Dorney Lake is ~2km long
color: Colors.green.withAlpha(229),
borderColor: Colors.black,
borderStrokeWidth: 2,
borderStrokeWidth: _initialBorderStrokeWidth,
useRadiusInMeter: true,
radius: 1000, // 1000 meters
radius: 1000,
// 1000 meters
hitValue: (
title: 'Green',
subtitle: 'Radius in meters, calibrated over ~2km rowing lake'
Expand Down Expand Up @@ -87,10 +91,12 @@ class _CirclePageState extends State<CirclePage> {

return CircleMarker<HitValue>(
point: original.point,
radius: original.radius + 6.5,
radius: original.radius +
_initialBorderStrokeWidth / 2 +
_hoverBorderStrokeWidth / 2,
useRadiusInMeter: original.useRadiusInMeter,
color: Colors.transparent,
borderStrokeWidth: 15,
borderStrokeWidth: _hoverBorderStrokeWidth,
borderColor: Colors.green,
);
}).toList();
Expand Down
33 changes: 33 additions & 0 deletions example/lib/pages/multi_worlds.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class MultiWorldsPage extends StatefulWidget {
}

class _MultiWorldsPageState extends State<MultiWorldsPage> {
final LayerHitNotifier<String> _hitNotifier = ValueNotifier(null);

@override
Widget build(BuildContext context) {
return Scaffold(
Expand All @@ -30,6 +32,37 @@ class _MultiWorldsPageState extends State<MultiWorldsPage> {
),
children: [
openStreetMapTileLayer,
GestureDetector(
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_hitNotifier.value!.hitValues.join(', ')),
duration: const Duration(seconds: 1),
showCloseIcon: true,
),
),
child: CircleLayer<String>(
circles: [
const CircleMarker(
point: LatLng(-27.466667, 153.033333),
radius: 1000000,
color: Color.from(alpha: .8, red: 1, green: 1, blue: 0),
borderColor: Colors.green,
borderStrokeWidth: 2,
hitValue: 'Brisbane',
useRadiusInMeter: true,
),
const CircleMarker(
point: LatLng(45.466667, 9.166667),
radius: 10,
color: Colors.green,
borderColor: Colors.red,
borderStrokeWidth: 2,
hitValue: 'Milan',
),
],
hitNotifier: _hitNotifier,
),
),
MarkerLayer(
markers: [
Marker(
Expand Down
181 changes: 128 additions & 53 deletions lib/src/layer/circle_layer/painter.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
part of 'circle_layer.dart';

/// The [CustomPainter] used to draw [CircleMarker] for the [CircleLayer].
@immutable
base class CirclePainter<R extends Object>
extends HitDetectablePainter<R, CircleMarker<R>> {
/// Reference to the list of [CircleMarker]s of the [CircleLayer].
Expand All @@ -23,76 +22,129 @@ base class CirclePainter<R extends Object>
required Offset point,
required LatLng coordinate,
}) {
final circle = element; // Should be optimized out by compiler, avoids lint

final center = camera.getOffsetFromOrigin(circle.point);
final radius = circle.useRadiusInMeter
? (center -
camera.getOffsetFromOrigin(
_distance.offset(circle.point, circle.radius, 180)))
.distance
: circle.radius;

return pow(point.dx - center.dx, 2) + pow(point.dy - center.dy, 2) <=
radius * radius;
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) {
final center = initialCenter + Offset(shift, 0);
if (!_isVisible(
screenRect: _screenRect,
center: center,
radiusInPixel: radius,
)) {
return null;
}

return pow(point.dx - center.dx, 2) + pow(point.dy - center.dy, 2) <=
radius * radius;
}

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;
}

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

late Rect _screenRect;

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

final worldWidth = _getWorldWidth();

// Let's calculate all the points grouped by color and radius
final points = <Color, Map<double, List<Offset>>>{};
final pointsFilledBorder = <Color, Map<double, List<Offset>>>{};
final pointsBorder = <Color, Map<double, Map<double, List<Offset>>>>{};
for (final circle in circles) {
final center = camera.getOffsetFromOrigin(circle.point);
final radius = circle.useRadiusInMeter
? (center -
camera.getOffsetFromOrigin(
_distance.offset(circle.point, circle.radius, 180)))
.distance
: circle.radius;
points[circle.color] ??= {};
points[circle.color]![radius] ??= [];
points[circle.color]![radius]!.add(center);

if (circle.borderStrokeWidth > 0) {
// Check if color have some transparency or not
// As drawPoints is more efficient than drawCircle
if (circle.color.a == 1) {
double radiusBorder = circle.radius + circle.borderStrokeWidth;
if (circle.useRadiusInMeter) {
final rBorder = _distance.offset(circle.point, radiusBorder, 180);
final deltaBorder = center - camera.getOffsetFromOrigin(rBorder);
radiusBorder = deltaBorder.distance;
final radiusWithoutBorder = _getRadiusInPixel(circle, withBorder: false);
final radiusWithBorder = _getRadiusInPixel(circle, withBorder: true);
final initialCenter = _getOffset(circle.point);

bool checkIfVisible(double shift) {
bool result = false;
final center = initialCenter + Offset(shift, 0);

bool isVisible(double radius) {
if (_isVisible(
screenRect: _screenRect,
center: center,
radiusInPixel: radius,
)) {
return result = true;
}
pointsFilledBorder[circle.borderColor] ??= {};
pointsFilledBorder[circle.borderColor]![radiusBorder] ??= [];
pointsFilledBorder[circle.borderColor]![radiusBorder]!.add(center);
} else {
double realRadius = circle.radius;
if (circle.useRadiusInMeter) {
final rBorder = _distance.offset(circle.point, realRadius, 180);
final deltaBorder = center - camera.getOffsetFromOrigin(rBorder);
realRadius = deltaBorder.distance;
return false;
}

if (isVisible(radiusWithoutBorder)) {
points[circle.color] ??= {};
points[circle.color]![radiusWithoutBorder] ??= [];
points[circle.color]![radiusWithoutBorder]!.add(center);
}

if (circle.borderStrokeWidth > 0 && isVisible(radiusWithBorder)) {
// Check if color have some transparency or not
// As drawPoints is more efficient than drawCircle
if (circle.color.a == 1) {
pointsFilledBorder[circle.borderColor] ??= {};
pointsFilledBorder[circle.borderColor]![radiusWithBorder] ??= [];
pointsFilledBorder[circle.borderColor]![radiusWithBorder]!
.add(center);
} else {
pointsBorder[circle.borderColor] ??= {};
pointsBorder[circle.borderColor]![circle.borderStrokeWidth] ??= {};
pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![
radiusWithoutBorder] ??= [];
pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![
radiusWithoutBorder]!
.add(center);
}
pointsBorder[circle.borderColor] ??= {};
pointsBorder[circle.borderColor]![circle.borderStrokeWidth] ??= {};
pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![
realRadius] ??= [];
pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![
realRadius]!
.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;
}
}

// Now that all the points are grouped, let's draw them

// First, the border when with non opaque disk
final paintBorder = Paint()..style = PaintingStyle.stroke;
for (final color in pointsBorder.keys) {
final paint = paintBorder..color = color;
Expand All @@ -108,7 +160,7 @@ base class CirclePainter<R extends Object>
}
}

// Then the filled border in order to be under the circle
// Then the filled border in order to be under the disk
final paintPoint = Paint()
..isAntiAlias = false
..strokeCap = StrokeCap.round;
Expand All @@ -122,7 +174,7 @@ base class CirclePainter<R extends Object>
}
}

// And then the circle
// And then the disk
for (final color in points.keys) {
final paint = paintPoint..color = color;
final pointsByRadius = points[color]!;
Expand All @@ -145,4 +197,27 @@ base class CirclePainter<R extends Object>
@override
bool shouldRepaint(CirclePainter oldDelegate) =>
circles != oldDelegate.circles || camera != oldDelegate.camera;

Offset _getOffset(LatLng pos) => camera.getOffsetFromOrigin(pos);

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
: 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();
}