diff --git a/app/lib/consts.dart b/app/lib/consts.dart deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/app/lib/main.dart b/app/lib/main.dart index c564513ea293ae60fdfb14f8894073b8b9eef99f..7f804e167f65ab7b2a70699d638bcb215f542a03 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,25 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:latlong2/latlong.dart'; // Import LatLng class from the latlong package -import 'dart:async'; -import 'dart:io'; -import 'dart:convert'; +import 'pages/default_page.dart'; +import 'pages/marker_data.dart'; - -const String port = "8443"; -const String serverURI = "https://127.0.0.1:$port/"; -const String mapEndpoint = "update_map"; -// NB: if http connection fails, run: adb reverse tcp:8443 tcp:8443 -const int fetchInterval = 5; - -// main is the entry point for the application, and starts the App() function void main() { - runApp(const App()); + runApp(MyApp()); } -class App extends StatelessWidget { - const App({super.key}); - +class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return const MaterialApp( @@ -27,193 +14,3 @@ class App extends StatelessWidget { ); } } - -class DefaultPage extends StatefulWidget { - const DefaultPage({Key? key}) : super(key: key); - - @override - _DefaultPageState createState() => _DefaultPageState(); -} - -// MarkerData holds data for the dynamicllay allocated markers -class MarkerData { - final LatLng location; - final double size; - final Color color; - final double radius; - - MarkerData({required this.location, required this.size, required this.color, required this.radius}); -} - -// parseMarkerData parses jsonData into an object of type MakerData -List<MarkerData> parseMarkerData(String jsonString) { - final parsed = json.decode(jsonString); - return List<MarkerData>.from(parsed.map((data) => MarkerData( - location: LatLng(data['latitude'], data['longitude']), - size: data['size'].toDouble(), - color: parseColor(data['color']), - radius: data['radius'].toDouble(), - ))); -} - -// parseColor parses the color strings into Colors types -Color parseColor(String colorString) { - switch (colorString) { - case 'blue': - return Colors.blue; - case 'red': - return Colors.red; - case 'green': - return Colors.green; - default: - return Colors.black; // Default color if unrecognized - } -} - -class _DefaultPageState extends State<DefaultPage> { - late Timer _timer; - - List<MarkerData> markerList = []; - - // fetchMarkerData requests data from the update_map endpoint - Future<void> fetchMarkerData() async { - try { - // Custom HTTP client - HttpClient client = HttpClient() - ..badCertificateCallback = // NB: temporary disable SSL certificate validation - (X509Certificate cert, String host, int port) => true; - - var request = await client.getUrl(Uri.parse(serverURI+mapEndpoint)); - var response = await request.close(); - - // Parse json response to list of MarkerData objects if request is ok - if (response.statusCode == 200) { - var responseBody = await response.transform(utf8.decoder).join(); - setState(() { - markerList = parseMarkerData(responseBody); - }); - } else { - print('Request failed with status: ${response.statusCode}'); - } - } catch (e) { - print('Failed to connect to the server: $e'); - } - } - - // Timer initializer - @override - void initState() { - super.initState(); - // Call fetchMarkerData when the widget is first created - fetchMarkerData(); - - // Schedule fetchMarkerData to run periodically based on fetchInterval const - const Duration fiveMinutes = Duration(minutes: fetchInterval); - _timer = Timer.periodic(fiveMinutes, (timer) { - fetchMarkerData(); - }); - } - - // Fetch timer - @override - void dispose() { - // Cancel timer on widget termination - _timer.cancel(); - super.dispose(); - } - - // Main widget - @override - Widget build(BuildContext context) { - double screenWidth = MediaQuery.of(context).size.width; - double boxWidth = 0.9; - double boxHeight = 1.5; - const double markerSize = 20; - - return Scaffold( - appBar: AppBar( - title: const Text('IceMap'), - ), - body: Center( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - Container( // Map container - width: screenWidth * boxWidth, - height: screenWidth * boxHeight, - color: Colors.blue, - child: FlutterMap( - options: MapOptions( - center: LatLng(60.7666, 10.8471), - zoom: 9.0, - ), - children: [ - TileLayer( - urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - subdomains: const ['a', 'b', 'c'], - ), - MarkerLayer( - // Dynamically allocate markers based on a list - markers: markerList.map((MarkerData markerData) { - return Marker( - width: markerData.size, - height: markerData.size, - point: markerData.location, - builder: (ctx) => GestureDetector( - onTap: () { - // NB: temporary print - print('Icon tapped'); - // NB: trigger function to add contents to the next box - }, - child: Image.asset( - 'assets/icons/circle-red.png', // Path to your custom icon asset - color: markerData.color, - width: markerData.radius, - height: markerData.radius, - ), - ), - ); - }).toList(), - ), - /*PolygonLayer( - polygons: [ - Polygon( - points: [ - LatLng(60.7600, 10.8000), - LatLng(60.7600, 11.0000), - LatLng(60.7000, 11.0000), - LatLng(60.7000, 10.8000), - ], - color: Colors.blue, - isFilled: true, - ), - ], - ),*/ - ], - ), - ), - const SizedBox(height: 20), - Container( // Detailed info container - width: screenWidth * boxWidth, - height: screenWidth * boxHeight, - color: Colors.blue, - child: const Align( - alignment: Alignment.topLeft, - child: Padding( - padding: EdgeInsets.only(top: 10, left: 10), // Edge padding, text - child: Text( - 'Placeholder text', - style: TextStyle(fontSize: 20, color: Colors.black), - ), - ), - ), - ), - const SizedBox(height: 20), - ], - ), - ), - ), - ); - } -} diff --git a/app/lib/pages/consts.dart b/app/lib/pages/consts.dart new file mode 100644 index 0000000000000000000000000000000000000000..8ee35bb36c0de55ab092b643fa59f015c51f8348 --- /dev/null +++ b/app/lib/pages/consts.dart @@ -0,0 +1,4 @@ +const String port = "8443"; +const String serverURI = "https://127.0.0.1:$port/"; +const String mapEndpoint = "update_map"; +const int fetchInterval = 5; \ No newline at end of file diff --git a/app/lib/pages/default_page.dart b/app/lib/pages/default_page.dart new file mode 100644 index 0000000000000000000000000000000000000000..8bdf56876acc0455b771977bfdcb24d08150980e --- /dev/null +++ b/app/lib/pages/default_page.dart @@ -0,0 +1,87 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'widgets/map_widget.dart'; +import 'marker_data.dart'; +import 'consts.dart'; + +class DefaultPage extends StatefulWidget { + const DefaultPage({super.key}); + + @override + _DefaultPageState createState() => _DefaultPageState(); +} + +class _DefaultPageState extends State<DefaultPage> { + late Timer _timer; + + List<MarkerTemplate> markerList = []; + + // fetchMarkerTemplate requests data from the update_map endpoint + Future<void> fetchMarkerTemplate() async { + try { + // Custom HTTP client + HttpClient client = HttpClient() + ..badCertificateCallback = // NB: temporary disable SSL certificate validation + (X509Certificate cert, String host, int port) => true; + + // Request makers from API and wait for response + var request = await client.getUrl(Uri.parse(serverURI+mapEndpoint)); + var response = await request.close(); + + // Attempt to parse json if request is ok + if (response.statusCode == 200) { + var responseBody = await response.transform(utf8.decoder).join(); + setState(() { + // Parse JSON string from response body + List<dynamic> jsonData = json.decode(responseBody); + + // Convert response from type List<dynamic> to List<MarkerTemplate> + markerList = jsonData.map((data) => MarkerTemplate.fromJson(data)).toList(); + }); + } else { + print('Request failed with status: ${response.statusCode}'); + } + } catch (e) { + print('Failed to connect to the server: $e'); + } + } + + // State initializer + @override + void initState() { + super.initState(); + // Call fetchMarkerTemplate when the widget is first created + fetchMarkerTemplate(); + + // Schedule fetchMarkerTemplate to run periodically based on fetchInterval const + const Duration fiveMinutes = Duration(minutes: fetchInterval); + _timer = Timer.periodic(fiveMinutes, (timer) { + fetchMarkerTemplate(); + }); + } + + // Fetch timer + @override + void dispose() { + // Cancel timer on widget termination + _timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('IceMap'), + ), + body: ListView( + children: [ + MapContainerWidget(markerList: markerList),// Return a specific list element + ], + ), + ); + } +} + diff --git a/app/lib/pages/marker_data.dart b/app/lib/pages/marker_data.dart new file mode 100644 index 0000000000000000000000000000000000000000..1c3d615097246f16efc6eed738494179f3042a98 --- /dev/null +++ b/app/lib/pages/marker_data.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; + +// Sensor holds data about a single sensor +class Sensor { + int id; + String type; + bool active; + + Sensor({required this.id, required this.type, required this.active}); + + factory Sensor.fromJson(Map<String, dynamic> json) { + return Sensor( + id: json['ID'], + type: json['type'], + active: json['active'], + ); + } +} + +// DateAndTime holds the date and time for a single measurement +class DateAndTime { + int year; + int month; + int day; + int hour; + int minute; + + DateAndTime({ + required this.year, + required this.month, + required this.day, + required this.hour, + required this.minute, + }); + + factory DateAndTime.fromJson(Map<String, dynamic> json) { + return DateAndTime( + year: json['year'], + month: json['month'], + day: json['day'], + hour: json['hour'], + minute: json['minute'], + ); + } +} + +// Measurement holds data related to a singular measurement taken +// at a given time +class Measurement { + double longitude; + double latitude; + DateAndTime datetime; + Sensor sensor; + double precipitation; + double thickness; + double maxWeight; + double safetyLevel; + double accuracy; + + Measurement({ + required this.longitude, + required this.latitude, + required this.datetime, + required this.sensor, + required this.precipitation, + required this.thickness, + required this.maxWeight, + required this.safetyLevel, + required this.accuracy, + }); + + factory Measurement.fromJson(Map<String, dynamic> json) { + return Measurement( + longitude: json['longitude'], + latitude: json['latitude'], + datetime: DateAndTime.fromJson(json['datetime']), + sensor: Sensor.fromJson(json['sensor']), + precipitation: json['precipitation'], + thickness: json['thickness'], + maxWeight: json['max_weight'], + safetyLevel: json['safety_level'], + accuracy: json['accuracy'], + ); + } +} + +// MarkerTemplate holds all data required for rendering a marker +class MarkerTemplate { + Measurement geoData; + LatLng location; + double size; + Color color; + + MarkerTemplate({ + required this.geoData, + required this.location, + required this.size, + required this.color + }); + + factory MarkerTemplate.fromJson(Map<String, dynamic> json) { + // Parse from JSON string to type Color + Color parsedColor = parseColor(json['color']); + + return MarkerTemplate( + geoData: Measurement.fromJson(json['geo_data']), + location: LatLng(json['latitude'], json['longitude']), + size: json['size'], + color: parsedColor, + ); + } + + // parseColor parses the color strings into Colors types + static Color parseColor(String colorString) { + switch (colorString) { + case 'yellow': + return Colors.yellow; + case 'red': + return Colors.red; + case 'green': + return Colors.green; + default: + return Colors.black; // Default color if unrecognized + } + } +} diff --git a/app/lib/pages/widgets/map_widget.dart b/app/lib/pages/widgets/map_widget.dart new file mode 100644 index 0000000000000000000000000000000000000000..ace0b220753daec3d1309a840c791207c48d3159 --- /dev/null +++ b/app/lib/pages/widgets/map_widget.dart @@ -0,0 +1,224 @@ +import 'package:flutter/material.dart'; +import '../marker_data.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:fl_chart/fl_chart.dart'; + +class MapContainerWidget extends StatefulWidget { + final List<MarkerTemplate> markerList; + + const MapContainerWidget({Key? key, required this.markerList}) : super(key: key); + + @override + _MapContainerWidgetState createState() => _MapContainerWidgetState(); +} + +class _MapContainerWidgetState extends State<MapContainerWidget> { + + MarkerTemplate? selectedMarker; + + @override + Widget build(BuildContext context) { + + // NB: temporary test data + final List<FlSpot> chartData = [ + FlSpot(0, 10), + FlSpot(1, 20), + FlSpot(2, 30), + FlSpot(3, 40), + ]; + double barWidth = 30; // Width for bars in bar chart + const double contPadding = 30; // Container padding space + + double screenWidth = MediaQuery.of(context).size.width; + double boxWidth = 0.9; + double boxHeight = 1.5; + + return Column( + children: [ + Container( + width: screenWidth * boxWidth, + height: screenWidth * boxHeight, + color: Colors.blue, + child: FlutterMap( + options: MapOptions( + center: LatLng(60.7666, 10.8471), + zoom: 9.0, + ), + children: [ + TileLayer( + urlTemplate: "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", + subdomains: const ['a', 'b', 'c'], + ), + MarkerLayer( + markers: widget.markerList.map((MarkerTemplate markerTemplate) { + return Marker( + width: markerTemplate.size, + height: markerTemplate.size, + point: markerTemplate.location, + builder: (ctx) => GestureDetector( + onTap: () { + setState(() { + selectedMarker = markerTemplate; + }); + }, + child: Image.asset( + 'assets/icons/circle-red.png', + // Path to your custom icon asset + color: markerTemplate.color, + width: markerTemplate.size, + height: markerTemplate.size, + ), + ), + ); + }).toList(), + ), + /*PolygonLayer( + polygons: [ + Polygon( + points: [ + LatLng(60.7600, 10.8000), + LatLng(60.7600, 11.0000), + LatLng(60.7000, 11.0000), + LatLng(60.7000, 10.8000), + ], + color: Colors.blue, + isFilled: true, + ), + ], + ),*/ + ], + ), + ), + const SizedBox(height: contPadding), // Padding between containers + Container( + //width: screenWidth * boxWidth, + height: screenWidth * boxHeight*1.5, // NB: make dynamic + color: const Color(0xFF64B5F6), + child: Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only(top: 20, left: 20), // Edge padding, text + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Ice stats', + style: TextStyle(fontSize: 30, color: Colors.black), + ), + Text( + 'Latitude: ${selectedMarker?.geoData.latitude}', + style: const TextStyle(fontSize: 20, color: Colors.black), + ), + Text( + 'Longitude: ${selectedMarker?.geoData.longitude}', + style: const TextStyle(fontSize: 20, color: Colors.black), + ), + const SizedBox(height: contPadding), + SizedBox( + width: screenWidth-screenWidth/10, + height: 200, + child: LineChart( + LineChartData( + backgroundColor: const Color(0xFFCAF0F8), + 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: chartData, + isCurved: true, + colors: [const Color(0xFF0077B6)], + ), + ], + ), + ), + ), + const SizedBox(height: contPadding), + SizedBox( + width: screenWidth-screenWidth/10, + height: 200, + child: BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + maxY: 20, // Maximum y-axis value + barTouchData: BarTouchData(enabled: false), + titlesData: FlTitlesData( + bottomTitles: SideTitles( + showTitles: true, + getTextStyles: (value) => const TextStyle(color: Colors.black, fontSize: 14), + 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, fontSize: 14), + margin: 10, + reservedSize: 30, + interval: 5, + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all(color: Colors.black, width: 1), + ), + barGroups: [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + y: 15, // Bar height + width: barWidth, // Bar width + ), + ], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + y: 10, + width: barWidth, + ), + ], + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData( + y: 18, + width: barWidth, + ), + ], + ), + ], + ), + ) + ) + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/app/pubspec.lock b/app/pubspec.lock index 29ec17a46d8598260b9d81c6ec4ef924f60898a6..839fa15f15b5e51b6a1918e0599fea9f6ff5d751 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: a92c5677c820884abc7cd7980ef2ecc6df094ecd2730074cbb77f7d195afefd4 + url: "https://pub.dev" + source: hosted + version: "0.20.1" flutter: dependency: "direct main" description: flutter @@ -155,6 +171,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -163,6 +187,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + path_drawing: + dependency: transitive + description: + name: path_drawing + sha256: "3bdd251dae9ffaef944450b73f168610db7e968e7b20daf0c3907f8b4aafc8a2" + url: "https://pub.dev" + source: hosted + version: "0.5.1+1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: ee5c47c1058ad66b4a41746ec3996af9593d0858872807bcd64ac118f0700337 + url: "https://pub.dev" + source: hosted + version: "0.2.1" + pedantic: + dependency: transitive + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" + source: hosted + version: "1.11.1" polylabel: dependency: transitive description: @@ -179,6 +227,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "59471e0a4595e264625d3496af567ac85bdae1148ec985aff1e0555786f53ecf" + url: "https://pub.dev" + source: hosted + version: "5.0.0" sky_engine: dependency: transitive description: flutter diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 040d821ed00ded67b57e9ba1601874cde445f8e2..0e2b2e793202b6b75ce78a97e8883829616bbd20 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -12,6 +12,8 @@ dependencies: flutter_map: ^4.0.0 http: ^0.13.3 latlong2: ^0.8.2 + provider: ^5.0.0 + fl_chart: ^0.20.0-nullsafety1 diff --git a/server/map/__pycache__/get_markers.cpython-311.pyc b/server/map/__pycache__/get_markers.cpython-311.pyc index d666b220cf6408f555dc5c3b7c112d6787766db2..7069518c401a9926d6c76eb189af1c3fec74ac49 100644 Binary files a/server/map/__pycache__/get_markers.cpython-311.pyc and b/server/map/__pycache__/get_markers.cpython-311.pyc differ diff --git a/server/map/get_markers.py b/server/map/get_markers.py index c18695766d0321df070ec83c2f0a58cd65e8d157..e827104993c0c76feb79ef02862f017b74fc0589 100644 --- a/server/map/get_markers.py +++ b/server/map/get_markers.py @@ -16,14 +16,14 @@ def get_all_markers(self, col): # Remove ObjectId field documents to allow for easy JSON to BSON conversion for document in marker_data: document.pop('_id', None) - if marker_data: # Data found in DB, convert from BSON to JSON resp_code = 200 marker_json_list = [json_util.dumps(document) for document in marker_data] marker_json = "[" + ",".join(marker_json_list) + "]" else: # Data not found in DB resp_code = 404 - marker_json = '[]' + marker_json = '[]' + except Exception as e: print(f"An error occurred while querying MongoDB: {e}") resp_code = 500