import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'osm_layer.dart'; import 'stat_charts.dart'; import '../../consts.dart'; import 'choropleth_map.dart'; import '../data_classes.dart'; import '../utils/export_data.dart'; import '../utils/format_month.dart'; /// MapContainerWidget is the main widget that contains the map with all /// its layers, polygons and markers. class MapContainerWidget extends StatefulWidget { final List<SubDiv> subdivisions; final List<Measurement> measurements; final Uint8List relation; final bool serverConnection; const MapContainerWidget({Key? key, required this.subdivisions, required this.measurements, required this.relation, required this.serverConnection, }) : super(key: key); @override _MapContainerWidgetState createState() => _MapContainerWidgetState(); } class _MapContainerWidgetState extends State<MapContainerWidget> { bool isMinimized = true; // Quick view box state tacker bool satLayer = false; // Satellite layer visibility state bool osmLayer = false; // OSM layer visibility state bool isSatTapped = false; // Satellite button tap state tracker bool isMapTapped = false; // OSM button tap state tracker bool showColorMeanings = false; // Additional color legend visibility Measurement? selectedMeasurement = selectedMeasurements[0]; // Initialise lastUpdate variable from persistent storage if server fetch fails Future<void> checkAndSetLastUpdate() async { if (lastUpdate == null) { final prefs = await SharedPreferences.getInstance(); final updateString = prefs.getString('lastUpdate'); if (updateString != null && updateString.isNotEmpty) { final updateData = DateTime.parse(updateString); setState(() { lastUpdate = updateData; }); } } } /// Tile selection handler void handleSelection(int index) { String indexString = index.toString(); setState(() { selectedSubDiv = widget.subdivisions[index]; for (Measurement measurement in widget.measurements) { for (SubDiv subdivision in measurement.subDivs) { if (subdivision.sub_div_id == indexString) { selectedMeasurement = measurement; break; } } } }); } // _buildLegendItem renders a colored circle and text to form a legend Widget _legendItem(Color color, String text) { return Row( children: [ Container( width: 20, height: 20, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), const SizedBox(width: 8), Text( text, style: const TextStyle( fontSize: 14, color: Colors.white, ), ), ], ); } /// Builds an additional legend to explain the colors Widget _buildLegend() { return Column( children: [ _legendItem(const Color(0xffff0000), "Very unsafe"), const SizedBox(height: 10), _legendItem(const Color(0xffff6a00), "Unsafe"), const SizedBox(height: 10), _legendItem(const Color(0xFFb1ff00), "Safe"), const SizedBox(height: 10), _legendItem(const Color(0xFF00d6ff), "Very safe"), const SizedBox(height: 10), ], ); } @override Widget build(BuildContext context) { // Initialise selectedMarker to first element in markerList selectedSubDiv ??= widget.measurements[0].subDivs[0]; checkAndSetLastUpdate(); const double contPadding = 30; // Container padding space return LayoutBuilder( builder: (context, constraints) { double screenWidth = constraints.maxWidth; double boxWidth = 0.86; double boxHeight = 1.4; return Column( children: [ const SizedBox(height: contPadding*1.5), ClipRRect( borderRadius: BorderRadius.circular(20), child: Stack( // Stack of quick view, map layer, satellite layer, and buttons children: [ SizedBox( // Colored box behind map width: screenWidth * boxWidth, height: screenWidth * boxHeight, child: Container( color: Colors.white12, ), ), SizedBox( // Color coded lake polygon width: screenWidth * boxWidth, height: screenWidth * boxHeight, child: Padding( padding: const EdgeInsets.all(15.0), // Padding around map child: ChoroplethMap( relation: widget.relation, measurements: widget.measurements, subdivisions: widget.subdivisions, onSelectionChanged: handleSelection, ), ), ), SizedBox( width: screenWidth * boxWidth, height: screenWidth * boxHeight, child: Visibility( visible: osmLayer, child: OSM(markerList: widget.measurements), ), ), Positioned( // Satellite button top: 10, right: 10, child: GestureDetector( onTap: () { setState(() { satLayer = !satLayer; // Toggle satellite layer state on press }); }, child: Container( padding: const EdgeInsets.all(8), decoration: satLayer ? const BoxDecoration( // Add decoration only when pressed shape: BoxShape.circle, color: Colors.grey, ) : null, child: const Icon( Icons.satellite_alt_outlined, color: Colors.white54, ), ), ), ), Positioned( // OSmap button top: 50, right: 10, child: GestureDetector( onTap: () { setState(() { osmLayer = !osmLayer; // Toggle satellite layer state on press }); }, child: Container( padding: const EdgeInsets.all(8), decoration: osmLayer ? const BoxDecoration( // Add decoration only when pressed shape: BoxShape.circle, color: Colors.grey, ) : null, child: const Icon( Icons.map, color: Colors.white54, ), ), ), ), Positioned( // Export button top: 90, right: 10, child: GestureDetector( onTap: () { showModalBottomSheet( context: context, builder: (BuildContext context) { return SizedBox( height: 200, width: screenWidth, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "Export ice data for $selectedLake", style: const TextStyle(fontSize: 20), ), const SizedBox(height: contPadding), ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.white24), ), child: const Text("Export JSON"), onPressed: () { // Close bottom sheet before displaying progress bar showProgressIndicator(context); }, ) ], ), ), ); }, ); }, child: Container( padding: const EdgeInsets.all(8), decoration: isSatTapped ? const BoxDecoration( shape: BoxShape.circle, color: Colors.blue, ) : null, child: const Icon( Icons.share, color: Colors.white54, ), ), ), ), Positioned( // Color info button top: 130, right: 10, child: GestureDetector( onTap: () { setState(() { showColorMeanings = !showColorMeanings; // Toggle satellite layer state on press }); }, child: Container( padding: const EdgeInsets.all(8), decoration: showColorMeanings ? const BoxDecoration( // Add decoration only when pressed shape: BoxShape.circle, color: Colors.grey, ) : null, child: const Icon( Icons.info, color: Colors.white54, ), ), ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 5), Text( // Display time of most recent server fetch 'Last updated at ${lastUpdate != null ? (lastUpdate?.day == DateTime.now().day && lastUpdate?.month == DateTime.now().month && lastUpdate?.year == DateTime.now().year ? '${lastUpdate?.hour}:${lastUpdate?.minute}' : '${lastUpdate?.day}.${formatMonth(lastUpdate!.month)} ${lastUpdate?.year}') : ''}', style: GoogleFonts.dmSans( fontSize: 14, color: Colors.white60, ), ), ], ), const SizedBox(height: contPadding), // Padding between containers Column( // Ice stats container crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: screenWidth * boxWidth, child: Align( alignment: Alignment.topLeft, child: Padding( padding: const EdgeInsets.only(top: 20, left: 30), // Custom padding child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Ice stats', style: titleStyle, ), const Divider(), const SizedBox(height: 10), Text( 'Tile ID: ${selectedSubDiv?.sub_div_id}', style: regTextStyle, ), const SizedBox(height: 20), Text( 'Measured at ', style: subHeadingStyle, ), Text( 'Date: ${(selectedMeasurement?.timeMeasured.day ?? '-')}/${(selectedMeasurement?.timeMeasured.month ?? '-')}/${(selectedMeasurement?.timeMeasured.year ?? '-')}', style: regTextStyle, ), Text( 'Time: ${selectedMeasurement?.timeMeasured.hour}:00', style: regTextStyle, ), const SizedBox(height: contPadding), Text( 'Center coordinate: (${selectedSubDiv?.center.latitude}, ${selectedSubDiv?.center.longitude})', style: regTextStyle, ), const SizedBox(height: contPadding/3), Text( 'Data certainty: ${selectedSubDiv?.accuracy}/4', style: regTextStyle, ), ], ), ), ), ), const SizedBox(height: contPadding*2.5), SizedBox( width: screenWidth * boxWidth * 1.2, child: const StatCharts(), ), const SizedBox(height: contPadding*4), ], ), ], ); }, ); } }