diff --git a/.gitignore b/.gitignore index 6668ffa9626fc3fb8f6c6a8bbb8b1ec1c07ac1f6..5bd67ec656a148677ed4235edf0b1531818c028b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ .vs/ # Auto-generated by Android Studio -.idea/ \ No newline at end of file +.idea/ + +# Python interpereter +venv/ \ No newline at end of file diff --git a/app/lib/consts.dart b/app/assets/fonts/lato/Lato-Black.ttf similarity index 100% rename from app/lib/consts.dart rename to app/assets/fonts/lato/Lato-Black.ttf diff --git a/app/assets/fonts/lato/Lato-BlackItalic.ttf b/app/assets/fonts/lato/Lato-BlackItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/lato/Lato-Bold.ttf b/app/assets/fonts/lato/Lato-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/lato/Lato-BoldItalic.ttf b/app/assets/fonts/lato/Lato-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/lato/Lato-Italic.ttf b/app/assets/fonts/lato/Lato-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/lato/Lato-Light.ttf b/app/assets/fonts/lato/Lato-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/lato/Lato-LightItalic.ttf b/app/assets/fonts/lato/Lato-LightItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/lato/Lato-Regular.ttf b/app/assets/fonts/lato/Lato-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/lato/Lato-Thin.ttf b/app/assets/fonts/lato/Lato-Thin.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/lato/Lato-ThinItalic.ttf b/app/assets/fonts/lato/Lato-ThinItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/lato/OFL.txt b/app/assets/fonts/lato/OFL.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/OFL.txt b/app/assets/fonts/rokkitt/OFL.txt new file mode 100644 index 0000000000000000000000000000000000000000..28dcbea4c24a053192736af9ccb6916717add176 --- /dev/null +++ b/app/assets/fonts/rokkitt/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Rokkit Project Authors (https://github.com/googlefonts/RokkittFont) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/assets/fonts/rokkitt/README.txt b/app/assets/fonts/rokkitt/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..7d629dc3a58aff08a64102ee3353e053c257903e --- /dev/null +++ b/app/assets/fonts/rokkitt/README.txt @@ -0,0 +1,81 @@ +Rokkitt Variable Font +===================== + +This download contains Rokkitt as both variable fonts and static fonts. + +Rokkitt is a variable font with this axis: + wght + +This means all the styles are contained in these files: + Rokkitt-VariableFont_wght.ttf + Rokkitt-Italic-VariableFont_wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Rokkitt: + static/Rokkitt-Thin.ttf + static/Rokkitt-ExtraLight.ttf + static/Rokkitt-Light.ttf + static/Rokkitt-Regular.ttf + static/Rokkitt-Medium.ttf + static/Rokkitt-SemiBold.ttf + static/Rokkitt-Bold.ttf + static/Rokkitt-ExtraBold.ttf + static/Rokkitt-Black.ttf + static/Rokkitt-ThinItalic.ttf + static/Rokkitt-ExtraLightItalic.ttf + static/Rokkitt-LightItalic.ttf + static/Rokkitt-Italic.ttf + static/Rokkitt-MediumItalic.ttf + static/Rokkitt-SemiBoldItalic.ttf + static/Rokkitt-BoldItalic.ttf + static/Rokkitt-ExtraBoldItalic.ttf + static/Rokkitt-BlackItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/app/assets/fonts/rokkitt/Rokkitt-Italic-VariableFont_wght.ttf b/app/assets/fonts/rokkitt/Rokkitt-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/Rokkitt-VariableFont_wght.ttf b/app/assets/fonts/rokkitt/Rokkitt-VariableFont_wght.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-Black.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-Black.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-BlackItalic.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-BlackItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-Bold.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-BoldItalic.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-ExtraBold.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-ExtraBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-ExtraBoldItalic.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-ExtraBoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-ExtraLight.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-ExtraLight.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-ExtraLightItalic.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-ExtraLightItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-Italic.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-Light.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-LightItalic.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-LightItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-Medium.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-MediumItalic.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-MediumItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-Regular.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-SemiBold.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-SemiBoldItalic.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-SemiBoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-Thin.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-Thin.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/assets/fonts/rokkitt/static/Rokkitt-ThinItalic.ttf b/app/assets/fonts/rokkitt/static/Rokkitt-ThinItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/lib/main.dart b/app/lib/main.dart index c564513ea293ae60fdfb14f8894073b8b9eef99f..506c846b8a8c16abc209e8883f05e57c1e5c5997 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,219 +1,17 @@ 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'; - -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(const MyApp()); } -class App extends StatelessWidget { - const App({super.key}); +class MyApp extends StatelessWidget { + const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( - home: DefaultPage(), - ); - } -} - -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), - ], - ), - ), - ), + home: const DefaultPage(), ); } } diff --git a/app/lib/pages/consts.dart b/app/lib/pages/consts.dart new file mode 100644 index 0000000000000000000000000000000000000000..c1e12101b57db4fd10d403c9067b1c0b0b099682 --- /dev/null +++ b/app/lib/pages/consts.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:latlong2/latlong.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 center +LatLng mapCenter = LatLng(60.7666, 10.8471); + +// Font variables +const textColor = Colors.white; +final appTitleStyle = GoogleFonts.dmSans( + fontSize: 35, + color: Colors.black, + fontWeight: FontWeight.bold, // Add this line to make the text bold +); +final titleStyle = GoogleFonts.dmSans( + fontSize: 35, + color: textColor, + fontWeight: FontWeight.bold, // Add this line to make the text bold +); +final regTextStyle = GoogleFonts.dmSans(fontSize: 20, color: textColor); +final chartTextStyle = GoogleFonts.dmSans(fontSize: 14, color: textColor); + +// Colors +const mediumBlue = Color(0xFF015E8F); +const darkBlue = 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/pages/default_page.dart b/app/lib/pages/default_page.dart new file mode 100644 index 0000000000000000000000000000000000000000..433d223fdc2e2e9ce4980e572df55315a5965e9c --- /dev/null +++ b/app/lib/pages/default_page.dart @@ -0,0 +1,96 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'widgets/map_widget.dart'; +import 'marker_handler/marker_data.dart'; +import 'consts.dart'; +import 'marker_handler/get_markers.dart'; + +class DefaultPage extends StatefulWidget { + const DefaultPage({super.key}); + + @override + _DefaultPageState createState() => _DefaultPageState(); +} + +class _DefaultPageState extends State<DefaultPage> { + late Timer _timer; + + List<Measurement> markerList = []; + + // Call fetchMarkerTemplate and await its result before setting the state + Future<void> loadMarkerList() async { + try { + List<Measurement> fetchedMarkers = await fetchMarkerData(); + + setState(() { + markerList = fetchedMarkers; + }); + } catch (e) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text("Error"), + content: Text(e.toString()), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text("OK"), + ), + ], + ); + }, + ); + } + } + + // State initializer + @override + void initState() { + super.initState(); + // Load marker data from server + loadMarkerList(); + + // Schedule fetchMarkerData to run periodically based on fetchInterval from consts + const Duration interval = Duration(minutes: fetchInterval); + _timer = Timer.periodic(interval, (timer) { + fetchMarkerData(); + }); + } + + // Fetch timer + @override + void dispose() { + // Cancel timer on widget termination + _timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Container( + decoration: const BoxDecoration( // Set background color + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [darkBlue, darkestBlue], + ), + ), + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + title: const Text('IceMap'), + ), + body: ListView( + children: [ // Add main widget + MapContainerWidget(markerList: markerList), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/app/lib/pages/marker_handler/get_markers.dart b/app/lib/pages/marker_handler/get_markers.dart new file mode 100644 index 0000000000000000000000000000000000000000..ebc50274eef2ba7278b48028a13a2132150a0b79 --- /dev/null +++ b/app/lib/pages/marker_handler/get_markers.dart @@ -0,0 +1,31 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import '../consts.dart'; +import 'marker_data.dart'; + +// fetchMarkerTemplate requests all marker data from the server +Future<List<Measurement>> fetchMarkerData() async { + try { + HttpClient client = HttpClient() + ..badCertificateCallback = (X509Certificate cert, String host, int port) => true; + + var request = await client.getUrl(Uri.parse(serverURI + mapEndpoint)); + var response = await request.close(); + + if (response.statusCode == 200) { + var responseBody = await response.transform(utf8.decoder).join(); + + List<dynamic> jsonData = json.decode(responseBody); + + return jsonData.map((data) => Measurement.Measurement(data)).toList(); + + } else { + print('Request failed with status: ${response.statusCode}'); + throw Exception('Failed to parse marker data'); + } + } catch (e) { + print('Error: $e'); + throw Exception('failed to connect to the server. Please check your network connection'); + } +} diff --git a/app/lib/pages/marker_handler/marker_data.dart b/app/lib/pages/marker_handler/marker_data.dart new file mode 100644 index 0000000000000000000000000000000000000000..fe0d4060750cbceda42eac2f964a07d34482cec9 --- /dev/null +++ b/app/lib/pages/marker_handler/marker_data.dart @@ -0,0 +1,101 @@ +class Measurement { + int measurementID; + int timeMeasured; + Sensor sensor; + List<Data> dataList; + List<Corner> cornerList; + String bodyOfWater; + + Measurement({ + required this.measurementID, + required this.timeMeasured, + required this.sensor, + required this.dataList, + required this.cornerList, + required this.bodyOfWater, + }); + + factory Measurement.Measurement(Map<String, dynamic> json) { + return Measurement( + measurementID: json['MeasurementID'], + timeMeasured: json['TimeMeasured'], + sensor: Sensor.fromJson(json['Sensor']), + dataList: (json['Data'] as List<dynamic>) + .map((data) => Data.fromJson(data)) + .toList(), + cornerList: (json['Corner'] as List<dynamic>) + .map((data) => Corner.fromJson(data)) + .toList(), + bodyOfWater: json['WaterBodyName'], + ); + } +} + +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'], + active: json['Active'], + ); + } +} + +class Data { + double latitude; + double longitude; + double iceTop; + double iceBottom; + double calculatedThickness; + double accuracy; + + Data({ + required this.latitude, + required this.longitude, + required this.iceTop, + required this.iceBottom, + required this.calculatedThickness, + required this.accuracy, + }); + + factory Data.fromJson(Map<String, dynamic> json) { + return Data( + latitude: json['Latitude'], + longitude: json['Longitude'], + iceTop: json['IceTop'], + iceBottom: json['IceBottom'], + calculatedThickness: json['CalculatedThickness'], + accuracy: json['Accuracy'], + ); + } +} + +class Corner { + double cornerID; + double latitude; + double longitude; + + Corner({ + required this.cornerID, + required this.latitude, + required this.longitude, + }); + + factory Corner.fromJson(Map<String, dynamic> json) { + return Corner( + cornerID: json['CornerID'], + latitude: json['CornerLatitude'], + longitude: json['CornerLongitude'], + ); + } +} \ No newline at end of file diff --git a/app/lib/pages/widgets/map_widget.dart b/app/lib/pages/widgets/map_widget.dart new file mode 100644 index 0000000000000000000000000000000000000000..dc92f6de4dedc21ec21b654b1a911fab13e9a952 --- /dev/null +++ b/app/lib/pages/widgets/map_widget.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import '../marker_handler/marker_data.dart'; +import '../consts.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'quick_view_chart.dart'; +import 'stat_charts.dart'; + +class MapContainerWidget extends StatefulWidget { + final List<Measurement> markerList; + + const MapContainerWidget({Key? key, required this.markerList}) : super(key: key); + + @override + _MapContainerWidgetState createState() => _MapContainerWidgetState(); +} + +class _MapContainerWidgetState extends State<MapContainerWidget> { + + Measurement? selectedMarker; + bool isMinimized = true; // Quick view box state tacker + + @override + Widget build(BuildContext context) { + + 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), + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Stack( // Stack quick view on top of map + children: [ + SizedBox( + width: screenWidth * boxWidth, + height: screenWidth * boxHeight, + child: FlutterMap( + options: MapOptions( + center: mapCenter, // From consts + zoom: 9.0, + ), + children: [ + TileLayer( // Map from OpenStreetMap + urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + subdomains: const ['a', 'b', 'c'], + ), + PolygonLayer( // Map each element in markerList to Measurement object + polygons: widget.markerList.map((Measurement measurement) { + return Polygon( + points: measurement.cornerList.map((Corner corner) { + // Map corners to LatLng objects + return LatLng(corner.latitude, corner.longitude); + }).toList(), + /*onTap: () { + setState(() { + selectedMarker = measurement; + }); + },*/ + color: Colors.blue, + isFilled: true, + ); + }).toList(), + ) + ], + ), + ), + 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( // Content only visible when box is maximized + visible: !isMinimized && selectedMarker != 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(), + ), + ), + ), + ), + 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), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: contPadding), // Padding between containers + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Container( + 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, + ), + Text( + 'Time of measurement: ${selectedMarker?.timeMeasured}', + style: regTextStyle, + ), + Text( + 'Location: (placeholder, placeholder)', + style: regTextStyle, + ), + const SizedBox(height: contPadding), + const StatCharts(), + ], + ), + ), + ), + ), + ), + ], + ); + }, + ); + } +} \ No newline at end of file diff --git a/app/lib/pages/widgets/quick_view_chart.dart b/app/lib/pages/widgets/quick_view_chart.dart new file mode 100644 index 0000000000000000000000000000000000000000..51dfa4f1dfed1e279fc2f76e427a595f5c6f3742 --- /dev/null +++ b/app/lib/pages/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/pages/widgets/stat_charts.dart b/app/lib/pages/widgets/stat_charts.dart new file mode 100644 index 0000000000000000000000000000000000000000..da6039af4b4b5709334fd7864038b8f5021e47ca --- /dev/null +++ b/app/lib/pages/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), // Add appropriate padding between charts + SizedBox( + width: MediaQuery.of(context).size.width * 0.8, // Adjust width as needed + height: 160, // Adjust height as needed + 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), // Example width + ], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(y: 10, width: 10), // Example width + ], + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(y: 18, width: 10), // Example width + ], + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817a52206e8a8eef501faed292993ff21a31..e777c67df2219fce2c33861bf98710f86bfc79b3 100644 --- a/app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/app/pubspec.lock b/app/pubspec.lock index 29ec17a46d8598260b9d81c6ec4ef924f60898a6..98e04063f8e444085494de5d6a636401f764f4cd 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -41,6 +41,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + 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 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + 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 @@ -75,6 +107,14 @@ packages: description: flutter source: sdk version: "0.0.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: "6b6f10f0ce3c42f6552d1c70d2c28d764cf22bb487f50f66cca31dcd5194f4d6" + url: "https://pub.dev" + source: hosted + version: "4.0.4" http: dependency: "direct main" description: @@ -155,6 +195,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 +211,94 @@ 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" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + pedantic: + dependency: transitive + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" polylabel: dependency: transitive description: @@ -179,6 +315,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 @@ -272,6 +416,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + url: "https://pub.dev" + source: hosted + version: "5.2.0" wkt_parser: dependency: transitive description: @@ -280,6 +432,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 040d821ed00ded67b57e9ba1601874cde445f8e2..804a5c15088dae9fee3ee156fa422909328c5ec6 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -12,6 +12,9 @@ dependencies: flutter_map: ^4.0.0 http: ^0.13.3 latlong2: ^0.8.2 + provider: ^5.0.0 + fl_chart: ^0.20.0-nullsafety1 + google_fonts: any @@ -23,5 +26,4 @@ dev_dependencies: flutter: uses-material-design: true assets: - - assets/icons/circle-red.png - + - assets/icons/ diff --git a/server/APIs/__pycache__/get_weather.cpython-311.pyc b/server/APIs/__pycache__/get_weather.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c48ae510b8dcd144dfcf8a877ec949cbd66f1107 Binary files /dev/null and b/server/APIs/__pycache__/get_weather.cpython-311.pyc differ diff --git a/server/APIs/get_weather.py b/server/APIs/get_weather.py new file mode 100644 index 0000000000000000000000000000000000000000..89b9997a94ffd9d012c0ace27371e1f64cf7eebb --- /dev/null +++ b/server/APIs/get_weather.py @@ -0,0 +1,90 @@ +from flask import request +import requests +import datetime +import json + + +# get_weather retrieves weather data for a list of coordinate pairs +def get_weather(self): + # Extract coordinates form json data in POST request + try: + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + request_data = json.loads(post_data.decode('utf-8')) + coordinates = request_data['coords'] + + except KeyError: # Coords key not found in request + self.send_response(400) + self.send_header("Content-type", "application/json") + self.end_headers() + response_message = {"error": "No 'coords' provided in request body"} + self.wfile.write(json.dumps(response_message).encode('utf-8')) + return + + except Exception as e: + self.send_response(500) + self.send_header("Content-type", "application/json") + self.end_headers() + response_message = {"error": "Failed to parse request body"} + self.wfile.write(json.dumps(response_message).encode('utf-8')) + return + + # Form timestamp string with correct format + # Form timestamp string with correct format + current_time = datetime.datetime.utcnow() + current_time = current_time.replace(minute=0, second=0) + current_time_str = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") + + # Set User-Agent header + user_agent = "IceApp sarasdj@stud.ntnu.no" + headers = {'User-Agent': user_agent} + + # Empty list for weather data + weather_data = [] + status_code = 500 + + # Request weather data for each coordinate pair + for coord in coordinates: + lat = round(coord.get('lat'), 4) # YR API only allows for up to 4 decimal points + lng = round(coord.get('lng'), 4) + + # Construct request string and execute get request to Yr API + url = f"https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={lat}&lon={lng}" + response = requests.get(url, headers=headers) + + if response.status_code == 200: + if status_code != 200: + status_code = 200 + + data = response.json() # Extract data from response + timeseries = data['properties']['timeseries'] + + # Get data for current time and append to weather_data + for entry in timeseries: + if entry['time'] == current_time_str: + data = entry['data']['instant']['details'] + temperature = data['air_temperature'] + humidity = data['relative_humidity'] + + weather_object = { + 'Latitude': lat, + 'Longitude': lng, + 'Temperature': temperature, + 'Humidity': humidity + } + # Append weather_object to weather_data list + weather_data.append(weather_object) + break + + + else: # Add error message if no weather data is found or the request fails + print(f"Request to weather API failed with status code {response.status_code}") #NB: append error message to weather_data + status_code = response.status_code + + # Set headers + self.send_response(status_code) + self.send_header("Content-type", "application/json") + self.end_headers() + + # Write weather_data to response object + self.wfile.write(json.dumps(weather_data).encode('utf-8')) diff --git a/server/__pycache__/consts.cpython-311.pyc b/server/__pycache__/consts.cpython-311.pyc index 9ff52d124944e7e6b5fd6348a75357356a3572d9..5c3188f8a9d95c5a2766585e72bbb8895f196618 100644 Binary files a/server/__pycache__/consts.cpython-311.pyc and b/server/__pycache__/consts.cpython-311.pyc differ diff --git a/server/__pycache__/data_structs.cpython-311.pyc b/server/__pycache__/data_structs.cpython-311.pyc index 4163607b4b1b56fb78c7310f4a0863892bb42ce3..928637c84a08e29d4863306f8637968ec21e14cb 100644 Binary files a/server/__pycache__/data_structs.cpython-311.pyc and b/server/__pycache__/data_structs.cpython-311.pyc differ diff --git a/server/consts.py b/server/consts.py index 368377575548ab79ed9c639cb2071523ddff0cb2..295463e89cc15ecc1d0ae51330ff0b7bd6ae44a6 100644 --- a/server/consts.py +++ b/server/consts.py @@ -6,7 +6,8 @@ PORT = 8443 # Database paths DB_NAME = 'IceMapDB' -COLLECTION = 'IceData' +#COLLECTION = 'IceData' +COLLECTION = 'TestCollection' # NB: temporary collection MONGO_URI = "mongodb+srv://icemapcluster.i02epob.mongodb.net/?authSource=%24external&authMechanism=MONGODB-X509&retryWrites=true&w=majority" # Certificate paths diff --git a/server/data_structs.py b/server/data_structs.py index 26428d4f6e4fb7d187ca34b003692c297a33d184..b5c4cd8d363936270e52277e9de3b903d22f8b56 100644 --- a/server/data_structs.py +++ b/server/data_structs.py @@ -1,4 +1,3 @@ - # Sensor contains data related to a single sensor class Sensor: def __init__(self, ID: int, type: str, active: bool): @@ -12,7 +11,8 @@ class Sensor: 'type': self.type, 'active': self.active } - + + # DateTime contains the date and time for a measurement class DateAndTime: def __init__(self, year: int, month: int, day: int, hour: int, minute: int): @@ -31,11 +31,13 @@ class DateAndTime: 'minute': self.minute } + # Measurement contains geo-data related to a single measurement point at a given time. It includes an instance # of the class Sensor. class Measurement: - def __init__(self, longitude: float, latitude: float, datetime: DateAndTime, sensor: Sensor, precipitation: float, thickness: float, - max_weight: float, safety_level: float, accuracy: float): + def __init__(self, longitude: float, latitude: float, datetime: DateAndTime, sensor: Sensor, precipitation: float, + thickness: float, + max_weight: float, safety_level: float, accuracy: float): self.longitude = longitude self.latitude = latitude self.datetime = datetime @@ -45,7 +47,7 @@ class Measurement: self.max_weight = max_weight self.safety_level = safety_level self.accuracy = accuracy - + def to_dict(self): return { 'longitude': self.longitude, @@ -58,12 +60,13 @@ class Measurement: 'safety_level': self.safety_level, 'accuracy': self.accuracy } - + + # MarkerTemplate is a template for map marker data. It includes an instance of the # DataPoint type. class MarkerTemplate: def __init__(self, geoData: Measurement, size: float, color: str): - self.geoData = geoData + self.geoData = geoData self.longitude = geoData.longitude self.latitude = geoData.latitude self.size = size @@ -71,9 +74,9 @@ class MarkerTemplate: def to_dict(self): return { - 'geo_data': self.geoData.to_dict(), + 'geo_data': self.geoData.to_dict(), 'latitude': self.latitude, 'longitude': self.longitude, 'size': self.size, 'color': self.color, - } \ No newline at end of file + } diff --git a/server/main.py b/server/main.py index ace59dd7c48a455f0c2d901fed9741c822d006d0..47d94aadead3156f2108379051b2eed001b05dab 100644 --- a/server/main.py +++ b/server/main.py @@ -1,82 +1,85 @@ -from flask import Flask, jsonify +from flask import Flask from http.server import HTTPServer, BaseHTTPRequestHandler -from pymongo import MongoClient -from pymongo.server_api import ServerApi -from consts import DB_NAME, COLLECTION, MONGO_URI, MONGO_CERT_PATH, SSL_CERT_PATH, SSL_KEY_PATH, HOST, PORT -from map.get_markers import get_markers -import atexit +from consts import SSL_CERT_PATH, SSL_KEY_PATH, HOST, PORT +from map.get_markers import get_all_markers +from APIs.get_weather import get_weather import ssl import keyboard +import sqlite3 app = Flask(__name__) terminate_server = 0 -""" -# Initialise MongoDB connection -try: - client = MongoClient(MONGO_URI, - tls=True, - tlsCertificateKeyFile=MONGO_CERT_PATH, - server_api=ServerApi('1')) - - db = client[DB_NAME] - collection = db[COLLECTION] - print("Connected to MongoDB") -except Exception as e: - print(f"Failed to connect to MongoDB: {e}") -""" - -# Define HTTP class +class IceHTTPServer(HTTPServer): + def __init__(self, server_address, handler_class, cursor): + super().__init__(server_address, handler_class) + self.cursor = cursor + + def get_request(self): + request, client_address = super().get_request() + return request, client_address + + +# Custom HTTP class class IceHTTP(BaseHTTPRequestHandler): + def __init__(self, request, client_address, server): + self.cursor = server.cursor + super().__init__(request, client_address, server) + def do_GET(self): - if self.path == '/': + # Root path + if self.path == '/': # NB: temporary root path behavior self.send_response(200) self.send_header("Content-type", "text/plain") self.end_headers() self.wfile.write(b"Root path hit!") - # Update_map endpoint - elif self.path == '/update_map': # NB: should be POST? - # Fetch marker data - markers_data, resp_code = get_markers() + elif self.path == '/update_map': # NB: should be POST? + get_all_markers(self, self.cursor, False) # Get all markers - # Set headers - self.send_response(resp_code) - self.send_header("Content-type", "application/json") - self.end_headers() + elif self.path == '/get_valid_markers': # NB: should be POST? + get_all_markers(self, self.cursor, True) # Get only valid markers - # Write the JSON data to response object - self.wfile.write(str(markers_data).encode('utf-8')) + def do_POST(self): + if self.path == '/get_weather_data': + get_weather(self) -# Listen for pressing of q key to terminate server -def on_key_press(server, event): +# Terminate server on key press q +def on_key_press(server, event, cursor, conn): if event.name == 'q': print('Terminating server...') server.server_close() + cursor.close() + conn.close() keyboard.unhook_all() quit() # Start a server on port 8443 using self defined HTTP class if __name__ == "__main__": - print("Starting server on port ", PORT) try: + # Initialize database connection + conn = sqlite3.connect('server/sql_db/icedb') + cursor = conn.cursor() + # Load SSL certificate and private key ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ssl_context.load_cert_chain(SSL_CERT_PATH, SSL_KEY_PATH) # Create HTTP server with SSL support - server = HTTPServer((HOST, PORT), IceHTTP) + server = IceHTTPServer((HOST, PORT), IceHTTP, cursor) server.socket = ssl_context.wrap_socket(server.socket, server_side=True) print("Server running on port ", PORT) # Register key press event handler - keyboard.on_press(lambda event: on_key_press(server, event)) + keyboard.on_press(lambda event: on_key_press(server, event, cursor, conn)) + + print("Server running on port ", PORT) # Run server indefinitely server.serve_forever() except Exception as e: - print(f"Failed to start server on port {PORT}: {e}") + print(f"Server terminated: {PORT}: {e}") diff --git a/server/map/__pycache__/get_markers.cpython-311.pyc b/server/map/__pycache__/get_markers.cpython-311.pyc index fe067d8d451136985eb529449e19ff358e97b22b..46facb818c457bb01157ffbd7d0c285473aad2e6 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 fe91e6ce085b31779cc0e259d0f4d2aeebac32d8..45f8965e5a9779ac3539d375232ff5b5f3dfa8c6 100644 --- a/server/map/get_markers.py +++ b/server/map/get_markers.py @@ -1,38 +1,104 @@ -import os -import sys -current_dir = os.path.dirname(__file__) -parent_dir = os.path.abspath(os.path.join(current_dir, '..')) -sys.path.append(parent_dir) -from data_structs import Measurement, Sensor, MarkerTemplate, DateAndTime -from flask import json - -# get_markers parses a list of MarkerTemplate objects to json, and returns either a successfully -# parsed json object with status code 200, or and error message and status code 501 -def get_markers(): +import json + + +# get_markers requests all marker data or valid markers, converts the data to json, and writes +# the data to the response object +def get_all_markers(self, cursor, valid: bool): try: - # NB: temporary test data - sensor1 = Sensor(ID=1, type="Type1", active=True) - sensor2 = Sensor(ID=2, type="Type2", active=False) - - datetime1 = DateAndTime(2023, 12, 31, 15, 43) - datetime2 = DateAndTime(2024, 1, 15, 12, 2) - datetime3 = DateAndTime(2024, 1, 31, 18, 10) - - measurement1 = Measurement(longitude=10.9771, latitude=60.7066, datetime=datetime1, sensor=sensor1, - precipitation=0.0, thickness=0.0, max_weight=0.0, safety_level=0.0, accuracy=2.5) - measurement2 = Measurement(longitude=10.8171, latitude=60.6366, datetime=datetime2, sensor=sensor2, - precipitation=0.0, thickness=0.0, max_weight=0.0, safety_level=0.0, accuracy=1.5) - measurement3 = Measurement(longitude=10.8471, latitude=60.7366, datetime=datetime3, sensor=sensor1, - precipitation=0.0, thickness=0.0, max_weight=0.0, safety_level=0.0, accuracy=4.0) - - testData = [ - MarkerTemplate(measurement1, 30.0-measurement1.accuracy, "Green"), - MarkerTemplate(measurement2, 10.0-measurement2.accuracy, "Red"), - MarkerTemplate(measurement3, 20.0-measurement3.accuracy, "Yellow"), - ] - - # NB: return test data as JSON - return json.dumps([marker.to_dict() for marker in testData]), 200 - + # NB: interval temporarily hard coded to 5 days + if valid: + cursor.execute(''' + SELECT m.MeasurementID, m.SensorID, m.TimeMeasured, d.Latitude, d.Longitude, + d.IceTop, d.IceBottom, d.CalculatedThickness, d.Accuracy, s.SensorType, s.Active, + c.CornerID, c.CornerLatitude, c.CornerLongitude, b.Name + FROM Measurement m + INNER JOIN Sensor s ON m.SensorID = s.SensorID + INNER JOIN Data d ON m.MeasurementID = d.MeasurementID + LEFT JOIN Corner c ON m.MeasurementID = c.MeasurementID + INNER JOIN BodyOfWater b ON m.WaterBodyName = b.Name + WHERE m.TimeMeasured > DATE_SUB(NOW(), INTERVAL 5 DAY); + ''') + else: + cursor.execute(''' + SELECT m.MeasurementID, m.SensorID, m.TimeMeasured, d.Latitude, d.Longitude, + d.IceTop, d.IceBottom, d.CalculatedThickness, d.Accuracy, s.SensorType, s.Active, + c.CornerID, c.CornerLatitude, c.CornerLongitude, b.Name + FROM Measurement m + INNER JOIN Sensor s ON m.SensorID = s.SensorID + INNER JOIN Data d ON m.MeasurementID = d.MeasurementID + LEFT JOIN Corner c ON m.MeasurementID = c.MeasurementID + INNER JOIN BodyOfWater b ON m.WaterBodyName = b.Name + ''') + + rows = cursor.fetchall() + + measurement_data = {} + + # Iterate over the fetched rows + for row in rows: + measurement_id = row[0] + + # Create a data object for current row + data_object = { + 'Latitude': row[3], + 'Longitude': row[4], + 'IceTop': row[5], + 'IceBottom': row[6], + 'CalculatedThickness': row[7], + 'Accuracy': row[8] + } + + # Create a corner object for current row + corner_object = { + 'Latitude': row[12], + 'Longitude': row[13] + } + + # Check if measurement ID already exists in measurement_data + if measurement_id in measurement_data: + # Check if the data object already exists in the list + if data_object not in measurement_data[measurement_id]['Data']: + measurement_data[measurement_id]['Data'].append(data_object) + + # Check if the corner object already exists in the list + if corner_object not in measurement_data[measurement_id]['Corners']: + measurement_data[measurement_id]['Corners'].append(corner_object) + else: + # Create a new entry for measurement_id if it does not already exist in the list + measurement_data[measurement_id] = { + 'MeasurementID': measurement_id, + 'TimeMeasured': row[2], + 'Sensor': { + 'SensorID': row[1], + 'SensorType': row[9], + 'Active': bool(row[10]) + }, + 'Data': [data_object], + 'Corners': [corner_object] + } + + # Convert dictionary values to list of measurements + data = list(measurement_data.values()) + + if len(rows) == 0 or len(data) == 0: # Return 500 and empty list if no data is found + print(f"An error occurred while querying the database") + resp_code = 500 + marker_data = '[]' + else: + resp_code = 200 + # Convert list of dictionaries to JSON + marker_data = json.dumps(data, indent=4) + except Exception as e: - return e, 500 \ No newline at end of file + print(f"An error occurred while querying the database: {e}") + resp_code = 500 + marker_data = '[]' + + # Set headers + self.send_response(resp_code) + self.send_header("Content-type", "application/json") + self.end_headers() + + # Write marker data to response object + self.wfile.write(marker_data.encode('utf-8')) + diff --git a/server/sql_db/icedb b/server/sql_db/icedb new file mode 100644 index 0000000000000000000000000000000000000000..e01a5712975ae037329fb433f502db93106da42e Binary files /dev/null and b/server/sql_db/icedb differ diff --git a/server/sql_db/schema.sql b/server/sql_db/schema.sql new file mode 100644 index 0000000000000000000000000000000000000000..3ac83acef029146dd4341548e86ee161273aeda3 --- /dev/null +++ b/server/sql_db/schema.sql @@ -0,0 +1,96 @@ +CREATE TABLE Measurement ( + MeasurementID INT PRIMARY KEY, + SensorID INT, + TimeMeasured DATETIME, + WaterBodyName TEXT, + FOREIGN KEY (SensorID) REFERENCES Sensor(SensorID), + FOREIGN KEY (WaterBodyName) REFERENCES BodyOfWater(Name) +); + +CREATE TABLE Sensor ( + SensorID INTEGER PRIMARY KEY, + SensorType TEXT CHECK(SensorType IN ('LiDar', 'Magnetic', 'Other')), + Active BOOLEAN +); + +CREATE TABLE Data ( + MeasurementID INT, + Latitude FLOAT, + Longitude FLOAT, + IceTop FLOAT, + IceBottom FLOAT, + CalculatedThickness FLOAT, + Accuracy FLOAT, + PRIMARY KEY (MeasurementID, Longitude, Latitude), + FOREIGN KEY (MeasurementID) REFERENCES Measurement(MeasurementID) +); + +CREATE TABLE Corner ( + CornerID INT, + MeasurementID INT, + CornerLatitude FLOAT, + CornerLongitude FLOAT, + PRIMARY KEY (CornerID, MeasurementID), + FOREIGN KEY (MeasurementID) REFERENCES Measurement(MeasurementID) +); + +CREATE TABLE BodyOfWater ( + Name TEXT PRIMARY KEY +); + +INSERT INTO Sensor (SensorID, SensorType, Active) VALUES +(1, 'LiDar', 1), +(2, 'Magnetic', 1), +(3, 'Other', 0); + +INSERT INTO Measurement (MeasurementID, SensorID, TimeMeasured, WaterBodyName) VALUES +(1, 2, '2024-01-01 10:00:00', 'Mjosa'), +(2, 2, '2024-02-04 11:00:00', 'Mjosa'), +(3, 1, '2024-02-13 12:00:00', 'Mjosa'); + +-- Measurement 1 +INSERT INTO Data (MeasurementID, Latitude, Longitude, IceTop, IceBottom, CalculatedThickness, Accuracy) VALUES +(1, 60.7070, 10.9771, 8.0, 3.0, 5.0, 1.0), +(1, 60.7066, 10.9772, 7.5, 2.5, 5.0, 1.0), +(1, 60.7067, 10.9773, 7.5, 2.5, 5.0, 1.0), +(1, 60.7062, 10.9774, 7.5, 2.5, 5.0, 1.0), +(1, 60.7067, 10.9775, 7.0, 2.0, 5.0, 1.0); + +INSERT INTO Corner (CornerID, MeasurementID, CornerLatitude, CornerLongitude) VALUES +(1, 1, 60.7060, 10.9770), +(2, 1, 60.7061, 10.9771), +(3, 1, 60.7062, 10.9772), +(4, 1, 60.7063, 10.9773); + +-- Measurement 2 +INSERT INTO Data (MeasurementID, Latitude, Longitude, IceTop, IceBottom, CalculatedThickness, Accuracy) VALUES +(2, 60.6366, 10.8171, 7.2, 2.2, 5.0, 1.5), +(2, 60.6367, 10.8172, 7.0, 2.0, 5.0, 1.5), +(2, 60.6368, 10.8172, 7.1, 2.1, 5.0, 1.5), +(2, 60.6369, 10.8172, 7.3, 2.3, 5.0, 1.5), +(2, 60.6367, 10.8173, 7.4, 2.4, 5.0, 1.5), +(2, 60.6367, 10.8179, 7.5, 2.5, 5.0, 1.5); + +INSERT INTO Corner (CornerID, MeasurementID, CornerLatitude, CornerLongitude) VALUES +(1, 2, 60.6360, 10.8170), +(2, 2, 60.6361, 10.8171), +(3, 2, 60.6362, 10.8172), +(4, 2, 60.6363, 10.8173); + +-- Measurement 3 +INSERT INTO Data (MeasurementID, Latitude, Longitude, IceTop, IceBottom, CalculatedThickness, Accuracy) VALUES +(3, 60.7366, 10.8471, 7.5, 2.5, 5.0, 2.5), +(3, 60.7369, 10.8471, 7.4, 2.4, 5.0, 2.5), +(3, 60.7367, 10.8480, 7.3, 2.3, 5.0, 2.5), +(3, 60.7368, 10.8481, 7.2, 2.2, 5.0, 2.5), +(3, 60.7370, 10.8475, 7.1, 2.1, 5.0, 2.5); + +INSERT INTO Corner (CornerID, MeasurementID, CornerLatitude, CornerLongitude) VALUES +(1, 3, 60.7360, 10.8470), +(2, 3, 60.7361, 10.8471), +(3, 3, 60.7362, 10.8472), +(4, 3, 60.7363, 10.8473); + +INSERT INTO BodyOfWater(Name) VALUES +('Mjosa'); +