diff --git a/app/lib/pages/marker_handler/get_markers.dart b/app/lib/pages/marker_handler/get_markers.dart index 702d41458a19f878d878bf46469e20481ed4db8d..156de62c0f5572d6bce55bec980c76844cf06086 100644 --- a/app/lib/pages/marker_handler/get_markers.dart +++ b/app/lib/pages/marker_handler/get_markers.dart @@ -12,7 +12,7 @@ Future<List<Measurement>> fetchMarkerData() async { ..badCertificateCallback = // NB: temporary disable SSL certificate validation (X509Certificate cert, String host, int port) => true; - // Request markers from API + // 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 @@ -23,8 +23,11 @@ Future<List<Measurement>> fetchMarkerData() async { if (responseBody.isNotEmpty) { var jsonData = json.decode(responseBody); - if (jsonData != null && jsonData is List) { // Check if jsonData is not null and is a List - return jsonData.map((data) => Measurement.Measurement(data)).toList(); + // Attempt to parse response to Measurement object only if the body + // contains correctly formatted data + if (jsonData != null && jsonData is List) { + print(jsonData.map((data) => Measurement.fromJson(data)).toList()); + return jsonData.map((data) => Measurement.fromJson(data)).toList(); } else { throw Exception('Failed to parse marker data: Unexpected response format'); } @@ -35,7 +38,6 @@ Future<List<Measurement>> fetchMarkerData() async { throw Exception('Failed to fetch marker data: Status code ${response.statusCode}'); } } catch (e) { - print('Error fetching marker data: $e'); throw Exception('Failed to fetch marker data: ${e.toString()}'); } } diff --git a/app/lib/pages/marker_handler/marker_data.dart b/app/lib/pages/marker_handler/marker_data.dart index fd466ed890e293cb2f3a4cffeea9edbfe47b3e77..e84c14160deffc06dfd71ac1d3521d53b635c3e2 100644 --- a/app/lib/pages/marker_handler/marker_data.dart +++ b/app/lib/pages/marker_handler/marker_data.dart @@ -1,43 +1,60 @@ import 'dart:core'; +import 'package:latlong2/latlong.dart'; class Measurement { int measurementID; DateTime timeMeasured; Sensor sensor; - List<Data> dataList; - List<Corner> cornerList; String bodyOfWater; + LatLng center; + List <SubDiv> subDiv; Measurement({ required this.measurementID, required this.timeMeasured, required this.sensor, - required this.dataList, - required this.cornerList, required this.bodyOfWater, + required this.center, + required this.subDiv, }); - factory Measurement.Measurement(Map<String, dynamic> json) { + factory Measurement.fromJson(Map<String, dynamic> json) { return Measurement( measurementID: json['MeasurementID'], timeMeasured: DateTime.parse(json['TimeMeasured']), sensor: Sensor.fromJson(json['Sensor']), - dataList: (json['Data'] as List<dynamic>) - .map((data) => Data.fromJson(data)) - .toList(), - cornerList: (json['Corners'] as List<dynamic>) - .map((data) => Corner.fromJson(data)) - .toList(), - bodyOfWater: json['BodyOfWater'], - /* - dataList: (json['Data'] != null && json['Data'] is List) - ? (json['Data'] as List<dynamic>).map((data) => Data.fromJson(data)).toList() - : [], - cornerList: (json['Corner'] != null && json['Corner'] is List) - ? (json['Corner'] as List<dynamic>).map((data) => Corner.fromJson(data)).toList() - : [], - bodyOfWater: json['WaterBodyName'] ?? '', - */ + bodyOfWater: json['BodyOfWater'] ?? 'nil', + center: LatLng(json['CenterLat'], json['CenterLon']), + subDiv: (json['Subdivisions'] as List<dynamic>).map((data) => SubDiv.fromJson(data)).toList(), + ); + } +} + +class SubDiv { + int subDivID; + int groupID; + double minThickness; + double avgThickness; + LatLng center; + double accuracy; + + SubDiv({ + required this.subDivID, + required this.groupID, + required this.minThickness, + required this.avgThickness, + required this.center, + required this.accuracy, + }); + + factory SubDiv.fromJson(Map<String, dynamic> json) { + return SubDiv( + subDivID: json['SubdivID'], + groupID: json['GroupID'], + minThickness: json['MinThickness'], + avgThickness: json['AvgThickness'], + center: LatLng(json['CenLatitude'], json['CenLongitude']), + accuracy: json['Accuracy'], ); } } @@ -56,57 +73,8 @@ class Sensor { factory Sensor.fromJson(Map<String, dynamic> json) { return Sensor( sensorID: json['SensorID'], - sensorType: json['SensorType'], + sensorType: json['SensorType'] ?? 'nil', 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 { - int 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['Latitude'], - longitude: json['Longitude'], - ); - } -} \ No newline at end of file diff --git a/app/lib/pages/widgets/interactive_polygon.dart b/app/lib/pages/widgets/interactive_polygon.dart new file mode 100644 index 0000000000000000000000000000000000000000..52968654d3d9eaec83169833ec8383ab5c278ec7 --- /dev/null +++ b/app/lib/pages/widgets/interactive_polygon.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +// InteractivePolygon returns a Polygon object wrapped in a GestureDetector +// in order to make the Polygon clickable +class InteractivePolygon extends StatelessWidget { + final List<Offset> points; + final VoidCallback onTap; + final Color color; + final double strokeWidth; + + InteractivePolygon({ + required this.points, + required this.onTap, + this.color = Colors.blue, + this.strokeWidth = 1.0, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: CustomPaint( + painter: PolygonPainter( + points: points, + color: color, + strokeWidth: strokeWidth, + ), + ), + ); + } +} + +// PolygonPainter takes the points, color, and stroke width from a +// object of type InteractivePolygon, and renders it +class PolygonPainter extends CustomPainter { + final List<Offset> points; + final Color color; + final double strokeWidth; + + PolygonPainter({ + required this.points, + required this.color, + required this.strokeWidth, + }); + + // paint() iterates through all the vertices of the polygon and connects + // each sequential pair with a line + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth; + + final path = Path(); + path.moveTo(points[0].dx, points[0].dy); + for (var i = 1; i < points.length; i++) { + path.lineTo(points[i].dx, points[i].dy); // Render line between vertices + } + path.close(); + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(PolygonPainter oldDelegate) { + return oldDelegate.points != points || + oldDelegate.color != color || + oldDelegate.strokeWidth != strokeWidth; + } +} \ No newline at end of file diff --git a/app/lib/pages/widgets/map_widget.dart b/app/lib/pages/widgets/map_widget.dart index ebb7521cb3626edfd9c3c06d2b3c5335c07808a7..ea3ec5811c64e396fdf84bb93dc15f2d858e598a 100644 --- a/app/lib/pages/widgets/map_widget.dart +++ b/app/lib/pages/widgets/map_widget.dart @@ -69,12 +69,8 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", subdomains: const ['a', 'b', 'c'], ), - PolygonLayer( + /*PolygonLayer( polygons: widget.markerList.map((Measurement measurement) { - // Map corners to a list of LatLng objects - List<LatLng> points = measurement.cornerList.map((Corner corner) { - return LatLng(corner.latitude, corner.longitude); - }).toList(); return Polygon( points: points, // Use list of corner coordinates to render polygon @@ -82,35 +78,14 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { isFilled: true, ); }).toList(), - ), + ),*/ MarkerLayer( markers: widget.markerList.map((Measurement measurement) { - List<LatLng> corners = measurement.cornerList.map((Corner corner) { - return LatLng(corner.latitude, corner.longitude); - }).toList(); - - // point calculates the middle point between corners - LatLng point(List<LatLng> coordinates) { - double averageLatitude = 0.0; - double averageLongitude = 0.0; - - for (LatLng point in coordinates) { - averageLatitude += point.latitude; - averageLongitude += point.longitude; - } - - // Calculate average latitude and longitude - averageLatitude /= coordinates.length; - averageLongitude /= coordinates.length; - - return LatLng(averageLatitude, averageLongitude); // Return the middle point - } - return Marker( width: 50, height: 50, - point: point(corners), + point: measurement.center, // Set markers at center of measurement builder: (ctx) => GestureDetector( onTap: () { setState(() { @@ -126,7 +101,6 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ); }).toList(), ), - ], ), ), @@ -268,7 +242,7 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ), const SizedBox(height: contPadding), Text( - 'Measuring point: (${selectedMarker?.dataList[0].latitude}, ${selectedMarker?.dataList[0].latitude})', + 'Measuring point: (${selectedMarker?.measurementID}, ${selectedMarker?.measurementID})', style: regTextStyle, ), const SizedBox(height: contPadding), diff --git a/server/__pycache__/consts.cpython-311.pyc b/server/__pycache__/consts.cpython-311.pyc index 5c3188f8a9d95c5a2766585e72bbb8895f196618..3c60371f12da04a05b4df70c07a52110b373b16c 100644 Binary files a/server/__pycache__/consts.cpython-311.pyc and b/server/__pycache__/consts.cpython-311.pyc differ diff --git a/server/consts.py b/server/consts.py index 295463e89cc15ecc1d0ae51330ff0b7bd6ae44a6..5653feea29bda21aae6d6ea43c0365f22b8a73c3 100644 --- a/server/consts.py +++ b/server/consts.py @@ -2,16 +2,12 @@ # Server variables HOST = "0.0.0.0" -PORT = 8443 - -# Database paths -DB_NAME = 'IceMapDB' -#COLLECTION = 'IceData' -COLLECTION = 'TestCollection' # NB: temporary collection -MONGO_URI = "mongodb+srv://icemapcluster.i02epob.mongodb.net/?authSource=%24external&authMechanism=MONGODB-X509&retryWrites=true&w=majority" +PORT = 8443 # Certificate paths CERT_DIR = "server/certificates/" -MONGO_CERT_PATH = CERT_DIR + "MongoCert.pem" SSL_KEY_PATH = CERT_DIR + "testKey.key" -SSL_CERT_PATH = CERT_DIR + "testCert.crt" \ No newline at end of file +SSL_CERT_PATH = CERT_DIR + "testCert.crt" + +# Measurement specs +AREA_SIZE = 20 \ No newline at end of file diff --git a/server/map/__pycache__/get_markers.cpython-311.pyc b/server/map/__pycache__/get_markers.cpython-311.pyc index 7dbac2b7ec9e41b54e8502402b73e37c0dd244fc..0936525a134986cdf0765898d85dcce201203bbb 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/divide_measurement.py b/server/map/divide_measurement.py new file mode 100644 index 0000000000000000000000000000000000000000..020819000d0b1c79bdc3a36e04a408f2518d8569 --- /dev/null +++ b/server/map/divide_measurement.py @@ -0,0 +1,24 @@ +from math import pi, cos + +EARTH = 6378.137 # Radius of the earth in kilometer +METER = (1 / ((2 * pi / 360) * EARTH)) / 1000 # 1 meter in degree +OFFSET = 20 # Offset in meters + + +def calculate_corners(lat, lng): + # From https://stackoverflow.com/questions/7477003/calculating-new-longitude-latitude-from-old-n-meters + # Formulas: + lat_pos = lat + (OFFSET * METER) + lng_pos = lng + (OFFSET * METER) / cos(lat * (pi / 180)) + + lat_neg = lat - (OFFSET * METER) + lng_neg = lng - (OFFSET * METER) / cos(lat * (pi / 180)) + + return [ + (lat_neg, lng_pos), + (lat_pos, lng_pos), + (lat_pos, lng_neg), + (lat_neg, lng_neg) + ] + +# def div_measurement(): diff --git a/server/map/get_markers.py b/server/map/get_markers.py index 48ce0673ae784d89649013246a802d65c874d6c5..2f5c6733bfe0528ce9d40441b450988994f9f76d 100644 --- a/server/map/get_markers.py +++ b/server/map/get_markers.py @@ -6,7 +6,7 @@ import json def get_all_markers(self, cursor, valid: bool, waterBodyName): try: sql_query = ''' - SELECT m.MeasurementID, m.SensorID, m.TimeMeasured, + SELECT m.MeasurementID, m.SensorID, m.TimeMeasured, m.CenterLat, m.CenterLon, s.SensorType, s.Active, b.Name, d.SubDivisionID, d.GroupID, d.MinimumThickness, @@ -35,13 +35,13 @@ def get_all_markers(self, cursor, valid: bool, waterBodyName): # Create subdivision new object sub_division = { - 'SubdivID': row[6], - 'GroupID': row[7], - 'MinThickness': row[8], - 'AvgThickness': row[9], - 'CenLatitude': row[10], - 'CenLongitude': row[11], - 'Accuracy': row[12] + 'SubdivID': row[8], + 'GroupID': row[9], + 'MinThickness': row[10], + 'AvgThickness': row[11], + 'CenLatitude': row[12], + 'CenLongitude': row[13], + 'Accuracy': row[14] } # Check if measurement ID already exists in measurement_data @@ -55,10 +55,12 @@ def get_all_markers(self, cursor, valid: bool, waterBodyName): measurement_data[measurement_id] = { 'MeasurementID': measurement_id, 'TimeMeasured': row[2], + 'CenterLat': row[3], + 'CenterLon': row[4], 'Sensor': { # Each measurement only has one related sensor 'SensorID': row[1], - 'SensorType': row[3], - 'Active': bool(row[4]) + 'SensorType': row[5], + 'Active': bool(row[6]) }, 'Subdivisions': [sub_division], # Array of sub_division objects } diff --git a/server/sql_db/icedb b/server/sql_db/icedb index b19e3e7766c83a08ad8f1b6aa7c08743c90ab02d..2ba5aeca796f334edd9df517a92df6a2d401852f 100644 Binary files a/server/sql_db/icedb and b/server/sql_db/icedb differ diff --git a/server/sql_db/schema.sql b/server/sql_db/schema.sql index 1413821693b00eaf9221756ddf08375bfbc10f61..ebc8dbaef7caa70b15387c2c7ca0c1a200ac6a0f 100644 --- a/server/sql_db/schema.sql +++ b/server/sql_db/schema.sql @@ -16,6 +16,8 @@ CREATE TABLE Measurement ( SensorID INT NOT NULL, TimeMeasured DATETIME NOT NULL, WaterBodyName TEXT NOT NULL, + CenterLat FLOAT NOT NULL, + CenterLon FLOAT NOT NULL, WholeAverageThickness FLOAT NOT NULL, FOREIGN KEY (SensorID) REFERENCES Sensor(SensorID), FOREIGN KEY (WaterBodyName) REFERENCES BodyOfWater(Name)