Newer
Older
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shared_preferences/shared_preferences.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<Measurement> measurements;
const MapContainerWidget({Key? key,
required this.measurements,
required this.serverConnection,
@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) {
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;
});
}
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// _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
SizedBox( // Colored box behind map
width: screenWidth * boxWidth,
height: screenWidth * boxHeight,
child: Container(
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,
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: 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);
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
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(),
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),
],
);
},