Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added interactivity to CircleLayer & refactored interactivity out into seperate classes #1886

Merged
merged 8 commits into from
May 23, 2024
165 changes: 144 additions & 21 deletions example/lib/pages/circle.dart
Original file line number Diff line number Diff line change
@@ -1,49 +1,172 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_example/misc/tile_providers.dart';
import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart';
import 'package:latlong2/latlong.dart';

class CirclePage extends StatelessWidget {
typedef HitValue = ({String title, String subtitle});

class CirclePage extends StatefulWidget {
static const String route = '/circle';

const CirclePage({super.key});

@override
State<CirclePage> createState() => _CirclePageState();
}

class _CirclePageState extends State<CirclePage> {
final LayerHitNotifier<HitValue> _hitNotifier = ValueNotifier(null);
List<HitValue>? _prevHitValues;
List<CircleMarker<HitValue>>? _hoverCircles;

final _circlesRaw = <CircleMarker<HitValue>>[
CircleMarker(
point: const LatLng(51.5, -0.09),
color: Colors.blue.withOpacity(0.7),
borderColor: Colors.black,
borderStrokeWidth: 2,
useRadiusInMeter: false,
radius: 100,
hitValue: (title: 'Blue', subtitle: 'Radius in logical pixels'),
),
CircleMarker(
point: const LatLng(51.4937, -0.6638),
// Dorney Lake is ~2km long
color: Colors.green.withOpacity(0.9),
borderColor: Colors.black,
borderStrokeWidth: 2,
useRadiusInMeter: true,
radius: 1000, // 1000 meters
hitValue: (
title: 'Green',
subtitle: 'Radius in meters, calibrated over ~2km rowing lake'
),
),
];
late final _circles =
Map.fromEntries(_circlesRaw.map((e) => MapEntry(e.hitValue, e)));

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Circle')),
drawer: const MenuDrawer(route),
appBar: AppBar(title: const Text('Circles')),
drawer: const MenuDrawer(CirclePage.route),
body: FlutterMap(
options: const MapOptions(
initialCenter: LatLng(51.5, -0.09),
initialZoom: 11,
),
children: [
openStreetMapTileLayer,
CircleLayer(
circles: [
CircleMarker(
point: const LatLng(51.5, -0.09),
color: Colors.blue.withOpacity(0.7),
borderColor: Colors.black,
borderStrokeWidth: 2,
useRadiusInMeter: true,
radius: 2000, // 2000 meters
MouseRegion(
hitTestBehavior: HitTestBehavior.deferToChild,
cursor: SystemMouseCursors.click,
onHover: (_) {
final hitValues = _hitNotifier.value?.hitValues.toList();
if (hitValues == null) return;

if (listEquals(hitValues, _prevHitValues)) return;
_prevHitValues = hitValues;

final hoverCircles = hitValues.map((v) {
final original = _circles[v]!;

return CircleMarker<HitValue>(
point: original.point,
radius: original.radius,
useRadiusInMeter: original.useRadiusInMeter,
color: Colors.transparent,
borderStrokeWidth: 15,
borderColor: Colors.green,
);
}).toList();
setState(() => _hoverCircles = hoverCircles);
},
onExit: (_) {
_prevHitValues = null;
setState(() => _hoverCircles = null);
},
child: GestureDetector(
onTap: () => _openTouchedCirclesModal(
'Tapped',
_hitNotifier.value!.hitValues,
_hitNotifier.value!.coordinate,
),
CircleMarker(
point: const LatLng(51.4937, -0.6638),
// Dorney Lake is ~2km long
color: Colors.green.withOpacity(0.9),
borderColor: Colors.black,
borderStrokeWidth: 2,
useRadiusInMeter: true,
radius: 1000, // 1000 meters
onLongPress: () => _openTouchedCirclesModal(
'Long pressed',
_hitNotifier.value!.hitValues,
_hitNotifier.value!.coordinate,
),
],
onSecondaryTap: () => _openTouchedCirclesModal(
'Secondary tapped',
_hitNotifier.value!.hitValues,
_hitNotifier.value!.coordinate,
),
child: CircleLayer(
hitNotifier: _hitNotifier,
circles: [..._circlesRaw, ...?_hoverCircles],
),
),
),
],
),
);
}

void _openTouchedCirclesModal(
String eventType,
List<HitValue> tappedCircles,
LatLng coords,
) {
showModalBottomSheet<void>(
context: context,
builder: (context) => Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Tapped Circle(s)',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
Text(
'$eventType at point: (${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})',
),
const SizedBox(height: 8),
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
final tappedLineData = tappedCircles[index];
return ListTile(
leading: index == 0
? const Icon(Icons.vertical_align_top)
: index == tappedCircles.length - 1
? const Icon(Icons.vertical_align_bottom)
: const SizedBox.shrink(),
title: Text(tappedLineData.title),
subtitle: Text(tappedLineData.subtitle),
dense: true,
);
},
itemCount: tappedCircles.length,
),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
),
),
],
),
),
);
}
}
3 changes: 2 additions & 1 deletion lib/flutter_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export 'package:flutter_map/src/layer/attribution_layer/rich/widget.dart';
export 'package:flutter_map/src/layer/attribution_layer/simple.dart';
export 'package:flutter_map/src/layer/circle_layer/circle_layer.dart';
export 'package:flutter_map/src/layer/marker_layer/marker_layer.dart';
export 'package:flutter_map/src/layer/misc/hit_detection.dart';
export 'package:flutter_map/src/layer/misc/layer_interactivity/layer_hit_notifier.dart';
export 'package:flutter_map/src/layer/misc/layer_interactivity/layer_hit_result.dart';
export 'package:flutter_map/src/layer/misc/line_patterns/stroke_pattern.dart';
export 'package:flutter_map/src/layer/misc/mobile_layer_transformer.dart';
export 'package:flutter_map/src/layer/misc/translucent_pointer.dart';
Expand Down
24 changes: 19 additions & 5 deletions lib/src/layer/circle_layer/circle_layer.dart
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
import 'dart:math';
import 'dart:ui';

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

part 'circle_marker.dart';
part 'painter.dart';

/// A layer that displays a list of [CircleMarker] on the map
@immutable
class CircleLayer extends StatelessWidget {
class CircleLayer<R extends Object> extends StatelessWidget {
/// The list of [CircleMarker]s.
final List<CircleMarker> circles;
final List<CircleMarker<R>> circles;

/// Create a new [CircleLayer] as a child for flutter map
const CircleLayer({super.key, required this.circles});
/// {@macro fm.lhn.layerHitNotifier.usage}
final LayerHitNotifier<R>? hitNotifier;

/// Create a new [CircleLayer] as a child for [FlutterMap]
const CircleLayer({
super.key,
required this.circles,
this.hitNotifier,
});

@override
Widget build(BuildContext context) {
final camera = MapCamera.of(context);

return MobileLayerTransformer(
child: CustomPaint(
painter: CirclePainter(circles, camera),
painter: CirclePainter(
circles: circles,
camera: camera,
hitNotifier: hitNotifier,
),
size: Size(camera.size.x, camera.size.y),
isComplex: true,
),
Expand Down
3 changes: 2 additions & 1 deletion lib/src/layer/circle_layer/circle_marker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ part of 'circle_layer.dart';
/// Immutable marker options for [CircleMarker]. Circle markers are a more
/// simple and performant way to draw markers as the regular [Marker]
@immutable
class CircleMarker {
base class CircleMarker<R extends Object> extends HitDetectableElement<R> {
/// An optional [Key] for the [CircleMarker].
/// This key is not used internally.
final Key? key;
Expand Down Expand Up @@ -36,5 +36,6 @@ class CircleMarker {
this.color = const Color(0xFF00FF00),
this.borderStrokeWidth = 0.0,
this.borderColor = const Color(0xFFFFFF00),
super.hitValue,
});
}
68 changes: 47 additions & 21 deletions lib/src/layer/circle_layer/painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,46 @@ part of 'circle_layer.dart';

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

/// Reference to the [MapCamera].
final MapCamera camera;
final List<CircleMarker<R>> circles;

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

static const _distance = Distance();

@override
bool elementHitTest(
CircleMarker<R> element, {
required Point<double> 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.x - center.dx, 2) + pow(point.y - center.dy, 2) <=
radius * radius;
}

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

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

Expand All @@ -24,35 +50,35 @@ class CirclePainter extends CustomPainter {
final pointsFilledBorder = <Color, Map<double, List<Offset>>>{};
final pointsBorder = <Color, Map<double, Map<double, List<Offset>>>>{};
for (final circle in circles) {
final offset = camera.getOffsetFromOrigin(circle.point);
double radius = circle.radius;
if (circle.useRadiusInMeter) {
final r = distance.offset(circle.point, circle.radius, 180);
final delta = offset - camera.getOffsetFromOrigin(r);
radius = delta.distance;
}
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(offset);
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.alpha == 0xFF) {
double radiusBorder = circle.radius + circle.borderStrokeWidth;
if (circle.useRadiusInMeter) {
final rBorder = distance.offset(circle.point, radiusBorder, 180);
final deltaBorder = offset - camera.getOffsetFromOrigin(rBorder);
final rBorder = _distance.offset(circle.point, radiusBorder, 180);
final deltaBorder = center - camera.getOffsetFromOrigin(rBorder);
radiusBorder = deltaBorder.distance;
}
pointsFilledBorder[circle.borderColor] ??= {};
pointsFilledBorder[circle.borderColor]![radiusBorder] ??= [];
pointsFilledBorder[circle.borderColor]![radiusBorder]!.add(offset);
pointsFilledBorder[circle.borderColor]![radiusBorder]!.add(center);
} else {
double realRadius = circle.radius;
if (circle.useRadiusInMeter) {
final rBorder = distance.offset(circle.point, realRadius, 180);
final deltaBorder = offset - camera.getOffsetFromOrigin(rBorder);
final rBorder = _distance.offset(circle.point, realRadius, 180);
final deltaBorder = center - camera.getOffsetFromOrigin(rBorder);
realRadius = deltaBorder.distance;
}
pointsBorder[circle.borderColor] ??= {};
Expand All @@ -61,7 +87,7 @@ class CirclePainter extends CustomPainter {
realRadius] ??= [];
pointsBorder[circle.borderColor]![circle.borderStrokeWidth]![
realRadius]!
.add(offset);
.add(center);
}
}
}
Expand Down
Loading
Loading