// widgets/vigna_map_widget.dart import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:get/get.dart'; import 'package:get/get_core/src/get_main.dart'; import 'package:latlong2/latlong.dart'; import 'package:u_vineyard/entities/coordinates.dart'; import 'package:u_vineyard/entities/vineyard.dart'; import 'package:u_vineyard/template/src/providers/auth.provider.dart'; import 'package:u_vineyard/template/src/widgets/card.widget.dart'; // Definizione del tipo di callback per i tap sulla mappa typedef MapTapCallback = void Function(TapPosition tapPosition, LatLng latLng); class MainMapWidget extends StatefulWidget { const MainMapWidget({super.key}); @override State createState() => _MainMapWidgetState(); } class _MainMapWidgetState extends State { final MapController _mapController = MapController(); final Map _mapUrlTemplates = { 'Standard': 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', 'Terreno (OpenTopoMap)': 'https://opentopomap.org/{z}/{x}/{y}.png', 'Satellite (ESRI)': 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', }; final Map _mapAttributions = { 'Standard': const Text('© OpenStreetMap contributors'), 'Terreno (OpenTopoMap)': const Text('Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | Kartendarstellung: © OpenTopoMap (CC-BY-SA)'), 'Satellite (ESRI)': const Text('Tiles © Esri — Source: Esri, i-cubed, USDA FSA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'), }; String? _selectedMapType = 'Standard'; double _currentZoom = 13.0; // Questo zoom verrà sovrascritto se ci sono aree // Variabile per tenere traccia se la mappa è già stata centrata una volta bool _mapCenteredInitially = false; @override void initState() { super.initState(); // Non inizializzare qui _vineyardList o _areas } void _zoomIn() { setState(() { _currentZoom += 1; _mapController.move(_mapController.camera.center, _currentZoom); }); } void _zoomOut() { setState(() { _currentZoom -= 1; _mapController.move(_mapController.camera.center, _currentZoom); }); } // Funzione per calcolare i limiti e centrare la mappa void _fitMapToPolygons(List> areas) { if (areas.isEmpty) { // Se non ci sono aree, centra sulla posizione predefinita _mapController.move(const LatLng(45.4642, 9.1900), 10.0); return; } // Raccogli tutti i punti da tutte le aree List allPoints = []; for (var areaMap in areas) { for (var coord in areaMap.values) { allPoints.add(LatLng(coord.latitude, coord.longitude)); } } if (allPoints.isEmpty) { // Se per qualche motivo non ci sono punti validi, torna al default _mapController.move(const LatLng(45.4642, 9.1900), 10.0); return; } // Calcola i limiti (min/max latitudine e longitudine) double? minLat, maxLat, minLon, maxLon; for (var point in allPoints) { if (minLat == null || point.latitude < minLat) minLat = point.latitude; if (maxLat == null || point.latitude > maxLat) maxLat = point.latitude; if (minLon == null || point.longitude < minLon) minLon = point.longitude; if (maxLon == null || point.longitude > maxLon) maxLon = point.longitude; } if (minLat != null && maxLat != null && minLon != null && maxLon != null) { // Crea un LatLngBounds final LatLngBounds bounds = LatLngBounds( LatLng(minLat, minLon), LatLng(maxLat, maxLon), ); // Adatta la camera per mostrare tutti i punti _mapController.fitCamera( CameraFit.bounds( bounds: bounds, padding: const EdgeInsets.all(50.0), // Aggiungi un po' di padding ), ); // Aggiorna lo zoom corrente dopo il fit _currentZoom = _mapController.camera.zoom; } } @override Widget build(BuildContext context) { final AuthProvider authProvider = Get.find(); return CardWidget( height: 700, child: Obx(() { final List vineyardList = authProvider.profile?.owners?.expand((owner) => owner.vineyards ?? []).cast().toList() ?? []; final List> areas = vineyardList .map((vineyard) => vineyard.area) .whereType>() .toList(); // Centra la mappa solo una volta o quando le aree cambiano significativamente // Per semplicità, la centreremo solo la prima volta che le aree vengono caricate // o se il flag _mapCenteredInitially è falso e ci sono aree. if (areas.isNotEmpty && !_mapCenteredInitially) { WidgetsBinding.instance.addPostFrameCallback((_) { _fitMapToPolygons(areas); _mapCenteredInitially = true; // Imposta il flag a true dopo il primo centraggio }); } else if (areas.isEmpty && _mapCenteredInitially) { // Se le aree diventano vuote dopo essere state caricate, resetta il flag _mapCenteredInitially = false; } // Logica per initialCenter della mappa (usata solo alla prima costruzione) // Dopo il primo render, _mapController.move gestirà il centro e lo zoom LatLng initialMapCenter; if (vineyardList.isNotEmpty && vineyardList.first.coordinates != null) { initialMapCenter = LatLng(vineyardList.first.coordinates!.latitude, vineyardList.first.coordinates!.longitude); } else { initialMapCenter = const LatLng(45.4642, 9.1900); // Centro di default (Milano) } return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Vigne', style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.green[800]), ), DropdownButton( value: _selectedMapType, items: const [ DropdownMenuItem(value: 'Standard', child: Text('Standard')), DropdownMenuItem(value: 'Terreno (OpenTopoMap)', child: Text('Terreno')), DropdownMenuItem(value: 'Satellite (ESRI)', child: Text('Satellite')), ], onChanged: (String? newValue) { setState(() { _selectedMapType = newValue; }); }, ), ], ), const SizedBox(height: 16), Expanded( child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(8.0), ), child: FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: initialMapCenter, initialZoom: _currentZoom, // Usa _currentZoom che può essere aggiornato da _fitMapToPolygons ), children: [ TileLayer( urlTemplate: _mapUrlTemplates[_selectedMapType]!, userAgentPackageName: 'com.example.vigna_app', ), RichAttributionWidget( attributions: [ TextSourceAttribution( (_mapAttributions[_selectedMapType]! as Text).data!, onTap: () => print('Attribution tapped: ${(_mapAttributions[_selectedMapType]! as Text).data}'), ), ], ), MarkerLayer( markers: vineyardList.map((vineyard) { return Marker( width: 40.0, height: 40.0, point: vineyard.coordinates != null ? LatLng(vineyard.coordinates!.latitude, vineyard.coordinates!.longitude) : const LatLng(0, 0), child: Tooltip( message: vineyard.name, child: const Icon(Icons.grass, color: Colors.green, size: 30.0), ), ); }).toList(), ), Positioned( top: 40, right: 10, child: Column( children: [ FloatingActionButton( heroTag: 'zoomIn', mini: true, onPressed: _zoomIn, child: const Icon(Icons.add), ), const SizedBox(height: 8), FloatingActionButton( heroTag: 'zoomOut', mini: true, onPressed: _zoomOut, child: const Icon(Icons.remove), ), ], ), ), if (areas.isNotEmpty) PolygonLayer( polygons: areas.expand((areaMap) { final List polygonPoints = areaMap.values .map((coord) => LatLng(coord.latitude, coord.longitude)) .toList(); if (polygonPoints.isNotEmpty && polygonPoints.first != polygonPoints.last) { polygonPoints.add(polygonPoints.first); } return [ if (polygonPoints.length >= 3) Polygon( points: polygonPoints, color: Colors.blue.withOpacity(0.3), borderStrokeWidth: 2, borderColor: Colors.blue[900]!, ), ]; }).toList(), ), ], ), ), ), ], ); }), ); } } // class MainMapWidget extends StatefulWidget { // const MainMapWidget({super.key}); // @override // State createState() => _MainMapWidgetState(); // } // class _MainMapWidgetState extends State { // // Le variabili di stato devono essere qui // final MapController _mapController = MapController(); // final Map _mapUrlTemplates = { // 'Standard': 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', // 'Terreno (OpenTopoMap)': 'https://opentopomap.org/{z}/{x}/{y}.png', // 'Satellite (ESRI)': 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', // }; // final Map _mapAttributions = { // 'Standard': const Text('© OpenStreetMap contributors'), // 'Terreno (OpenTopoMap)': const Text('Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | Kartendarstellung: © OpenTopoMap (CC-BY-SA)'), // 'Satellite (ESRI)': const Text('Tiles © Esri — Source: Esri, i-cubed, USDA FSA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'), // }; // String? _selectedMapType = 'Standard'; // Tipo di mappa selezionato // double _currentZoom = 13.0; // List _vineyardList = []; // List> _areas = []; // @override // void initState() { // super.initState(); // _vineyardList = Get.find().profile?.owners?.expand((owner) => owner.vineyards ?? []).cast().toList() ?? []; // _areas = _vineyardList // .map((vineyard) => vineyard.area) // .whereType>() // .toList(); // } // void _zoomIn() { // setState(() { // _currentZoom += 1; // _mapController.move(_mapController.camera.center, _currentZoom); // }); // } // void _zoomOut() { // setState(() { // _currentZoom -= 1; // _mapController.move(_mapController.camera.center, _currentZoom); // }); // void _fitMapToPolygons(List> areas) { // if (areas.isEmpty) { // // Se non ci sono aree, centra sulla posizione predefinita // _mapController.move(const LatLng(45.4642, 9.1900), 10.0); // return; // } // // Raccogli tutti i punti da tutte le aree // List allPoints = []; // for (var areaMap in areas) { // for (var coord in areaMap.values) { // allPoints.add(LatLng(coord.latitude, coord.longitude)); // } // } // if (allPoints.isEmpty) { // // Se per qualche motivo non ci sono punti validi, torna al default // _mapController.move(const LatLng(45.4642, 9.1900), 10.0); // return; // } // // Calcola i limiti (min/max latitudine e longitudine) // double? minLat, maxLat, minLon, maxLon; // for (var point in allPoints) { // if (minLat == null || point.latitude < minLat) minLat = point.latitude; // if (maxLat == null || point.latitude > maxLat) maxLat = point.latitude; // if (minLon == null || point.longitude < minLon) minLon = point.longitude; // if (maxLon == null || point.longitude > maxLon) maxLon = point.longitude; // } // if (minLat != null && maxLat != null && minLon != null && maxLon != null) { // // Crea un LatLngBounds // final LatLngBounds bounds = LatLngBounds( // LatLng(minLat, minLon), // LatLng(maxLat, maxLon), // ); // // Adatta la camera per mostrare tutti i punti // _mapController.fitCamera( // CameraFit.bounds( // bounds: bounds, // padding: const EdgeInsets.all(50.0), // Aggiungi un po' di padding // ), // ); // // Aggiorna lo zoom corrente dopo il fit // _currentZoom = _mapController.camera.zoom; // } // } // } // @override // Widget build(BuildContext context) { // // Puoi usare Obx per reagire ai cambiamenti in AuthProvider se il profilo può cambiare dinamicamente // print( _areas); // return CardWidget( // height: 700, // child: Column( // children: [ // Row( // mainAxisAlignment: MainAxisAlignment.spaceBetween, // children: [ // Text( // 'Vigne', // style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.green[800]), // ), // DropdownButton( // value: _selectedMapType, // items: [ // DropdownMenuItem(value: 'Standard', child: const Text('Standard')), // DropdownMenuItem(value: 'Terreno (OpenTopoMap)', child: const Text('Terreno')), // DropdownMenuItem(value: 'Satellite (ESRI)', child: const Text('Satellite')), // ], // onChanged: (String? newValue) { // // Chiamata a setState() per aggiornare l'UI // setState(() { // _selectedMapType = newValue; // newValue può essere null, ma il DropdownButton lo gestisce // }); // }, // ), // ], // ), // const SizedBox(height: 16), // Expanded( // child: Container( // // Rimuovi height: 500 se Expanded è sufficiente per il layout // decoration: BoxDecoration( // border: Border.all(color: Colors.grey), // borderRadius: BorderRadius.circular(8.0), // ), // child: FlutterMap( // mapController: _mapController, // options: MapOptions( // initialCenter:initialMapCenter, // Centro iniziale (es. Milano) // initialZoom: 10.0, // ), // children: [ // TileLayer( // // Usa l'operatore '!' perché _selectedMapType è garantito non essere null qui // // dato che ha un valore iniziale e il DropdownButton non permette null. // urlTemplate: _mapUrlTemplates[_selectedMapType]!, // userAgentPackageName: 'com.example.vigna_app', // ), // RichAttributionWidget( // attributions: [ // TextSourceAttribution( // (_mapAttributions[_selectedMapType]! as Text).data!, // onTap: () => print('Attribution tapped: ${(_mapAttributions[_selectedMapType]! as Text).data}'), // ), // ], // ), // // MarkerLayer per i vigneti // MarkerLayer( // markers: _vineyardList.map((vineyard) { // return Marker( // width: 40.0, // height: 40.0, // point: vineyard.coordinates != null // ? LatLng(vineyard.coordinates!.latitude, vineyard.coordinates!.longitude) // : const LatLng(0, 0), // child: Tooltip( // Aggiunto Tooltip per mostrare il nome del vigneto al tap // message: vineyard.name, // child: const Icon(Icons.grass, color: Colors.green, size: 30.0), // ), // ); // }).toList(), // ), // Positioned( // top: 40, // right: 10, // child: Column( // children: [ // FloatingActionButton( // heroTag: 'zoomIn', // mini: true, // onPressed: _zoomIn, // child: Icon(Icons.add), // ), // SizedBox(height: 8), // FloatingActionButton( // heroTag: 'zoomOut', // mini: true, // onPressed: _zoomOut, // child: Icon(Icons.remove), // ), // ], // ), // ), // if (_areas.isNotEmpty) // Mostra il layer solo se ci sono aree da disegnare // PolygonLayer( // polygons: _areas.expand((areaMap) { // // Converte la Map in List // final List polygonPoints = areaMap.values // .map((coord) => LatLng(coord.latitude, coord.longitude)) // .toList(); // // FlutterMap PolygonLayer richiede che il poligono sia chiuso. // // Ovvero, il primo e l'ultimo punto devono essere uguali. // // Aggiungiamo il primo punto alla fine se non è già così. // if (polygonPoints.isNotEmpty && polygonPoints.first != polygonPoints.last) { // polygonPoints.add(polygonPoints.first); // } // // Restituisci una lista di Polygon, uno per ogni area // return [ // if (polygonPoints.length >= 3) // Un poligono valido ha almeno 3 punti // Polygon( // points: polygonPoints, // color: Colors.blue.withOpacity(0.3), // Colore di riempimento dell'area // borderStrokeWidth: 2, // Spessore del bordo // borderColor: Colors.blue[900]!, // Colore del bordo // ), // ]; // }).toList(), // ), // ], // ), // ), // ), // // Questa SizedBox era posizionata male, l'ho rimossa o spostata se necessaria altrove. // // Se volevi spazio alla destra della mappa, dovrebbe essere nel Row con Expanded. // // const SizedBox(width: 16), // Rimosso o riposizionato se necessario // ], // ), // ); // } // } class VignaMapWidget extends StatefulWidget { final List polygonPoints; final MapTapCallback onMapTap; final VoidCallback onClearPolygon; final double calculatedArea; const VignaMapWidget({ super.key, required this.polygonPoints, required this.onMapTap, required this.onClearPolygon, required this.calculatedArea, }); @override State createState() => _VignaMapWidgetState(); } class _VignaMapWidgetState extends State { final MapController _mapController = MapController(); String _selectedMapType = 'Standard'; // Default: OpenStreetMap Standard final Map _mapUrlTemplates = { 'Standard': 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', 'Terreno (OpenTopoMap)': 'https://opentopomap.org/{z}/{x}/{y}.png', 'Satellite (ESRI)': 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', }; final Map _mapAttributions = { 'Standard': const Text('© OpenStreetMap contributors'), 'Terreno (OpenTopoMap)': const Text('Kartendaten: © OpenStreetMap-Mitwirkende, SRTM | Kartendarstellung: © OpenTopoMap (CC-BY-SA)'), 'Satellite (ESRI)': const Text('Tiles © Esri — Source: Esri, i-cubed, USDA FSA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'), }; @override Widget build(BuildContext context) { return CardWidget( height: 700, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Mappa Vigna (Disegna Poligono)', style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: Colors.green[800]), ), // Dropdown per cambiare il tipo di mappa DropdownButtonHideUnderline( child: DropdownButton( value: _selectedMapType, dropdownColor: Colors.green[700], style: const TextStyle(color: Colors.white, fontSize: 16), icon: const Icon(Icons.map, color: Colors.green), // Icona qui, non nell'AppBar items: _mapUrlTemplates.keys.map((String value) { return DropdownMenuItem( value: value, child: Text(value), ); }).toList(), onChanged: (String? newValue) { setState(() { _selectedMapType = newValue!; }); }, ), ), ], ), const SizedBox(height: 32), Container( height: 300, decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(8.0), ), child: FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: const LatLng(45.4642, 9.1900), // Centro iniziale (es. Milano) initialZoom: 10.0, onTap: widget.onMapTap, // Usa la callback passata ), children: [ TileLayer( urlTemplate: _mapUrlTemplates[_selectedMapType]!, userAgentPackageName: 'com.example.vigna_app', ), RichAttributionWidget( attributions: [ TextSourceAttribution( (_mapAttributions[_selectedMapType]! as Text).data!, onTap: () => print('Attribution tapped: ${(_mapAttributions[_selectedMapType]! as Text).data}'), ), ], ), if (widget.polygonPoints.isNotEmpty) PolygonLayer( polygons: [ Polygon( points: widget.polygonPoints, color: Colors.green.withOpacity(0.5), borderStrokeWidth: 3, borderColor: Colors.green[900]!, ), ], ), if (widget.polygonPoints.isNotEmpty) MarkerLayer( markers: widget.polygonPoints.map((point) { return Marker( width: 40.0, height: 40.0, point: point, child: const Icon( Icons.grass, color: Colors.red, size: 30.0, ), ); }).toList(), ), ], ), ), const SizedBox(height: 10), Align( alignment: Alignment.centerRight, child: ElevatedButton.icon( onPressed: widget.onClearPolygon, // Usa la callback passata icon: const Icon(Icons.clear), label: const Text('Resetta Poligono'), style: ElevatedButton.styleFrom( backgroundColor: Colors.red[400], foregroundColor: Colors.white, ), ), ), if (widget.calculatedArea > 0) Padding( padding: const EdgeInsets.only(top: 10.0), child: Text( 'Area Calcolata: ${widget.calculatedArea.toStringAsFixed(2)} ettari', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.green[800], ), ), ), ], ), ), ); } }