From 9b990d403eb2a6c1260f43fc72fd18ba28bc1975 Mon Sep 17 00:00:00 2001 From: Hoa Ben The Nguyen <hbnguye@stud.ntnu.no> Date: Thu, 21 Mar 2024 12:37:52 +0100 Subject: [PATCH] changes: new assets --- app/assets/icons/frozen.png | Bin 0 -> 12888 bytes app/lib/consts.dart | 37 +++ app/lib/data_classes.dart | 88 +++++ app/lib/server_requests/fetch_markers.dart | 77 +++++ app/lib/server_requests/fetch_relation.dart | 60 ++++ app/lib/widgets/choropleth_map.dart | 107 ++++++ app/lib/widgets/main_layout.dart | 347 ++++++++++++++++++++ app/lib/widgets/osm_layer.dart | 38 +++ app/lib/widgets/quick_view_chart.dart | 37 +++ app/lib/widgets/satellite_layer.dart | 30 ++ app/lib/widgets/stat_charts.dart | 102 ++++++ 11 files changed, 923 insertions(+) create mode 100644 app/assets/icons/frozen.png create mode 100644 app/lib/consts.dart create mode 100644 app/lib/data_classes.dart create mode 100644 app/lib/server_requests/fetch_markers.dart create mode 100644 app/lib/server_requests/fetch_relation.dart create mode 100644 app/lib/widgets/choropleth_map.dart create mode 100644 app/lib/widgets/main_layout.dart create mode 100644 app/lib/widgets/osm_layer.dart create mode 100644 app/lib/widgets/quick_view_chart.dart create mode 100644 app/lib/widgets/satellite_layer.dart create mode 100644 app/lib/widgets/stat_charts.dart diff --git a/app/assets/icons/frozen.png b/app/assets/icons/frozen.png new file mode 100644 index 0000000000000000000000000000000000000000..5c224e3c664cdb1628cff2a696e6ed9c045d0bf5 GIT binary patch literal 12888 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 G{r>}sTY(+` literal 0 HcmV?d00001 diff --git a/app/lib/consts.dart b/app/lib/consts.dart new file mode 100644 index 00000000..ff51be45 --- /dev/null +++ b/app/lib/consts.dart @@ -0,0 +1,37 @@ +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 diff --git a/app/lib/data_classes.dart b/app/lib/data_classes.dart new file mode 100644 index 00000000..8de99433 --- /dev/null +++ b/app/lib/data_classes.dart @@ -0,0 +1,88 @@ +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'], + ); + } +} diff --git a/app/lib/server_requests/fetch_markers.dart b/app/lib/server_requests/fetch_markers.dart new file mode 100644 index 00000000..410a25df --- /dev/null +++ b/app/lib/server_requests/fetch_markers.dart @@ -0,0 +1,77 @@ +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'); + } +} diff --git a/app/lib/server_requests/fetch_relation.dart b/app/lib/server_requests/fetch_relation.dart new file mode 100644 index 00000000..ab77dcec --- /dev/null +++ b/app/lib/server_requests/fetch_relation.dart @@ -0,0 +1,60 @@ +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'); + } +} + diff --git a/app/lib/widgets/choropleth_map.dart b/app/lib/widgets/choropleth_map.dart new file mode 100644 index 00000000..56452604 --- /dev/null +++ b/app/lib/widgets/choropleth_map.dart @@ -0,0 +1,107 @@ +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); + }, + ), + ], + ), + ], + ); + } +} + diff --git a/app/lib/widgets/main_layout.dart b/app/lib/widgets/main_layout.dart new file mode 100644 index 00000000..ae2a233a --- /dev/null +++ b/app/lib/widgets/main_layout.dart @@ -0,0 +1,347 @@ +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 diff --git a/app/lib/widgets/osm_layer.dart b/app/lib/widgets/osm_layer.dart new file mode 100644 index 00000000..33b000b6 --- /dev/null +++ b/app/lib/widgets/osm_layer.dart @@ -0,0 +1,38 @@ +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 + ), + ], + ); + } +} diff --git a/app/lib/widgets/quick_view_chart.dart b/app/lib/widgets/quick_view_chart.dart new file mode 100644 index 00000000..51dfa4f1 --- /dev/null +++ b/app/lib/widgets/quick_view_chart.dart @@ -0,0 +1,37 @@ +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], + ), + ], + ), + ); + } +} diff --git a/app/lib/widgets/satellite_layer.dart b/app/lib/widgets/satellite_layer.dart new file mode 100644 index 00000000..a82614bc --- /dev/null +++ b/app/lib/widgets/satellite_layer.dart @@ -0,0 +1,30 @@ +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'], + ), + ], + ); + } +} diff --git a/app/lib/widgets/stat_charts.dart b/app/lib/widgets/stat_charts.dart new file mode 100644 index 00000000..86d511e6 --- /dev/null +++ b/app/lib/widgets/stat_charts.dart @@ -0,0 +1,102 @@ +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), + ], + ), + ], + ), + ), + ), + ], + ); + } +} -- GitLab