Files
2025-07-18 22:24:01 +02:00

670 lines
27 KiB
Dart

// 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<MainMapWidget> createState() => _MainMapWidgetState();
}
class _MainMapWidgetState extends State<MainMapWidget> {
final MapController _mapController = MapController();
final Map<String, String> _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<String, Widget> _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<Map<int, Coordinates>> 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<LatLng> 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<AuthProvider>();
return CardWidget(
height: 700,
child: Obx(() {
final List<Vineyard> vineyardList = authProvider.profile?.owners?.expand((owner) => owner.vineyards ?? <Vineyard>[]).cast<Vineyard>().toList() ?? [];
final List<Map<int, Coordinates>> areas = vineyardList
.map((vineyard) => vineyard.area)
.whereType<Map<int, Coordinates>>()
.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<String>(
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<LatLng> 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<MainMapWidget> createState() => _MainMapWidgetState();
// }
// class _MainMapWidgetState extends State<MainMapWidget> {
// // Le variabili di stato devono essere qui
// final MapController _mapController = MapController();
// final Map<String, String> _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<String, Widget> _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<Vineyard> _vineyardList = [];
// List<Map<int, Coordinates>> _areas = [];
// @override
// void initState() {
// super.initState();
// _vineyardList = Get.find<AuthProvider>().profile?.owners?.expand((owner) => owner.vineyards ?? <Vineyard>[]).cast<Vineyard>().toList() ?? [];
// _areas = _vineyardList
// .map((vineyard) => vineyard.area)
// .whereType<Map<int, Coordinates>>()
// .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<Map<int, Coordinates>> 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<LatLng> 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<String>(
// 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<int, Coordinates> in List<LatLng>
// final List<LatLng> 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<LatLng> 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<VignaMapWidget> createState() => _VignaMapWidgetState();
}
class _VignaMapWidgetState extends State<VignaMapWidget> {
final MapController _mapController = MapController();
String _selectedMapType = 'Standard'; // Default: OpenStreetMap Standard
final Map<String, String> _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<String, Widget> _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<String>(
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<String>(
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],
),
),
),
],
),
),
);
}
}