|
| 1 | +import 'package:flutter/foundation.dart'; |
1 | 2 | import 'package:flutter/material.dart';
|
2 | 3 | import 'package:flutter_map/flutter_map.dart';
|
3 | 4 | import 'package:flutter_map_example/misc/tile_providers.dart';
|
4 | 5 | import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart';
|
5 | 6 | import 'package:latlong2/latlong.dart';
|
6 | 7 |
|
7 |
| -class CirclePage extends StatelessWidget { |
| 8 | +typedef HitValue = ({String title, String subtitle}); |
| 9 | + |
| 10 | +class CirclePage extends StatefulWidget { |
8 | 11 | static const String route = '/circle';
|
9 | 12 |
|
10 | 13 | const CirclePage({super.key});
|
11 | 14 |
|
| 15 | + @override |
| 16 | + State<CirclePage> createState() => _CirclePageState(); |
| 17 | +} |
| 18 | + |
| 19 | +class _CirclePageState extends State<CirclePage> { |
| 20 | + final LayerHitNotifier<HitValue> _hitNotifier = ValueNotifier(null); |
| 21 | + List<HitValue>? _prevHitValues; |
| 22 | + List<CircleMarker<HitValue>>? _hoverCircles; |
| 23 | + |
| 24 | + final _circlesRaw = <CircleMarker<HitValue>>[ |
| 25 | + CircleMarker( |
| 26 | + point: const LatLng(51.5, -0.09), |
| 27 | + color: Colors.white.withOpacity(0.7), |
| 28 | + borderColor: Colors.black, |
| 29 | + borderStrokeWidth: 2, |
| 30 | + useRadiusInMeter: false, |
| 31 | + radius: 100, |
| 32 | + hitValue: (title: 'White', subtitle: 'Radius in logical pixels'), |
| 33 | + ), |
| 34 | + CircleMarker( |
| 35 | + point: const LatLng(51.5, -0.09), |
| 36 | + color: Colors.black.withOpacity(0.7), |
| 37 | + borderColor: Colors.black, |
| 38 | + borderStrokeWidth: 2, |
| 39 | + useRadiusInMeter: false, |
| 40 | + radius: 50, |
| 41 | + hitValue: ( |
| 42 | + title: 'Black', |
| 43 | + subtitle: 'Radius in logical pixels, should be above white.', |
| 44 | + ), |
| 45 | + ), |
| 46 | + CircleMarker( |
| 47 | + point: const LatLng(51.4937, -0.6638), |
| 48 | + // Dorney Lake is ~2km long |
| 49 | + color: Colors.green.withOpacity(0.9), |
| 50 | + borderColor: Colors.black, |
| 51 | + borderStrokeWidth: 2, |
| 52 | + useRadiusInMeter: true, |
| 53 | + radius: 1000, // 1000 meters |
| 54 | + hitValue: ( |
| 55 | + title: 'Green', |
| 56 | + subtitle: 'Radius in meters, calibrated over ~2km rowing lake' |
| 57 | + ), |
| 58 | + ), |
| 59 | + ]; |
| 60 | + late final _circles = |
| 61 | + Map.fromEntries(_circlesRaw.map((e) => MapEntry(e.hitValue, e))); |
| 62 | + |
12 | 63 | @override
|
13 | 64 | Widget build(BuildContext context) {
|
14 | 65 | return Scaffold(
|
15 |
| - appBar: AppBar(title: const Text('Circle')), |
16 |
| - drawer: const MenuDrawer(route), |
| 66 | + appBar: AppBar(title: const Text('Circles')), |
| 67 | + drawer: const MenuDrawer(CirclePage.route), |
17 | 68 | body: FlutterMap(
|
18 | 69 | options: const MapOptions(
|
19 | 70 | initialCenter: LatLng(51.5, -0.09),
|
20 | 71 | initialZoom: 11,
|
21 | 72 | ),
|
22 | 73 | children: [
|
23 | 74 | openStreetMapTileLayer,
|
24 |
| - CircleLayer( |
25 |
| - circles: [ |
26 |
| - CircleMarker( |
27 |
| - point: const LatLng(51.5, -0.09), |
28 |
| - color: Colors.blue.withOpacity(0.7), |
29 |
| - borderColor: Colors.black, |
30 |
| - borderStrokeWidth: 2, |
31 |
| - useRadiusInMeter: true, |
32 |
| - radius: 2000, // 2000 meters |
| 75 | + MouseRegion( |
| 76 | + hitTestBehavior: HitTestBehavior.deferToChild, |
| 77 | + cursor: SystemMouseCursors.click, |
| 78 | + onHover: (_) { |
| 79 | + final hitValues = _hitNotifier.value?.hitValues.toList(); |
| 80 | + if (hitValues == null) return; |
| 81 | + |
| 82 | + if (listEquals(hitValues, _prevHitValues)) return; |
| 83 | + _prevHitValues = hitValues; |
| 84 | + |
| 85 | + final hoverCircles = hitValues.map((v) { |
| 86 | + final original = _circles[v]!; |
| 87 | + |
| 88 | + return CircleMarker<HitValue>( |
| 89 | + point: original.point, |
| 90 | + radius: original.radius + 6.5, |
| 91 | + useRadiusInMeter: original.useRadiusInMeter, |
| 92 | + color: Colors.transparent, |
| 93 | + borderStrokeWidth: 15, |
| 94 | + borderColor: Colors.green, |
| 95 | + ); |
| 96 | + }).toList(); |
| 97 | + setState(() => _hoverCircles = hoverCircles); |
| 98 | + }, |
| 99 | + onExit: (_) { |
| 100 | + _prevHitValues = null; |
| 101 | + setState(() => _hoverCircles = null); |
| 102 | + }, |
| 103 | + child: GestureDetector( |
| 104 | + onTap: () => _openTouchedCirclesModal( |
| 105 | + 'Tapped', |
| 106 | + _hitNotifier.value!.hitValues, |
| 107 | + _hitNotifier.value!.coordinate, |
| 108 | + ), |
| 109 | + onLongPress: () => _openTouchedCirclesModal( |
| 110 | + 'Long pressed', |
| 111 | + _hitNotifier.value!.hitValues, |
| 112 | + _hitNotifier.value!.coordinate, |
| 113 | + ), |
| 114 | + onSecondaryTap: () => _openTouchedCirclesModal( |
| 115 | + 'Secondary tapped', |
| 116 | + _hitNotifier.value!.hitValues, |
| 117 | + _hitNotifier.value!.coordinate, |
33 | 118 | ),
|
34 |
| - CircleMarker( |
35 |
| - point: const LatLng(51.4937, -0.6638), |
36 |
| - // Dorney Lake is ~2km long |
37 |
| - color: Colors.green.withOpacity(0.9), |
38 |
| - borderColor: Colors.black, |
39 |
| - borderStrokeWidth: 2, |
40 |
| - useRadiusInMeter: true, |
41 |
| - radius: 1000, // 1000 meters |
| 119 | + child: CircleLayer( |
| 120 | + hitNotifier: _hitNotifier, |
| 121 | + circles: [ |
| 122 | + ..._circlesRaw, |
| 123 | + ...?_hoverCircles, |
| 124 | + ], |
42 | 125 | ),
|
43 |
| - ], |
| 126 | + ), |
44 | 127 | ),
|
45 | 128 | ],
|
46 | 129 | ),
|
47 | 130 | );
|
48 | 131 | }
|
| 132 | + |
| 133 | + void _openTouchedCirclesModal( |
| 134 | + String eventType, |
| 135 | + List<HitValue> tappedCircles, |
| 136 | + LatLng coords, |
| 137 | + ) { |
| 138 | + showModalBottomSheet<void>( |
| 139 | + context: context, |
| 140 | + builder: (context) => Padding( |
| 141 | + padding: const EdgeInsets.all(16), |
| 142 | + child: Column( |
| 143 | + crossAxisAlignment: CrossAxisAlignment.start, |
| 144 | + children: [ |
| 145 | + const Text( |
| 146 | + 'Tapped Circle(s)', |
| 147 | + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), |
| 148 | + ), |
| 149 | + Text( |
| 150 | + '$eventType at point: (${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})', |
| 151 | + ), |
| 152 | + const SizedBox(height: 8), |
| 153 | + Expanded( |
| 154 | + child: ListView.builder( |
| 155 | + itemBuilder: (context, index) { |
| 156 | + final tappedLineData = tappedCircles[index]; |
| 157 | + return ListTile( |
| 158 | + leading: index == 0 |
| 159 | + ? const Icon(Icons.vertical_align_top) |
| 160 | + : index == tappedCircles.length - 1 |
| 161 | + ? const Icon(Icons.vertical_align_bottom) |
| 162 | + : const SizedBox.shrink(), |
| 163 | + title: Text(tappedLineData.title), |
| 164 | + subtitle: Text(tappedLineData.subtitle), |
| 165 | + dense: true, |
| 166 | + ); |
| 167 | + }, |
| 168 | + itemCount: tappedCircles.length, |
| 169 | + ), |
| 170 | + ), |
| 171 | + const SizedBox(height: 8), |
| 172 | + Align( |
| 173 | + alignment: Alignment.bottomCenter, |
| 174 | + child: SizedBox( |
| 175 | + width: double.infinity, |
| 176 | + child: OutlinedButton( |
| 177 | + onPressed: () => Navigator.pop(context), |
| 178 | + child: const Text('Close'), |
| 179 | + ), |
| 180 | + ), |
| 181 | + ), |
| 182 | + ], |
| 183 | + ), |
| 184 | + ), |
| 185 | + ); |
| 186 | + } |
49 | 187 | }
|
0 commit comments