Skip to content
Snippets Groups Projects
Commit 9b990d40 authored by Hoa Ben The Nguyen's avatar Hoa Ben The Nguyen
Browse files

changes: new assets

parent a7b76e40
No related branches found
No related tags found
No related merge requests found
app/assets/icons/frozen.png

12.6 KiB

import 'package:latlong2/latlong.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
// API variables
const String port = "8443";
const String serverURI = "https://127.0.0.1:$port/";
const String mapEndpoint = "update_map";
const int fetchInterval = 60; // Fetch marker data every n minutes
// Map variables
LatLng mapCenter = LatLng(60.7666, 10.8471);
DateTime ?lastUpdate; // Last time data was fetched from server
// Font variables
const textColor = Colors.white;
final appTitleStyle = GoogleFonts.dmSans(
fontSize: 35,
color: Colors.black,
fontWeight: FontWeight.bold,
);
final titleStyle = GoogleFonts.dmSans(
fontSize: 35,
color: textColor,
fontWeight: FontWeight.bold,
);
final regTextStyle = GoogleFonts.dmSans(fontSize: 19, color: textColor);
final chartTextStyle = GoogleFonts.dmSans(fontSize: 14, color: textColor);
final subHeadingStyle = GoogleFonts.dmSans(fontSize: 22, 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
import 'dart:core';
import 'package:latlong2/latlong.dart';
import 'package:flutter/material.dart';
class Measurement {
int measurementID;
DateTime timeMeasured;
Sensor sensor;
String bodyOfWater;
LatLng center;
List <SubDiv> subDivs;
Measurement({
required this.measurementID,
required this.timeMeasured,
required this.sensor,
required this.bodyOfWater,
required this.center,
required this.subDivs,
});
factory Measurement.fromJson(Map<String, dynamic> json) {
return Measurement(
measurementID: json['MeasurementID'],
timeMeasured: DateTime.parse(json['TimeMeasured']),
sensor: Sensor.fromJson(json['Sensor']),
bodyOfWater: json['BodyOfWater'] ?? 'nil',
center: LatLng(json['CenterLat'], json['CenterLon']),
subDivs: (json['Subdivisions'] as List<dynamic>).map((data) => SubDiv.fromJson(data)).toList(),
);
}
}
class SubDiv {
String sub_div_id;
int groupID;
double minThickness;
double avgThickness;
LatLng center;
double accuracy;
Color color;
Color savedColor;
SubDiv({
required this.sub_div_id,
required this.groupID,
required this.minThickness,
required this.avgThickness,
required this.center,
required this.accuracy,
required this.color,
required this.savedColor
});
factory SubDiv.fromJson(Map<String, dynamic> json) {
return SubDiv(
sub_div_id: json['SubdivID'].toString(),
groupID: json['GroupID'],
minThickness: json['MinThickness'],
avgThickness: json['AvgThickness'],
center: LatLng(json['CenLatitude'], json['CenLongitude']),
accuracy: json['Accuracy'],
// Set grey as default color
color: json['Color'] != null ? Color(json['Color']) : Colors.grey,
savedColor: json['Color'] != null ? Color(json['Color']) : Colors.grey,
);
}
}
class Sensor {
int sensorID;
String sensorType;
bool active;
Sensor({
required this.sensorID,
required this.sensorType,
required this.active,
});
factory Sensor.fromJson(Map<String, dynamic> json) {
return Sensor(
sensorID: json['SensorID'],
sensorType: json['SensorType'] ?? 'nil',
active: json['Active'],
);
}
}
import 'dart:io';
import 'dart:async';
import 'dart:convert';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../consts.dart';
import '../data_classes.dart';
class FetchResult {
final List<Measurement> measurements;
final bool connected;
FetchResult(this.measurements, this.connected);
}
/// fetchMarkerData fetches measurement data from the server
Future<FetchResult> fetchMeasurements() async {
try {
// Custom HTTP client
HttpClient client = HttpClient()
..badCertificateCallback = // NB: temporary disable SSL certificate validation
(X509Certificate cert, String host, int port) => true;
// Request markers from server
var request = await client.getUrl(Uri.parse(serverURI + mapEndpoint));
var response = await request.close(); // Close response body at end of function
// Parse body to JSON if request is ok
if (response.statusCode == 200) {
var responseBody = await response.transform(utf8.decoder).join();
if (responseBody.isNotEmpty) {
var jsonData = json.decode(responseBody);
// Attempt to parse response to Measurement object only if the body
// contains correctly formatted data
if (jsonData != null && jsonData is List) {
Directory appDocumentsDirectory = await getApplicationDocumentsDirectory();
String filePath = '${appDocumentsDirectory.path}/last_data.json';
try { // Write most recent time of update to file
await File(filePath).writeAsString(responseBody, mode: FileMode.write);
print('Lake data written to file');
} catch (error) { print('Error in writing to file: $error');}
// Update local and persistent lastUpdate variable
lastUpdate = DateTime.now();
final prefs = await SharedPreferences.getInstance();
await prefs.setString('lastUpdate', '${DateTime.now()}');
return FetchResult(jsonData.map((data) => Measurement.fromJson(data)).toList(), true);
}
}
}
return loadSavedData();
} catch (e) {
return loadSavedData();
}
}
Future<FetchResult> loadSavedData() async {
// Get latest saved data from file if the server does not respond
Directory appDocumentsDirectory = await getApplicationDocumentsDirectory();
String filePath = '${appDocumentsDirectory.path}/last_data.json';
// Read file contents
File file = File(filePath);
if (await file.exists()) {
String contents = await file.readAsString();
List<dynamic> jsonData = json.decode(contents); // Parse JSON string from file
List<Measurement> measurements = jsonData.map((data) => Measurement.fromJson(data)).toList();
return FetchResult(measurements, false);
} else {
throw Exception('File does not exist');
}
}
import 'dart:io';
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:path_provider/path_provider.dart';
import '../../consts.dart';
/// Fetch relation data from server
Future<Uint8List> fetchRelation() async {
try {
// Custom HTTP client
HttpClient client = HttpClient()
..badCertificateCallback = // NB: temporary disable SSL certificate validation
(X509Certificate cert, String host, int port) => true;
// Execute request to to get_relation endpoint
var request = await client.getUrl(Uri.parse('${serverURI}get_relation'));
var response = await request.close(); // Close response body at end of function
// Try to parse body to JSON if request is ok
if (response.statusCode == 200) {
var responseBody = await response.transform(utf8.decoder).join();
if (responseBody.isNotEmpty) {
Directory appDocumentsDirectory = await getApplicationDocumentsDirectory();
String filePath = '${appDocumentsDirectory.path}/last_relation.json';
try { // Write most recent time of update to file
await File(filePath).writeAsString(responseBody, mode: FileMode.write);
print('Relation written to file');
} catch (error) { print('Error in writing to file: $error');}
// Return relation data from the response body
return Uint8List.fromList(utf8.encode(responseBody));
}
}
return loadSavedRelation();
} catch (e) {
return loadSavedRelation();
}
}
Future<Uint8List> loadSavedRelation() async {
// Get latest saved relation from file if the server does not respond
Directory appDocumentsDirectory = await getApplicationDocumentsDirectory();
String filePath = '${appDocumentsDirectory.path}/last_relation.json';
// Read file contents
File file = File(filePath);
if (await file.exists()) {
String contents = await file.readAsString();
List<dynamic> jsonData = json.decode(contents); // Parse JSON string from file
Uint8List relation = Uint8List.fromList(utf8.encode(jsonData.toString()));
return relation;
} else {
throw Exception('File does not exist');
}
}
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_maps/maps.dart';
import 'package:latlong2/latlong.dart';
import '../data_classes.dart';
/// A class containing thickness data for each subdivision of the map.
class IceThicknessModel {
IceThicknessModel(this.sub_div_id, this.thickness, this.color, this.savedColor);
final String sub_div_id;
final int thickness;
Color color;
final Color savedColor;
}
/// ChoroplethMap is a stateful widget that contains a choropleth map.
/// The map is created using the Syncfusion Flutter Maps library and
/// coordinates fetched from the server.
class ChoroplethMap extends StatefulWidget {
const ChoroplethMap({Key? key,
required this.relation,
required this.measurements,
required this.onSelectionChanged,
}) : super(key: key);
final Uint8List relation;
final List<Measurement> measurements;
final void Function(int selectedIndex) onSelectionChanged; // Callback function
@override
_ChoroplethMapState createState() => _ChoroplethMapState();
}
class _ChoroplethMapState extends State<ChoroplethMap> {
int selectedIndex = -1;
late MapShapeSource mapShapeSource;
late final MapZoomPanBehavior _zoomPanBehavior = MapZoomPanBehavior();
List<SubDiv> subdivisions = <SubDiv>[];
int count = 0;
@override
void initState() {
super.initState();
// Create list of all subdivisions
for (Measurement measurement in widget.measurements) {
for (SubDiv subdivision in measurement.subDivs) {
subdivisions.add(subdivision);
print("SubDivID: ${subdivision.sub_div_id}");
count++;
}
};
// NB temporary filler
for (var i = count; i < 250; i++) {
SubDiv subdivision = SubDiv(
sub_div_id: i.toString(),
groupID: 0,
minThickness: 0.0,
avgThickness: 0.0,
center: LatLng(0.0, 0.0),
accuracy: 0.0,
color: Colors.grey,
savedColor: Colors.grey,
);
subdivisions.add(subdivision);
}
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
SfMaps(
layers: [
MapShapeLayer(
source: MapShapeSource.memory( // Map polygon
widget.relation, // JSON coordinates from server
shapeDataField: 'sub_div_id',
dataCount: 250,
primaryValueMapper: (int index) => subdivisions[index].sub_div_id,
shapeColorValueMapper: (int index) => subdivisions[index].color,
),
zoomPanBehavior: _zoomPanBehavior,
strokeColor: Colors.blue.shade50,
// Shape selection
selectedIndex: selectedIndex,
onSelectionChanged: (int index) {
setState(() {
selectedIndex = index;
for (int i = 0; i < subdivisions.length; i++) {
subdivisions[i].color = i == index ? Colors.red : subdivisions[i].savedColor;
}
});
widget.onSelectionChanged(selectedIndex);
},
),
],
),
],
);
}
}
import 'dart:typed_data';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'satellite_layer.dart';
import 'stat_charts.dart';
import '../../consts.dart';
import 'choropleth_map.dart';
import '../data_classes.dart';
import 'quick_view_chart.dart';
/// MapContainerWidget is the main widget that contains the map with all
/// its layers, polygons and markers.
class MapContainerWidget extends StatefulWidget {
final List<Measurement> markerList;
final Uint8List relation;
final bool serverConnection;
const MapContainerWidget({Key? key,
required this.markerList,
required this.relation,
required this.serverConnection,
}) : super(key: key);
@override
_MapContainerWidgetState createState() => _MapContainerWidgetState();
}
class _MapContainerWidgetState extends State<MapContainerWidget> {
Measurement? selectedTile; // Containing data for selected marker
int selectedTileIndex = 0;
bool isMinimized = true; // Quick view box state tacker
bool satLayer = false; // Satellite layer visibility tracker
bool isTapped = false; // Button tap state tracker
final MapController _mapController = MapController(); // Map controller to re-center map view
// recenterMap moves the map back to its initial view
void recenterMap() {
_mapController.move(mapCenter, 9.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(() {
selectedTileIndex = index;
});
}
@override
Widget build(BuildContext context) {
// Initialise selectedMarker to first element in markerList
selectedTile ??= widget.markerList[selectedTileIndex];
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),
/*if (true) NB: add search bar
const SearchBar(),
const SizedBox(height: contPadding),*/
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack( // Stack of quick view, map layer, satellite layer, and buttons
children: [
/*SizedBox(
width: screenWidth * boxWidth,
height: screenWidth * boxHeight,
child: Stack(
children: [
SatLayer(markerList: widget.markerList), // Satellite layer
Visibility(
visible: satLayer, // Only show layer if satellite button is toggled on
child: FlutterMap(
options: MapOptions(
center: mapCenter,
zoom: 9.0,
),
mapController: _mapController,
children: [
TileLayer(
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: const ['a', 'b', 'c'],
),
MarkerLayer(
markers: widget.markerList.map((Measurement measurement) {
return Marker(
width: 50,
height: 50,
point: measurement.center, // Set markers at center of measurement
builder: (ctx) => GestureDetector(
onTap: () {
setState(() {
selectedMarker = measurement;
});
},
child: Icon(
Icons.severe_cold,
color: measurement == selectedMarker ? Colors.green : Colors.blue,
size: measurement == selectedMarker ? 40.0 : 30.0,
),
),
);
}).toList(),
),
],
),
),
],
),
),*/
SizedBox( // Colored box behind map
width: screenWidth * boxWidth,
height: screenWidth * boxHeight,
child: Container(
color: const Color(0x883366ff),
child: 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}-${lastUpdate?.month}-${lastUpdate?.year}') : ''}',
style: GoogleFonts.dmSans(
fontSize: 14,
color: Colors.black,
),
),
],
),
),
),
SizedBox( // Lake map
width: screenWidth * boxWidth,
height: screenWidth * boxHeight,
child: Padding(
padding: const EdgeInsets.all(15.0), // Padding around map
child: ChoroplethMap(
relation: widget.relation,
measurements: widget.markerList,
onSelectionChanged: handleSelection,),
),
),
Positioned( // Quick view box layered over map
bottom: 10,
right: 10,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(
width: (screenWidth * boxWidth) / 2.4,
height: isMinimized ? 20 : (screenWidth * boxWidth) / 2.4,
color: Colors.blue.withOpacity(0.7),
child: Stack(
children: [
Visibility( // Graph only visible when box is maximized and a marker is selected
visible: !isMinimized && selectedTile != null,
child: Center(
child: Padding(
padding: const EdgeInsets.only(right: 16.0, top: 17.0),
child: SizedBox(
width: (screenWidth * boxWidth) / 2.3,
height: (screenWidth * boxWidth) / 2.3,
child: const QuickViewChart(), // Quick view graph
),
),
),
),
Positioned(
top: 0,
right: 5,
child: GestureDetector(
onTap: () {
setState(() {
isMinimized = !isMinimized; // Toggle minimized state
});
},
child: Icon(isMinimized ? Icons.arrow_drop_up : Icons.arrow_drop_down),
),
),
],
),
),
),
),
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.blue,
) : null,
child: const Icon(Icons.satellite_alt_outlined),
),
),
),
Positioned( // Back to center button
top: 45,
right: 10,
child: GestureDetector(
onTapDown: (_) {
setState(() {
recenterMap(); // Reset map view
isTapped = true;
});
},
onTapUp: (_) {
setState(() {
isTapped = false;
});
},
onTapCancel: () {
setState(() {
isTapped = false;
});
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: isTapped ? const BoxDecoration( // Add decoration only when pressed
shape: BoxShape.circle,
color: Colors.blue,
) : null,
child: const Icon(Icons.settings_backup_restore),
),
),
),
Positioned( // No wifi icon
top: 80,
right: 10,
child: GestureDetector(
onTapDown: (_) {
setState(() {
// Add functionality
});
},
onTapUp: (_) {
setState(() {
// Add functionality
});
},
onTapCancel: () {
setState(() {
// Add functionality
});
},
child: Visibility( // Show icon only when no server connection
visible: !widget.serverConnection,
child: Container(
padding: const EdgeInsets.all(8),
decoration: isTapped ? const BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue,
) : null,
child: const Icon(Icons.perm_scan_wifi, color: Color(0xFF5B0000)),
),
),
),
),
],
),
),
const SizedBox(height: contPadding), // Padding between containers
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: SizedBox(
width: screenWidth * boxWidth,
height: screenWidth * boxHeight * 1.5, // NB: make dynamic
child: Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.only(top: 20, left: 20), // Edge padding, text
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ice stats',
style: titleStyle,
),
const Divider(),
Text(
'Time of measurement',
style: subHeadingStyle,
),
Text(
'Date ${(selectedTile?.timeMeasured.day ?? '-')}/${(selectedTile?.timeMeasured.month ?? '-')}/${(selectedTile?.timeMeasured.year ?? '-')}',
style: regTextStyle,
),
Text(
'Time: ${selectedTile?.timeMeasured.hour}:00',
style: regTextStyle,
),
const SizedBox(height: contPadding),
Text(
'Measuring point: (${selectedTile?.measurementID}, ${selectedTile?.measurementID})',
style: regTextStyle,
),
const SizedBox(height: contPadding),
const StatCharts(),
],
),
),
),
),
),
],
);
},
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import '../../consts.dart';
import '../data_classes.dart';
class OSM extends StatelessWidget {
final List<Measurement> markerList;
const OSM({
Key? key,
required this.markerList,
}) : super(key: key);
@override
Widget build(BuildContext context) {
// Init list of polygons
List<Polygon> polygons = [];
// Map each element from markerList to a measurement object
return FlutterMap(
options: MapOptions(
center: mapCenter,
zoom: 9.0,
),
children: [
TileLayer(
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: const ['a', 'b', 'c'],
),
PolygonLayer(
polygons: polygons, // Return map with list of polygons included
),
],
);
}
}
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
class QuickViewChart extends StatelessWidget {
const QuickViewChart({super.key});
@override
Widget build(BuildContext context) {
return LineChart(
LineChartData(
titlesData: FlTitlesData(
leftTitles: SideTitles(showTitles: true),
bottomTitles: SideTitles(showTitles: true),
),
borderData: FlBorderData(
show: true,
),
minX: 0, // Test data
maxX: 4,
minY: 0,
maxY: 50,
lineBarsData: [
LineChartBarData(
spots: [
FlSpot(0, 10), // Test data
FlSpot(1, 20),
FlSpot(2, 30),
FlSpot(3, 40),
],
isCurved: true,
colors: [Colors.blue],
),
],
),
);
}
}
import 'package:latlong2/latlong.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import '../data_classes.dart';
class SatelliteLayer extends StatelessWidget {
final List<Measurement> markerList;
const SatelliteLayer({
Key? key,
required this.markerList,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FlutterMap(
options: MapOptions(
center: LatLng(60.7666, 10.8471),
zoom: 9.0,
),
children: [
TileLayer( // Map from OpenStreetMap
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: const ['a', 'b', 'c'],
),
],
);
}
}
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
class StatCharts extends StatelessWidget {
const StatCharts({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
LineChart(
LineChartData(
backgroundColor: Colors.lightBlue,
titlesData: FlTitlesData(
leftTitles: SideTitles(showTitles: true),
bottomTitles: SideTitles(showTitles: true),
),
borderData: FlBorderData(show: true),
minX: 0,
maxX: 4,
minY: 0,
maxY: 50,
lineBarsData: [
LineChartBarData(
spots: [
FlSpot(0, 10),
FlSpot(1, 20),
FlSpot(2, 30),
FlSpot(3, 40),
],
isCurved: true,
colors: [Colors.blue],
),
],
),
),
const SizedBox(height: 20),
SizedBox(
width: MediaQuery.of(context).size.width * 0.8,
height: 160,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: 20,
barTouchData: BarTouchData(enabled: false),
titlesData: FlTitlesData(
bottomTitles: SideTitles(
showTitles: true,
getTextStyles: (value) => const TextStyle(color: Colors.black),
margin: 10,
getTitles: (value) {
switch (value.toInt()) {
case 0:
return 'Placeholder1';
case 1:
return 'Placeholder2';
case 2:
return 'Placeholder3';
default:
return '';
}
},
),
leftTitles: SideTitles(
showTitles: true,
getTextStyles: (value) => const TextStyle(color: Colors.black),
margin: 10,
reservedSize: 30,
interval: 5,
),
),
borderData: FlBorderData(
show: true,
border: Border.all(color: Colors.white, width: 1),
),
barGroups: [
BarChartGroupData(
x: 0,
barRods: [
BarChartRodData(y: 15, width: 10),
],
),
BarChartGroupData(
x: 1,
barRods: [
BarChartRodData(y: 10, width: 10),
],
),
BarChartGroupData(
x: 2,
barRods: [
BarChartRodData(y: 18, width: 10),
],
),
],
),
),
),
],
);
}
}
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