I have used flutter_map package:https://github.com/fleaflet/flutter_map
I have already modified it for my own project in such a way that a user can use 1 click to start drawing, simplifying hovering mouse ( no clicks needed), a 2nd click to stop drawing, followed by either a long press to discard the drawing or a right click to save the polygon.
The problem is that ONLY the last one that is just drawn can be seen and as soon as a new polygon is started to be drawn, the previous one disappeared (all others already hidden ;) )
Any comments please?
FYI, my desired platform is Windows only ...
import 'package:flutter/foundation.dart';import 'package:flutter/material.dart';import 'package:flutter_map/flutter_map.dart';import 'package:flutter_map_example/main.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'; import 'dart:math';int polygonssaved = 1; // one is defined as a constant belowtypedef HitValue = ({String title, String subtitle});void writedatajson(List <LatLng> coordinates) async{ Geofences geo; if (defaultTargetPlatform == TargetPlatform.windows) { final parsedCoordinates=coordinates.toString().replaceAll('LatLng(latitude:','[').replaceAll(', longitude:',',').replaceAll('), ','],').replaceAll(')',']'); geo= Geofences(1,mycomputerName , DateTime.now().toString(),'myaudio' ,'emeregency','myprovince' ,parsedCoordinates ,'active',1); geo.writeJsonFile(); }}class PolygonPage extends StatefulWidget { static const String route = '/polygon'; const PolygonPage({super.key}); @override State<PolygonPage> createState() => _PolygonPageState();}class _PolygonPageState extends State<PolygonPage> { bool drawingfinished=true, oncedone=false; List<LatLng> mypolygonpoints=List.empty(growable: true); static const double pointSize = 65; static const double pointY = 250; final mapController = MapController(); LatLng? latLng; Polygon<HitValue> buildPolygon(List<LatLng> points, HitValue hitvals) { final Polygon<HitValue> p= Polygon<HitValue>( points:points, pattern: StrokePattern.dashed(segments: const [50, 20]), // pattern: const StrokePattern.dotted(), color: Colors.blue, borderColor: Colors.red, rotateLabel: true, // label: 'Pre-defined polygon ', borderStrokeWidth: 2, hitValue: ( title: 'Created at ${DateTime.now()}', subtitle: '- Nothing really special here...' ), ); return p; } final LayerHitNotifier<HitValue> _hitNotifier = ValueNotifier(null); List<HitValue>? _prevHitValues; List<Polygon<HitValue>>? _hoverGons; List<Polygon<HitValue>> polygonsRaw =<Polygon<HitValue>>[ Polygon( points: const [LatLng(-37.927145, 144.984677), LatLng(-37.931937, 144.985144), LatLng(-37.932306, 144.985144), LatLng(-37.932674, 144.985144), LatLng(-37.932859, 144.985144), LatLng(-37.933227, 144.985144), LatLng(-37.933596, 144.985144), LatLng(-37.93378, 144.98491), LatLng(-37.934149, 144.984443), LatLng(-37.934518, 144.984443), LatLng(-37.934886, 144.984209), LatLng(-37.935071, 144.983742), LatLng(-37.935071, 144.983274), LatLng(-37.935439, 144.983274), LatLng(-37.935439, 144.982807), LatLng(-37.935808, 144.982807), LatLng(-37.935808, 144.982573), LatLng(-37.935808, 144.982106), LatLng(-37.935808, 144.981639), LatLng(-37.935808, 144.981405), LatLng(-37.935808, 144.980938), LatLng(-37.935808, 144.98047), LatLng(-37.935623, 144.980236), LatLng(-37.935623, 144.979769), LatLng(-37.935255, 144.979302), LatLng(-37.935255, 144.978834), LatLng(-37.934886, 144.978133), LatLng(-37.934702, 144.977666), LatLng(-37.934702, 144.977198), LatLng(-37.934333, 144.976965), LatLng(-37.934333, 144.976497), LatLng(-37.933965, 144.976497), LatLng(-37.933965, 144.97603), LatLng(-37.93378, 144.97603), LatLng(-37.93378, 144.975563), LatLng(-37.933412, 144.975329), LatLng(-37.933412, 144.974862), LatLng(-37.933043, 144.974862), LatLng(-37.933043, 144.974394), LatLng(-37.933043, 144.974394)], pattern: StrokePattern.dashed(segments: const [50, 20]), // pattern: const StrokePattern.dotted(), color: Colors.blue, borderColor: Colors.red, rotateLabel: true, // label: 'Pre-defined polygon ', borderStrokeWidth: 2, hitValue: ( title: 'Created at ${DateTime.now()}', subtitle: '- Nothing really special here...' ), ), ]; //final List<Polygon<HitValue>> _polygonsRaw = List.empty(growable: true); late var _polygons = Map.fromEntries(polygonsRaw.map((e) => MapEntry(e.hitValue, e))); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Draw & Manage Geofences ...')), drawer: const MenuDrawer(PolygonPage.route), body: Stack( children: [ Flexible( child: FlutterMap( mapController: mapController, options: MapOptions( keepAlive: true, onPositionChanged: (_, __) => updatePoint(context), onTap: (tapPos, latLng) =>{ drawingfinished=drawingfinished?false:true, if (drawingfinished==false) {mypolygonpoints.clear(),// first time click means just started gathering latlngs (previous ones must be deleted) print('1st click ...> drawing has just started ')}, ScaffoldMessenger.maybeOf(context)?.showSnackBar( SnackBar(duration: const Duration(milliseconds: 100), backgroundColor:Colors.green[400] , content: Text('Primary tap at $latLng')) ), mypolygonpoints.add(latLng), if (drawingfinished==false) print('2nd click ...> drawing has just Ended for total number of points ${mypolygonpoints.length} selected ... ') //called when the 2nd click (finished drawing indicator) is called reporting number of locations gathered }, onPointerHover: (tapPos, latLng) =>{ //freehand print('moved .............'), if (!drawingfinished) { mypolygonpoints.add(latLng), setState(() => polygonsRaw.add(buildPolygon(mypolygonpoints,( title:'Created at ${DateTime.now()}', subtitle: 'the subtitle test' )))) , _polygons = Map.fromEntries(polygonsRaw.map((e) => MapEntry(e.hitValue, e))), } }, onLongPress: (tapPos, latLng) => { ScaffoldMessenger.maybeOf(context)?.showSnackBar( SnackBar(duration: const Duration(milliseconds: 1000), padding: const EdgeInsets.symmetric( horizontal: 100, ), backgroundColor:mypolygonpoints.isNotEmpty ? Colors.red[400] : Colors.blueAccent, width: 500, behavior: SnackBarBehavior.floating, content: mypolygonpoints.isEmpty ? const Text('No polygons to delete !'): const Text('Polygon Deleted successfully ...') , shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(100), ))), if(mypolygonpoints.isNotEmpty &&drawingfinished ) { setState(() => polygonsRaw.removeRange(polygonssaved,polygonsRaw.length)), _polygons = Map.fromEntries(polygonsRaw.map((e) => MapEntry(e.hitValue, e))), print('Long press : ${mypolygonpoints.length} points Deleted --> number of polygons left :${polygonsRaw.length}'), } }, onSecondaryTap: (tapPos, latLng) => { ScaffoldMessenger.maybeOf(context)?.showSnackBar( SnackBar(duration: const Duration(milliseconds: 1000), padding: const EdgeInsets.symmetric( horizontal: 100, ), backgroundColor:mypolygonpoints.isNotEmpty ? Colors.red[600] : Colors.greenAccent, width: 500, behavior: SnackBarBehavior.floating, content: mypolygonpoints.isEmpty ? const Text('No polygons to save !'): const Text('Polygon saved successfully ...') , shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(100), ))), if(mypolygonpoints.isNotEmpty && drawingfinished) finalizepolygonsaving(), }, initialCenter: myglobalccp, initialZoom: 20, ), children: [ openStreetMapTileLayer, if (latLng != null) MarkerLayer( markers: [ Marker( width: pointSize, height: pointSize, point: latLng!, child: const Icon( Icons.circle, size: 10, color: Color.fromARGB(255, 252, 3, 3), ), ) ], ), MouseRegion( hitTestBehavior: HitTestBehavior.deferToChild, cursor: SystemMouseCursors.click, onHover: (_) { if((!drawingfinished) || mypolygonpoints.isNotEmpty ) return; // added to avoid unrequired processing fees ( wait till polygon is saved or discarded ) final hitValues = _hitNotifier.value?.hitValues.toList(); if (hitValues == null) return; if (listEquals(hitValues, _prevHitValues)) return; _prevHitValues = hitValues; final hoverLines = hitValues.map((v) { // _polygons = Map.fromEntries(polygonsRaw.map((e) => MapEntry(e.hitValue, e))); final original = _polygons[v]!; return Polygon<HitValue>( points: original.points, holePointsList: original.holePointsList, color: Colors.blue, borderStrokeWidth: 2, borderColor: Colors.green, disableHolesBorder: original.disableHolesBorder, ); }).toList(); setState(() => _hoverGons = hoverLines); }, onExit: (_) {if((!drawingfinished) || mypolygonpoints.isNotEmpty ) return; // added to avoid unrequired processing fees ( wait till polygon is saved or discarded ) _prevHitValues = null; setState(() => _hoverGons = null); }, child: GestureDetector( onTap: () =>_openTouchedGonsModal('Clicked ', _hitNotifier.value!.hitValues, _hitNotifier.value!.coordinate, ), onLongPress: () =>{ _openTouchedGonsModal('Long press ', _hitNotifier.value!.hitValues, _hitNotifier.value!.coordinate, ), }, onSecondaryTap: () => _openTouchedGonsModal('Secondary click ', _hitNotifier.value!.hitValues, _hitNotifier.value!.coordinate, ), child: PolygonLayer( hitNotifier: _hitNotifier, simplificationTolerance: 0, polygons: [...polygonsRaw, ...?_hoverGons], ), ), ), const Scalebar( textStyle: TextStyle(color: Colors.black, fontSize: 14), padding: EdgeInsets.only(right: 10, left: 10, bottom: 40), alignment: Alignment.bottomLeft, ), const Scalebar( textStyle: TextStyle(color: Colors.black, fontSize: 14), padding: EdgeInsets.only(right: 10, left: 10, bottom: 80), alignment: Alignment.bottomLeft, length: ScalebarLength.s, ), const Scalebar( textStyle: TextStyle(color: Colors.black, fontSize: 14), alignment: Alignment.bottomCenter, length: ScalebarLength.s, ), const Scalebar( textStyle: TextStyle(color: Colors.black, fontSize: 14), length: ScalebarLength.xxl, ), const Scalebar( textStyle: TextStyle(color: Colors.black, fontSize: 14), padding: EdgeInsets.only(right: 10, left: 10, top: 40), ), const Scalebar( textStyle: TextStyle(color: Colors.black, fontSize: 14), padding: EdgeInsets.only(right: 10, left: 10, top: 80), length: ScalebarLength.s, ), ], ), ), Positioned( top: pointY - pointSize / 2, left: _getPointX(context) - pointSize / 2, child: const IgnorePointer( child: Icon( Icons.center_focus_strong_outlined, size: pointSize, color: Color(0xFF0E0E0F), ), ), ), Positioned( top: pointY + pointSize / 2 + 6, left: 0, right: 0, child: IgnorePointer( child: Text('(${latLng?.latitude.toStringAsFixed(3)},${latLng?.longitude.toStringAsFixed(3)})', textAlign: TextAlign.center, style: const TextStyle( color: Color.fromARGB(255, 4, 67, 255), fontWeight: FontWeight.bold, fontSize: 16, ), ), ), ) ], ), ); } @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => updatePoint(context)); } void finalizepolygonsaving(){ setState(() => polygonsRaw.removeRange(polygonssaved,polygonsRaw.length)); _polygons = Map.fromEntries(polygonsRaw.map((e) => MapEntry(e.hitValue, e))); setState(() => polygonsRaw.add(buildPolygon(mypolygonpoints,( title:'Created at ${DateTime.now()}', subtitle: 'the subtitle test' )))) ; _polygons = Map.fromEntries(polygonsRaw.map((e) => MapEntry(e.hitValue, e))); print('Right clicked (Saving Polygon requested) with: ${mypolygonpoints.length} vertices --> NEW number of polygons =${polygonsRaw.length}'); polygonssaved++; } void _openTouchedGonsModal( String eventType, List<HitValue> tappedLines, LatLng coords, ) { // if((drawingfinished) && mypolygonpoints.isEmpty ) return; // added to avoid unrequired processing fees ( wait till polygon is saved or discarded ) showModalBottomSheet<void>( context: context, builder: (context) => Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('The Geofence(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 = tappedLines[index]; return ListTile( leading: index == 0 ? const Icon(Icons.vertical_align_top) : index == tappedLines.length - 1 ? const Icon(Icons.vertical_align_bottom) : const SizedBox.shrink(), title: Text(tappedLineData.title), subtitle: Text(tappedLineData.subtitle), dense: true, ); }, itemCount: tappedLines.length, ), ), const SizedBox(height: 8), Align( alignment: Alignment.bottomCenter, child: SizedBox( width: double.infinity, child: OutlinedButton( onPressed: () => Navigator.pop(context), child: const Text('Close'), ), ), ), ], ), ), ); } void updatePoint(BuildContext context) => setState(() => latLng = mapController.camera.pointToLatLng(Point(_getPointX(context), pointY))); double _getPointX(BuildContext context) => MediaQuery.sizeOf(context).width / 2;}