Skip to content
Snippets Groups Projects
Commit 41a7f9c6 authored by Sara Savanovic Djordjevic's avatar Sara Savanovic Djordjevic
Browse files

Merge branch 'clhp_map' into 'main'

Clhp map

See merge request !12
parents 92787c8b e715e4dd
No related branches found
No related tags found
1 merge request!12Clhp map
Showing
with 309 additions and 222 deletions
# PROG2900 # PROG2900
## Dependencies ## Dependencies
### Python
## Endpoints
```
\
\update_map
\get_relation
\add_new_lake?lake=name
```
### Server
To run the server... To run the server...
### Dart & Flutter ### Application
### Database ### Database
...@@ -13,9 +22,9 @@ Precompiled binaries can be found on https://www.sqlite.org/download.html. Extra ...@@ -13,9 +22,9 @@ Precompiled binaries can be found on https://www.sqlite.org/download.html. Extra
binary in a folder and note its path. Add the path to your system environment variables. Now you can binary in a folder and note its path. Add the path to your system environment variables. Now you can
manage the SQLite database. manage the SQLite database.
## Adding new maps ## Adding new lakes
The current server only contains the data for a single lake, Mjøsa. To add more lakes To add a new lake to the system, go to https://overpass-turbo.eu/.
go to https://overpass-turbo.eu/. Once you have navigated to Overpass API, enter Once you have navigated to Overpass API, enter
the Overpass query below in the left field, but swap 'lakeName' out the Overpass query below in the left field, but swap 'lakeName' out
with the name of the lake you want to add. Once the query has been adjusted, with the name of the lake you want to add. Once the query has been adjusted,
press the 'Run' button. press the 'Run' button.
...@@ -29,14 +38,25 @@ press the 'Run' button. ...@@ -29,14 +38,25 @@ press the 'Run' button.
(._;>;); (._;>;);
out body; out body;
``` ```
![img.png](images/overpass-query.png)
![img.png](images/geojson-export.png)
If a text box saying "This query returned quite a lot of data (approx. x MB). Your browser may have a hard time trying to render this. Do you really want to continue? If a text box saying "This query returned quite a lot of data (approx. x MB). Your browser may have a hard time trying to render this. Do you really want to continue?
" appears, press 'continue anyway'. Double check that you have " appears, press 'continue anyway'. Double check that you have
the correct lake, then press 'Export'. In the 'Export' menu, download the shape data as the correct lake, then press 'Export'. In the 'Export' menu, download the shape data as
GeoJson. Once downloaded, name the file the *lakeName.json, and move the file into GeoJson. Once downloaded, name the file the *lakeName.json, and move the file into
IceMap/server/lake_relations. Once you have added the file, run map division... IceMap/server/lake_relations. Once you have added the file, run map division...
## Endpoints ![img.png](images/geojson-file.png)
The result will be two new files named lakeName_centers.txt and lakeName_div.json. The original
lakeName.geojson file should also remain in the system. Additionally, the file named all_lake_names.json
should be updated to contain the newly added lake name.
![img.png](images/resulting-files.png)
## Bugs ## Known bugs
## Developers ## Developers
...@@ -8,15 +8,16 @@ import 'package:google_fonts/google_fonts.dart'; ...@@ -8,15 +8,16 @@ import 'package:google_fonts/google_fonts.dart';
const String port = "8443"; const String port = "8443";
const String serverURI = "https://127.0.0.1:$port/"; const String serverURI = "https://127.0.0.1:$port/";
const String mapEndpoint = "update_map"; const String mapEndpoint = "update_map";
const int fetchInterval = 60; // Fetch marker data every n minutes
// Map variables // Map variables
String selectedLake = 'Mjøsa'; // Init to Mjøsa, NB should be initialised to last selected lake String selectedLake = 'Mjøsa'; // Init to Mjøsa, NB should be initialised to last selected lake
Uint8List selectedRelation = Uint8List(0); Uint8List selectedRelation = Uint8List(0);
List<Measurement> selectedMarkerList = []; List<Measurement> selectedMarkerList = [];
LatLng mapCenter = LatLng(60.8000, 10.8471); SubDiv? selectedSubDiv;
LatLng mapCenter = LatLng(60.8000, 10.8471); // NB may not be necessary
DateTime ?lastUpdate; // Last time data was fetched from server DateTime ?lastUpdate; // Last time data was fetched from server
List<String> lakeSearchOptions = []; // Init empty List<String> lakeSearchOptions = [];
bool internetConnection = true; bool internetConnection = true;
// Font settings // Font settings
...@@ -32,14 +33,6 @@ final titleStyle = GoogleFonts.chakraPetch( ...@@ -32,14 +33,6 @@ final titleStyle = GoogleFonts.chakraPetch(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
); );
final regTextStyle = GoogleFonts.chakraPetch(fontSize: 16, color: textColor); final regTextStyle = GoogleFonts.chakraPetch(fontSize: 16, color: textColor);
final regTextStyleBig = GoogleFonts.chakraPetch(fontSize: 18, color: textColor); final regTextStyleBig = GoogleFonts.chakraPetch(fontSize: 20, color: textColor);
final chartTextStyle = GoogleFonts.chakraPetch(fontSize: 12, color: textColor); final chartTextStyle = GoogleFonts.chakraPetch(fontSize: 12, color: textColor);
final subHeadingStyle = GoogleFonts.chakraPetch(fontSize: 18, color: textColor, fontWeight: FontWeight.bold); final subHeadingStyle = GoogleFonts.chakraPetch(fontSize: 18, color: textColor, fontWeight: FontWeight.bold);
// Colors
const darkBlue = Color(0xFF015E8F);
const teal = Color(0xFF00B4D8);
const darkestBlue = Color(0xFF03045E);
const lightBlue = Color(0xFFCAF0F8);
const superLightBlue = Color(0xFFCAF0F8);
const barBlue = Color(0xFF0077B6);
\ No newline at end of file
...@@ -39,7 +39,7 @@ class SubDiv { ...@@ -39,7 +39,7 @@ class SubDiv {
LatLng center; LatLng center;
double accuracy; double accuracy;
Color color; Color color;
Color savedColor; List<IceStats> iceStats;
SubDiv({ SubDiv({
required this.sub_div_id, required this.sub_div_id,
...@@ -49,7 +49,7 @@ class SubDiv { ...@@ -49,7 +49,7 @@ class SubDiv {
required this.center, required this.center,
required this.accuracy, required this.accuracy,
required this.color, required this.color,
required this.savedColor required this.iceStats,
}); });
factory SubDiv.fromJson(Map<String, dynamic> json) { factory SubDiv.fromJson(Map<String, dynamic> json) {
...@@ -62,7 +62,57 @@ class SubDiv { ...@@ -62,7 +62,57 @@ class SubDiv {
accuracy: json['Accuracy'], accuracy: json['Accuracy'],
// Set grey as default color // Set grey as default color
color: json['Color'] != null ? Color(json['Color']) : Colors.grey, color: json['Color'] != null ? Color(json['Color']) : Colors.grey,
savedColor: json['Color'] != null ? Color(json['Color']) : Colors.grey, iceStats: (json['IceStats'] as List<dynamic>?)
?.map((data) => IceStats.fromJson(data))
.toList() ?? [],
);
}
}
class IceStats {
DateTime dateTime;
double slushIce;
double blackIce;
double totalIce;
double snowDepth;
double totalSnow;
double cloudCover;
double temperature;
IceStats({
required this.dateTime,
required this.slushIce,
required this.blackIce,
required this.totalIce,
required this.snowDepth,
required this.totalSnow,
required this.cloudCover,
required this.temperature,
});
factory IceStats.fromJson(Map<String, dynamic>? json) {
if (json == null) { // Return empty json
return IceStats(
dateTime: DateTime.now(),
slushIce: 0.0,
blackIce: 0.0,
totalIce: 0.0,
snowDepth: 0.0,
totalSnow: 0.0,
cloudCover: 0.0,
temperature: 0.0,
);
}
return IceStats(
dateTime: DateTime.parse(json['Date']),
slushIce: json['Slush ice (m)'] != null ? json['Slush ice (m)'].toDouble() : 0.0,
blackIce: json['Black ice (m)'] != null ? json['Black ice (m)'].toDouble() : 0.0,
totalIce: json['Total ice (m)'] != null ? json['Total ice (m)'].toDouble() : 0.0,
snowDepth: json['Snow depth (m)'] != null ? json['Snow depth (m)'].toDouble() : 0.0,
totalSnow: json['Total snow (m)'] != null ? json['Total snow (m)'].toDouble() : 0.0,
cloudCover: json['Cloud cover'] != null ? json['Cloud cover'].toDouble() : 0.0,
temperature: json['Temperature (t)'] != null ? json['Temperature (t)'].toDouble() : 0.0,
); );
} }
} }
......
import 'dart:async'; import 'dart:async';
import 'package:app/server_requests/fetch_relation.dart'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:app/server_requests/init_state.dart';
import 'package:liquid_pull_to_refresh/liquid_pull_to_refresh.dart';
import '../consts.dart'; import '../consts.dart';
import '../data_classes.dart';
import '../widgets/main_layout.dart'; import '../widgets/main_layout.dart';
import '../utils/custom_search_delegate.dart'; import '../utils/custom_search_delegate.dart';
import '../server_requests/fetch_relation.dart';
import '../server_requests/fetch_markers.dart';
class DefaultPage extends StatefulWidget { class DefaultPage extends StatefulWidget {
const DefaultPage({Key? key}) : super(key: key); const DefaultPage({Key? key}) : super(key: key);
...@@ -20,6 +19,7 @@ class _DefaultPageState extends State<DefaultPage> { ...@@ -20,6 +19,7 @@ class _DefaultPageState extends State<DefaultPage> {
late Timer _timer; late Timer _timer;
bool serverConnection = true; bool serverConnection = true;
bool dialogShown = false; bool dialogShown = false;
final backgroundColor = Colors.black87;
@override @override
void dispose() { void dispose() {
...@@ -27,37 +27,17 @@ class _DefaultPageState extends State<DefaultPage> { ...@@ -27,37 +27,17 @@ class _DefaultPageState extends State<DefaultPage> {
super.dispose(); super.dispose();
} }
/// Display message to user Future<void> _handleRefresh() async {
void showConnectionMessage() { return await initialiseState(false);
showDialog(
context: context,
builder: (context) => AlertDialog(
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Ok"),
)
],
title: const Center(
child: Text("No server connection")
),
contentPadding: const EdgeInsets.all(10.0),
content: const Text(
"The app may display outdated information. Use with caution!",
textAlign: TextAlign.center, // Align text center
),
),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
home: Scaffold( home: Scaffold(
backgroundColor: backgroundColor,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.black87, backgroundColor: backgroundColor,
leading: IconButton( leading: IconButton(
icon: const Icon( icon: const Icon(
Icons.menu, Icons.menu,
...@@ -81,36 +61,38 @@ class _DefaultPageState extends State<DefaultPage> { ...@@ -81,36 +61,38 @@ class _DefaultPageState extends State<DefaultPage> {
showSearch( // Fetch new relation and measurements on search showSearch( // Fetch new relation and measurements on search
context: context, context: context,
delegate: CustomSearchDelegate((String result) { delegate: CustomSearchDelegate((String result) {
setState(() async { // Make request only if the selected lake is different from the current selected lake
selectedRelation = await fetchRelation(); if (result != selectedLake) {
selectedMarkerList = await fetchMeasurements().then((fetchResult) { setState(() {
List<Measurement> measurements = fetchResult.measurements; print("SetState called!");
serverConnection = fetchResult.connected; selectedLake = result;
// Return the measurements
return measurements;
}).catchError((error) {
serverConnection = false;
throw Exception("Failed to fetch measurements: $error");
}); });
selectedLake = result; initialiseState(false);
}); }
}), }),
); );
}, },
), ),
], ],
), ),
body: Container( // Return container with list view and background color body: LiquidPullToRefresh(
color: const Color(0xff151515), color: backgroundColor,
child: ListView( height: 100,
children: [ backgroundColor: Colors.grey[600],
MapContainerWidget( onRefresh: _handleRefresh,
markerList: selectedMarkerList, animSpeedFactor: 3,
relation: selectedRelation, showChildOpacityTransition: false,
serverConnection: serverConnection, child: Container( // Return main container with map and stats widget
), color: const Color(0xff151515),
], child: ListView(
children: [
MapContainerWidget(
measurements: selectedMarkerList,
relation: selectedRelation,
serverConnection: serverConnection,
),
],
),
), ),
), ),
), ),
......
...@@ -25,7 +25,7 @@ class _LoadingPageState extends State<LoadingPage> ...@@ -25,7 +25,7 @@ class _LoadingPageState extends State<LoadingPage>
} }
Future<void> _navigateToDefaultPage() async { Future<void> _navigateToDefaultPage() async {
await initialiseState(); await initialiseState(true);
// Navigate to the default page once state is initialised // Navigate to the default page once state is initialised
Navigator.of(context).pushReplacement(MaterialPageRoute( Navigator.of(context).pushReplacement(MaterialPageRoute(
......
...@@ -23,9 +23,9 @@ Future<FetchResult> fetchMeasurements() async { ...@@ -23,9 +23,9 @@ Future<FetchResult> fetchMeasurements() async {
(X509Certificate cert, String host, int port) => true; (X509Certificate cert, String host, int port) => true;
// Request markers from server // Request markers from server
var parameterValue = 'Mjosa'; // NB temp hardcoded, should use selectedLake directly in url param
var request = await client.getUrl(Uri.parse('$serverURI$mapEndpoint?lake=' var request = await client.getUrl(Uri.parse('$serverURI$mapEndpoint?lake='
'${Uri.encodeComponent(parameterValue)}')); '${Uri.encodeFull(selectedLake)}'));
var response = await request.close(); // Close response body at end of function var response = await request.close(); // Close response body at end of function
// Parse body to JSON if request is ok // Parse body to JSON if request is ok
...@@ -55,14 +55,16 @@ Future<FetchResult> fetchMeasurements() async { ...@@ -55,14 +55,16 @@ Future<FetchResult> fetchMeasurements() async {
} }
} }
} }
return loadSavedData(); return loadMeasurements();
} catch (e) { } catch (e) {
return loadSavedData(); print("Error in fetching measurements from server: $e");
return loadMeasurements();
} }
} }
Future<FetchResult> loadSavedData() async { Future<FetchResult> loadMeasurements() async {
try { try {
print("Loading measurements from file");
// Get latest saved data from file if the server does not respond // Get latest saved data from file if the server does not respond
Directory appDocumentsDirectory = await getApplicationDocumentsDirectory(); Directory appDocumentsDirectory = await getApplicationDocumentsDirectory();
String filePath = '${appDocumentsDirectory.path}/last_data.json'; String filePath = '${appDocumentsDirectory.path}/last_data.json';
...@@ -70,6 +72,9 @@ Future<FetchResult> loadSavedData() async { ...@@ -70,6 +72,9 @@ Future<FetchResult> loadSavedData() async {
// Read file contents // Read file contents
File file = File(filePath); File file = File(filePath);
if (await file.exists()) { if (await file.exists()) {
print('Reading marker data from file');
String contents = await file.readAsString(); String contents = await file.readAsString();
List<dynamic> jsonData = json.decode(contents); // Parse JSON string from file List<dynamic> jsonData = json.decode(contents); // Parse JSON string from file
List<Measurement> measurements = jsonData.map((data) => Measurement.fromJson(data)).toList(); List<Measurement> measurements = jsonData.map((data) => Measurement.fromJson(data)).toList();
......
...@@ -15,10 +15,8 @@ Future<Uint8List> fetchRelation() async { ...@@ -15,10 +15,8 @@ Future<Uint8List> fetchRelation() async {
(X509Certificate cert, String host, int port) => true; (X509Certificate cert, String host, int port) => true;
// Execute request to to get_relation endpoint // Execute request to to get_relation endpoint
var parameterValue = 'Mjosa'; // NB temp hardcoded, should use selectedLake directly in url param
//var request = await client.getUrl(Uri.parse('${serverURI}get_relation'));
var request = await client.getUrl(Uri.parse('${serverURI}get_relation?lake=' var request = await client.getUrl(Uri.parse('${serverURI}get_relation?lake='
'${Uri.encodeComponent(parameterValue)}')); '${Uri.encodeFull(selectedLake)}'));
var response = await request.close(); // Close response body at end of function var response = await request.close(); // Close response body at end of function
...@@ -39,15 +37,17 @@ Future<Uint8List> fetchRelation() async { ...@@ -39,15 +37,17 @@ Future<Uint8List> fetchRelation() async {
return Uint8List.fromList(utf8.encode(responseBody)); return Uint8List.fromList(utf8.encode(responseBody));
} }
} }
return loadSavedRelation(); return loadRelation();
} catch (e) { } catch (e) {
return loadSavedRelation(); print("Error in fetching relation from server: $e");
return loadRelation();
} }
} }
/// Load last saved relation data form last_relation.json /// Load last saved relation data form last_relation.json
Future<Uint8List> loadSavedRelation() async { Future<Uint8List> loadRelation() async {
try { try {
print("Loading relation from file");
// Get latest saved relation from file if the server does not respond // Get latest saved relation from file if the server does not respond
Directory appDocumentsDirectory = await getApplicationDocumentsDirectory(); Directory appDocumentsDirectory = await getApplicationDocumentsDirectory();
String filePath = '${appDocumentsDirectory.path}/last_relation.json'; String filePath = '${appDocumentsDirectory.path}/last_relation.json';
......
...@@ -13,7 +13,7 @@ import '../server_requests/fetch_relation.dart'; ...@@ -13,7 +13,7 @@ import '../server_requests/fetch_relation.dart';
/// initialiseState makes three requests to the server, one requesting /// initialiseState makes three requests to the server, one requesting
/// measurements for the selected relation, the other requesting the relation, /// measurements for the selected relation, the other requesting the relation,
/// and the last requesting the list of all system lakes /// and the last requesting the list of all system lakes
Future<void> initialiseState() async { Future<void> initialiseState(bool fetchSearchOptions) async {
bool serverConnection = true; bool serverConnection = true;
late Future<List<Measurement>> markerListFuture; late Future<List<Measurement>> markerListFuture;
...@@ -21,13 +21,12 @@ Future<void> initialiseState() async { ...@@ -21,13 +21,12 @@ Future<void> initialiseState() async {
try { try {
if (!internetConnection) { // Read data from files if no internet connection if (!internetConnection) { // Read data from files if no internet connection
selectedRelation = await loadSavedRelation(); selectedRelation = await loadRelation();
FetchResult fetchResult = await loadSavedData(); FetchResult fetchResult = await loadMeasurements();
List<Measurement> measurements = fetchResult.measurements; List<Measurement> measurements = fetchResult.measurements;
selectedMarkerList = measurements; selectedMarkerList = measurements;
lakeSearchOptions = ["Mjøsa"];
} else { // Try to fetch measurement data from server } else { // Try to fetch measurement data from server
markerListFuture = fetchMeasurements().then((fetchResult) { markerListFuture = fetchMeasurements().then((fetchResult) {
List<Measurement> measurements = fetchResult.measurements; List<Measurement> measurements = fetchResult.measurements;
...@@ -44,10 +43,12 @@ Future<void> initialiseState() async { ...@@ -44,10 +43,12 @@ Future<void> initialiseState() async {
if (serverConnection) { if (serverConnection) {
relationFuture = fetchRelation(); relationFuture = fetchRelation();
} else { // Read last saved data } else { // Read last saved data
relationFuture = loadSavedRelation(); relationFuture = loadRelation();
} }
initSearchOptions(); if (fetchSearchOptions) {
initSearchOptions();
}
//selectedRelation = await relationFuture; //selectedRelation = await relationFuture;
selectedRelation = await relationFuture; // NB update once fixed selectedRelation = await relationFuture; // NB update once fixed
...@@ -84,6 +85,6 @@ Future<void> initSearchOptions() async { ...@@ -84,6 +85,6 @@ Future<void> initSearchOptions() async {
} }
} }
} catch (e) { } catch (e) {
lakeSearchOptions = ["Mjøsa"]; // Init default list print("Failed to fetch lake names: $e");
} }
} }
\ No newline at end of file
...@@ -44,14 +44,9 @@ class CustomSearchDelegate extends SearchDelegate { ...@@ -44,14 +44,9 @@ class CustomSearchDelegate extends SearchDelegate {
itemCount: searchResults.length, itemCount: searchResults.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
var result = searchResults[index]; var result = searchResults[index];
return GestureDetector( return ListTile(
onTap: () { title: Text(result),
onResultSelected(result); tileColor: Colors.red,
close(context, result);
},
child: ListTile(
title: Text(result),
),
); );
}, },
); );
...@@ -72,6 +67,10 @@ class CustomSearchDelegate extends SearchDelegate { ...@@ -72,6 +67,10 @@ class CustomSearchDelegate extends SearchDelegate {
if (searchResults.isNotEmpty) { if (searchResults.isNotEmpty) {
return ListTile( return ListTile(
title: Text(result), title: Text(result),
onTap: () {
onResultSelected(result);
close(context, result);
},
); );
} }
else { else {
......
String formatMonth(int month) {
switch (month) {
case 1:
return 'Jan';
case 2:
return 'Feb';
case 3:
return 'Mar';
case 4:
return 'Apr';
case 5:
return 'May';
case 6:
return 'Jun';
case 7:
return 'Jul';
case 8:
return 'Aug';
case 9:
return 'Sep';
case 10:
return 'Oct';
case 11:
return 'Nov';
case 12:
return 'Dec';
default:
return '';
}
}
\ No newline at end of file
...@@ -27,14 +27,14 @@ class ChoroplethMap extends StatefulWidget { ...@@ -27,14 +27,14 @@ class ChoroplethMap extends StatefulWidget {
final Uint8List relation; final Uint8List relation;
final List<Measurement> measurements; final List<Measurement> measurements;
final void Function(int _selectedIndex) onSelectionChanged; // Callback function final void Function(int _selectedIndex) onSelectionChanged;
@override @override
_ChoroplethMapState createState() => _ChoroplethMapState(); _ChoroplethMapState createState() => _ChoroplethMapState();
} }
class _ChoroplethMapState extends State<ChoroplethMap> { class _ChoroplethMapState extends State<ChoroplethMap> {
int _selectedIndex = -1; int _selectedIndex = -1; // Subdivision/map tile index
Color _selectedColor = Colors.grey; // Initialise to gray Color _selectedColor = Colors.grey; // Initialise to gray
late MapShapeSource _dataSource; late MapShapeSource _dataSource;
late final MapZoomPanBehavior _zoomPanBehavior = MapZoomPanBehavior(); late final MapZoomPanBehavior _zoomPanBehavior = MapZoomPanBehavior();
...@@ -86,6 +86,7 @@ class _ChoroplethMapState extends State<ChoroplethMap> { ...@@ -86,6 +86,7 @@ class _ChoroplethMapState extends State<ChoroplethMap> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
print("Map built!");
return Stack( return Stack(
children: [ children: [
SfMapsTheme( SfMapsTheme(
...@@ -103,7 +104,7 @@ class _ChoroplethMapState extends State<ChoroplethMap> { ...@@ -103,7 +104,7 @@ class _ChoroplethMapState extends State<ChoroplethMap> {
strokeWidth: 1, strokeWidth: 1,
// Shape selection // Shape selection
selectedIndex: _selectedIndex, selectedIndex: _selectedIndex,
onSelectionChanged: (int index) { // Shape selection behavior onSelectionChanged: (int index) {
setState(() { setState(() {
_selectedIndex = index; _selectedIndex = index;
_selectedColor = _subdivisions[index].color; _selectedColor = _subdivisions[index].color;
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import '../../consts.dart';
import '../../utils/format_month.dart';
class BarData extends StatefulWidget { class BarData extends StatefulWidget {
const BarData({super.key}); const BarData({super.key});
...@@ -12,23 +15,34 @@ class BarData extends StatefulWidget { ...@@ -12,23 +15,34 @@ class BarData extends StatefulWidget {
class _BarDataState extends State<BarData> { class _BarDataState extends State<BarData> {
static const double barWidth = 30; static const double barWidth = 30;
// NB should be rounded to two decimals in server // Allocate bar data dynamically from selected subdivision
// NB should be allocated values dynamically var barData = <int, List<double>>{};
// Bar items show data for 10 previous days double totalHeight = 0;
static const barData = <int, List<double>>{
0: [1.5, 4, 2.5],
1: [1.8, 5.6, 3],
2: [1.5, 3.1, 3.5],
3: [1.5, 1.5, 4],
4: [2, 2, 5],
5: [1.2, 1.5, 4.3],
6: [1.2, 4.8, 5],
};
int touchedIndex = -1; int touchedIndex = -1;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Allocate bar data dynamically based from the selected subdivision
for (int i = 0; i < 7; i++) {
var entry = selectedSubDiv?.iceStats[i];
if (entry != null) {
barData[i] = [
entry.slushIce,
entry.blackIce,
entry.snowDepth,
];
// Find tallest layer
if (entry.totalIce > totalHeight) {
totalHeight = entry.totalIce;
}
} else {
barData[i] = [0.0, 0.0, 0.0];
}
}
} }
BarChartGroupData generateGroup( BarChartGroupData generateGroup(
...@@ -59,7 +73,7 @@ class _BarDataState extends State<BarData> { ...@@ -59,7 +73,7 @@ class _BarDataState extends State<BarData> {
BarChartRodStackItem( BarChartRodStackItem(
value1, value1,
value1 + value2, value1 + value2,
const Color(0xFF000085), const Color(0xFF3766E0),
), ),
BarChartRodStackItem( BarChartRodStackItem(
value1 + value2, value1 + value2,
...@@ -106,28 +120,55 @@ class _BarDataState extends State<BarData> { ...@@ -106,28 +120,55 @@ class _BarDataState extends State<BarData> {
child: BarChart( child: BarChart(
BarChartData( BarChartData(
alignment: BarChartAlignment.center, alignment: BarChartAlignment.center,
maxY: 12, maxY: totalHeight + totalHeight/4, // NB Set
minY: 0, minY: 0,
titlesData: FlTitlesData( titlesData: FlTitlesData(
show: true, show: true,
bottomTitles: SideTitles( bottomTitles: SideTitles(
showTitles: true, showTitles: true,
reservedSize: 5, reservedSize: 20,
getTextStyles: (value) => const TextStyle(color: Colors.white60), getTextStyles: (value) => const TextStyle(color: Colors.white60),
getTitles: (value) {
// Convert bar indexes to dates
if (barData.isNotEmpty && value >= 0 && value < barData.length) {
int index = value.toInt();
DateTime today = DateTime.now();
// Subtract index from the day of the month of the current date
int day = today.day - (6-index);
String date = day.toString();
String month = formatMonth(today.month);
return '$month $date';
}
return '';
},
), ),
leftTitles: SideTitles( leftTitles: SideTitles(
showTitles: true, showTitles: true,
getTextStyles: (value) => const TextStyle(color: Colors.white60), getTextStyles: (value) => const TextStyle(color: Colors.white60),
margin: 10, margin: 5,
reservedSize: 30, reservedSize: 30,
interval: 2, interval: totalHeight/5,
), ),
rightTitles: SideTitles( rightTitles: SideTitles(
showTitles: true, showTitles: true,
getTextStyles: (value) => const TextStyle(color: Colors.white60), getTextStyles: (value) => const TextStyle(color: Colors.white60),
margin: 10, margin: 5,
reservedSize: 30, reservedSize: 30,
interval: 2, interval: totalHeight/5,
),
topTitles: SideTitles(
showTitles: true,
getTextStyles: (value) => const TextStyle(color: Colors.white60),
margin: 0,
reservedSize: 10,
getTitles: (value) {
// Return "cm" for a specific value (e.g., 0) and empty string for others
return value == 0 ? 'cm' : '';
},
), ),
), ),
groupsSpace: 14, groupsSpace: 14,
...@@ -155,9 +196,9 @@ class _BarDataState extends State<BarData> { ...@@ -155,9 +196,9 @@ class _BarDataState extends State<BarData> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
_buildLegendItem(const Color(0xFF13dbff), "Black ice"),
_buildLegendItem(const Color(0xFF3766E0), "Slush ice"),
_buildLegendItem(Colors.white60, "Snow"), _buildLegendItem(Colors.white60, "Snow"),
_buildLegendItem(const Color(0xFF000085), "Black ice"),
_buildLegendItem(const Color(0xFF13dbff), "Slush ice"),
], ],
), ),
), ),
......
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_map/flutter_map.dart';
import 'package:fuzzy/fuzzy.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
...@@ -12,16 +10,17 @@ import 'choropleth_map.dart'; ...@@ -12,16 +10,17 @@ import 'choropleth_map.dart';
import '../data_classes.dart'; import '../data_classes.dart';
import 'satellite_layer.dart'; import 'satellite_layer.dart';
import 'quick_view_chart.dart'; import 'quick_view_chart.dart';
import '../utils/format_month.dart';
/// MapContainerWidget is the main widget that contains the map with all /// MapContainerWidget is the main widget that contains the map with all
/// its layers, polygons and markers. /// its layers, polygons and markers.
class MapContainerWidget extends StatefulWidget { class MapContainerWidget extends StatefulWidget {
final List<Measurement> markerList; final List<Measurement> measurements;
final Uint8List relation; final Uint8List relation;
final bool serverConnection; final bool serverConnection;
const MapContainerWidget({Key? key, const MapContainerWidget({Key? key,
required this.markerList, required this.measurements,
required this.relation, required this.relation,
required this.serverConnection, required this.serverConnection,
}) : super(key: key); }) : super(key: key);
...@@ -32,14 +31,15 @@ class MapContainerWidget extends StatefulWidget { ...@@ -32,14 +31,15 @@ class MapContainerWidget extends StatefulWidget {
class _MapContainerWidgetState extends State<MapContainerWidget> { class _MapContainerWidgetState extends State<MapContainerWidget> {
Measurement? selectedTile; // Containing data for selected marker
bool isMinimized = true; // Quick view box state tacker bool isMinimized = true; // Quick view box state tacker
bool satLayer = false; // Satellite layer visibility tracker bool satLayer = false; // Satellite layer visibility state
bool OSMlayer = false; bool osmLayer = false; // OSM layer visibility state
bool isSatTapped = false; // Button tap state tracker, satellite bool isSatTapped = false; // Satellite button tap state tracker
bool isMapTapped = false; // Button tap state tracker, OSmap bool isMapTapped = false; // OSM button tap state tracker
Measurement? selectedMeasurement = selectedMarkerList[0];
// Initialise lastUpdate variable from persistent storage if server fetch fails // Initialise lastUpdate variable from persistent storage if server fetch fails
Future<void> checkAndSetLastUpdate() async { Future<void> checkAndSetLastUpdate() async {
...@@ -60,11 +60,11 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -60,11 +60,11 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
void handleSelection(int index) { void handleSelection(int index) {
String indexString = index.toString(); String indexString = index.toString();
setState(() { setState(() {
// NB should be optimalised for (Measurement measurement in widget.measurements) {
for (Measurement measurement in widget.markerList) {
for (SubDiv subdivision in measurement.subDivs) { for (SubDiv subdivision in measurement.subDivs) {
if (subdivision.sub_div_id == indexString) { if (subdivision.sub_div_id == indexString) {
selectedTile= widget.markerList[index]; selectedSubDiv = subdivision;
selectedMeasurement = measurement;
break; break;
} }
} }
...@@ -75,7 +75,7 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -75,7 +75,7 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Initialise selectedMarker to first element in markerList // Initialise selectedMarker to first element in markerList
selectedTile ??= widget.markerList[0]; selectedSubDiv ??= widget.measurements[0].subDivs[0];
checkAndSetLastUpdate(); checkAndSetLastUpdate();
...@@ -107,7 +107,7 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -107,7 +107,7 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
padding: const EdgeInsets.all(15.0), // Padding around map padding: const EdgeInsets.all(15.0), // Padding around map
child: ChoroplethMap( child: ChoroplethMap(
relation: widget.relation, relation: widget.relation,
measurements: widget.markerList, measurements: widget.measurements,
onSelectionChanged: handleSelection,), onSelectionChanged: handleSelection,),
), ),
), ),
...@@ -115,8 +115,8 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -115,8 +115,8 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
width: screenWidth * boxWidth, width: screenWidth * boxWidth,
height: screenWidth * boxHeight, height: screenWidth * boxHeight,
child: Visibility( child: Visibility(
visible: OSMlayer, visible: osmLayer,
child: OSM(markerList: widget.markerList), child: OSM(markerList: widget.measurements),
), ),
), ),
Positioned( // Satellite button Positioned( // Satellite button
...@@ -147,12 +147,12 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -147,12 +147,12 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
setState(() { setState(() {
OSMlayer = !OSMlayer; // Toggle satellite layer state on press osmLayer = !osmLayer; // Toggle satellite layer state on press
}); });
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: OSMlayer ? const BoxDecoration( // Add decoration only when pressed decoration: osmLayer ? const BoxDecoration( // Add decoration only when pressed
shape: BoxShape.circle, shape: BoxShape.circle,
color: Colors.grey, color: Colors.grey,
) : null, ) : null,
...@@ -217,7 +217,7 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -217,7 +217,7 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
], ],
), ),
const SizedBox(height: contPadding), // Padding between containers const SizedBox(height: contPadding), // Padding between containers
Column( Column( // Ice stats container
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( SizedBox(
...@@ -240,11 +240,16 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -240,11 +240,16 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
style: subHeadingStyle, style: subHeadingStyle,
), ),
Text( Text(
'Date: ${(selectedTile?.timeMeasured.day ?? '-')}/${(selectedTile?.timeMeasured.month ?? '-')}/${(selectedTile?.timeMeasured.year ?? '-')}', 'Date: ${(selectedMeasurement?.timeMeasured.day ?? '-')}/${(selectedMeasurement?.timeMeasured.month ?? '-')}/${(selectedMeasurement?.timeMeasured.year ?? '-')}',
style: regTextStyle,
),
Text(
'Time: ${selectedMeasurement?.timeMeasured.hour}:00',
style: regTextStyle, style: regTextStyle,
), ),
const SizedBox(height: contPadding),
Text( Text(
'Time: ${selectedTile?.timeMeasured.hour}:00', 'Measuring point: (${selectedMeasurement?.measurementID}, ${selectedMeasurement?.measurementID})',
style: regTextStyle, style: regTextStyle,
), ),
], ],
...@@ -252,49 +257,12 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -252,49 +257,12 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
), ),
), ),
), ),
const SizedBox(height: contPadding*2), const SizedBox(height: contPadding*2.5),
SizedBox(
width: screenWidth * boxWidth * 1.2,
child: Center(
child: Text(
'Measuring point: (${selectedTile?.measurementID}, ${selectedTile?.measurementID})',
style: regTextStyle,
),
),
),
SizedBox( SizedBox(
width: screenWidth * boxWidth * 1.2, width: screenWidth * boxWidth * 1.2,
child: const StatCharts(), child: const StatCharts(),
), ),
const SizedBox(height: contPadding*2), const SizedBox(height: contPadding*4),
SizedBox(
width: screenWidth * boxWidth * 1.2,
child: Padding(
padding: const EdgeInsets.only(top: 20, left: 30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
Icons.info,
color: Colors.white54,
),
const SizedBox(width: 10),
Expanded(
child: Text(
'For every x of y, there has to be z cm of '
'q for every kg of applied weight to ensure ?',
style: regTextStyle,
),
),
],
),
],
),
),
),
const SizedBox(height: contPadding*2),
], ],
), ),
], ],
...@@ -303,34 +271,3 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -303,34 +271,3 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
); );
} }
} }
String formatMonth(int month) {
switch (month) {
case 1:
return 'Jan';
case 2:
return 'Feb';
case 3:
return 'Mar';
case 4:
return 'Apr';
case 5:
return 'May';
case 6:
return 'Jun';
case 7:
return 'Jul';
case 8:
return 'Aug';
case 9:
return 'Sep';
case 10:
return 'Oct';
case 11:
return 'Nov';
case 12:
return 'Dec';
default:
return '';
}
}
\ No newline at end of file
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'bar_graph/bar_data.dart'; import 'graph_data/bar_data.dart';
import '../../consts.dart';
class StatCharts extends StatelessWidget { class StatCharts extends StatelessWidget {
const StatCharts({Key? key}) : super(key: key); const StatCharts({Key? key}) : super(key: key);
...@@ -68,16 +69,34 @@ class StatCharts extends StatelessWidget { ...@@ -68,16 +69,34 @@ class StatCharts extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
/*SizedBox( SizedBox(
child: Center(
child: Text(
'Ice layers',
style: regTextStyleBig,
),
),
),
SizedBox(
width: MediaQuery.of(context).size.width-30,
child: const BarData(),
),
/*
const SizedBox(height: 60),
SizedBox(
child: Center(
child: Text(
'Total ice thickness',
style: regTextStyleBig,
),
),
),
const SizedBox(height: 10),
SizedBox(
width: MediaQuery.of(context).size.width * 0.8, // Set the width of the LineChart width: MediaQuery.of(context).size.width * 0.8, // Set the width of the LineChart
height: 200, height: 200,
child: buildLineChart(context), child: buildLineChart(context),
),*/ ),*/
const SizedBox(height: 20),
SizedBox(
width: MediaQuery.of(context).size.width,
child: const BarData(),
),
], ],
); );
} }
......
...@@ -248,6 +248,14 @@ packages: ...@@ -248,6 +248,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
liquid_pull_to_refresh:
dependency: "direct main"
description:
name: liquid_pull_to_refresh
sha256: "11e4cd8c5460085a31b479ec4e1cd063eb8e599f35684d57a44dafa1fd1f67f3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
lists: lists:
dependency: transitive dependency: transitive
description: description:
......
...@@ -22,6 +22,7 @@ dependencies: ...@@ -22,6 +22,7 @@ dependencies:
fuzzy: any # Search algorithm fuzzy: any # Search algorithm
connectivity_plus: ^3.0.3 # Check internet connection connectivity_plus: ^3.0.3 # Check internet connection
get: ^4.6.5 get: ^4.6.5
liquid_pull_to_refresh: ^3.0.0 # Pull to refresh
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
......
images/geojson-export.png

28.4 KiB

images/geojson-file.png

25.6 KiB

images/overpass-query.png

46 KiB

images/resulting-files.png

19.4 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment