diff --git a/README.md b/README.md index b033a30410705a742f4498310bac635e1bce73c6..9eda7ed84ee9fa60b2a95ea27f3e64945c77bddc 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,28 @@ Precompiled binaries can be found on https://www.sqlite.org/download.html. Extra binary in a folder and note its path. Add the path to your system environment variables. Now you can manage the SQLite database. +## Adding new maps +The current server only contains the data for a single lake, Mjøsa. To add more lakes +go to https://overpass-turbo.eu/. Once you have navigated to Overpass API, enter +the Overpass query below in the left field, but swap 'lakeName' out +with the name of the lake you want to add. Once the query has been adjusted, +press the 'Run' button. + +``` +[out:json]; +( + way["natural"="water"]["name"="lakeName"]; + relation["natural"="water"]["name"="lakeName"]; +); +(._;>;); +out body; +``` +If a text box saying "This query returned quite a lot of data (approx. x MB). Your browser may have a hard time trying to render this. Do you really want to continue? +" appears, press 'continue anyway'. Double check that you have +the correct lake, then press 'Export'. In the 'Export' menu, download the shape data as +GeoJson. Once downloaded, name the file the *lakeName.json, and move the file into +IceMap/server/lake_relations. Once you have added the file, run map division... + ## Endpoints ## Bugs diff --git a/app/lib/pages/consts.dart b/app/lib/pages/consts.dart index 35b89973d96707fc9a2c6058069335c500b919f4..6b2e7ac3142f2661e6a473e202cfa02734e1f305 100644 --- a/app/lib/pages/consts.dart +++ b/app/lib/pages/consts.dart @@ -10,7 +10,7 @@ const int fetchInterval = 60; // Fetch marker data every n minutes // Map variables LatLng mapCenter = LatLng(60.7666, 10.8471); -DateTime ?lastUpdate; // Last time marker data was fetched from server +DateTime ?lastUpdate; // Last time data was fetched from server // Font variables const textColor = Colors.white; diff --git a/app/lib/pages/marker_handler/get_relation.dart b/app/lib/pages/marker_handler/get_relation.dart index 77d3297dd56438177d8327056aa7e3617c51f4a1..2a4abf5df854289596065e04fe3457657fa887a4 100644 --- a/app/lib/pages/marker_handler/get_relation.dart +++ b/app/lib/pages/marker_handler/get_relation.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import '../consts.dart'; import 'dart:typed_data'; +import 'package:path_provider/path_provider.dart'; + +import '../consts.dart'; /// Fetch relation data from server Future<Uint8List> fetchRelation() async { @@ -21,16 +23,38 @@ Future<Uint8List> fetchRelation() async { var responseBody = await response.transform(utf8.decoder).join(); if (responseBody.isNotEmpty) { + Directory appDocumentsDirectory = await getApplicationDocumentsDirectory(); + String filePath = '${appDocumentsDirectory.path}/last_relation.json'; + + try { // Write most recent time of update to file + await File(filePath).writeAsString(responseBody, mode: FileMode.write); + print('Relation written to file'); + } catch (error) { print('Error in writing to file: $error');} + // Return relation data from the response body return Uint8List.fromList(utf8.encode(responseBody)); - } else { - throw Exception('Response body is empty'); } - } else { - throw Exception('Failed to fetch relation data: Status code ${response.statusCode}'); } + return loadSavedRelation(); } catch (e) { - throw Exception('Failed to fetch relation data: ${e.toString()}'); + return loadSavedRelation(); + } +} + +Future<Uint8List> loadSavedRelation() async { + // Get latest saved relation from file if the server does not respond + Directory appDocumentsDirectory = await getApplicationDocumentsDirectory(); + String filePath = '${appDocumentsDirectory.path}/last_relation.json'; + + // Read file contents + File file = File(filePath); + if (await file.exists()) { + String contents = await file.readAsString(); + List<dynamic> jsonData = json.decode(contents); // Parse JSON string from file + Uint8List relation = Uint8List.fromList(utf8.encode(jsonData.toString())); + return relation; + } else { + throw Exception('File does not exist'); } } diff --git a/app/lib/pages/widgets/cloropleth_map.dart b/app/lib/pages/widgets/cloropleth_map.dart index 9c59c4fd33b7759eaf2883783c657c982d3aa1e4..ea33009a252ee2efb5cc34af16d580ab190035fa 100644 --- a/app/lib/pages/widgets/cloropleth_map.dart +++ b/app/lib/pages/widgets/cloropleth_map.dart @@ -7,10 +7,12 @@ import 'dart:convert'; /// A class containing thickness data for each subdivision of the map. class IceThicknessModel { - IceThicknessModel(this.subDivID, this.thickness); + IceThicknessModel(this.sub_div_id, this.thickness, this.color, this.savedColor); - final String subDivID; + final String sub_div_id; final int thickness; + Color color; + final Color savedColor; } /// ChoroplethMap is a stateful widget that contains a choropleth map. @@ -19,29 +21,42 @@ class IceThicknessModel { class ChoroplethMap extends StatefulWidget { const ChoroplethMap({Key? key, required this.relation, - required this.measurements + required this.measurements, + required this.onSelectionChanged, }) : super(key: key); final Uint8List relation; final List<Measurement> measurements; + final void Function(int selectedIndex) onSelectionChanged; // Callback function @override _ChoroplethMapState createState() => _ChoroplethMapState(); } class _ChoroplethMapState extends State<ChoroplethMap> { + int selectedIndex = -1; late MapShapeSource mapShapeSource; late final MapZoomPanBehavior _zoomPanBehavior = MapZoomPanBehavior(); List<IceThicknessModel> iceThicknessList = <IceThicknessModel>[]; + List<Color> testColors = [ // NB test color + const Color(0xff8a003b), + const Color(0xff8a4300), + const Color(0xff8a7a00), + const Color(0xff538a00), + const Color(0xff007b8a), + ]; @override void initState() { super.initState(); final Random random = Random(); - for (int i = 0; i <= 60; i++) { + for (int i = 0; i <= 120; i++) { + int ran = random.nextInt(5); // NB test color + Color randomColor = testColors[ran]; + int randomNumber = random.nextInt(21); // 0 -> 20, NB: temp test data - iceThicknessList.add(IceThicknessModel(i.toString(), randomNumber)); + iceThicknessList.add(IceThicknessModel(i.toString(), randomNumber, randomColor, randomColor)); } } @@ -54,14 +69,30 @@ class _ChoroplethMapState extends State<ChoroplethMap> { MapShapeLayer( source: MapShapeSource.memory( // Map polygon widget.relation, // JSON coordinates from server - shapeDataField: 'properties.SubDivID', + shapeDataField: 'sub_div_id', dataCount: iceThicknessList.length, - primaryValueMapper: (int index) => iceThicknessList[index].subDivID, - shapeColorValueMapper: (int index) => iceThicknessList[index].thickness, + primaryValueMapper: (int index) => iceThicknessList[index].sub_div_id, + shapeColorValueMapper: (int index) => iceThicknessList[index].color, ), - color: Colors.blue.shade400, // Map color + //color: Colors.blue.shade400, // Map color zoomPanBehavior: _zoomPanBehavior, strokeColor: Colors.black, + // Shape selection + selectedIndex: selectedIndex, + onSelectionChanged: (int index) { + setState(() { + selectedIndex = index; + for (int i = 0; i < iceThicknessList.length; i++) { + iceThicknessList[i].color = i == index ? Colors.red : iceThicknessList[i].savedColor; + } + }); + widget.onSelectionChanged(selectedIndex); + }, + selectionSettings: MapSelectionSettings( + color: Colors.orange, + strokeColor: Colors.red[900], + strokeWidth: 3, + ), ), ], ), diff --git a/app/lib/pages/widgets/map_widget.dart b/app/lib/pages/widgets/map_widget.dart index d308527304fe415fb17550657a2f715a36f2df4d..7d824eef6f43bbdfdfd7c80fe579bee0470c81af 100644 --- a/app/lib/pages/widgets/map_widget.dart +++ b/app/lib/pages/widgets/map_widget.dart @@ -28,6 +28,7 @@ class MapContainerWidget extends StatefulWidget { class _MapContainerWidgetState extends State<MapContainerWidget> { Measurement? selectedMarker; // Containing data for selected marker + int selectedMarkerIndex = 0; bool isMinimized = true; // Quick view box state tacker bool satLayer = false; // Satellite layer visibility tracker bool isTapped = false; // Button tap state tracker @@ -53,10 +54,17 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { } } + // Tile selection handler + void handleSelection(int index) { + setState(() { + selectedMarkerIndex = index; + }); + } + @override Widget build(BuildContext context) { // Initialise selectedMarker to first element in markerList - selectedMarker ??= widget.markerList[0]; + selectedMarker ??= widget.markerList[selectedMarkerIndex]; checkAndSetLastUpdate(); @@ -154,7 +162,10 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { height: screenWidth * boxHeight, child: Padding( padding: const EdgeInsets.all(15.0), // Padding around map - child: ChoroplethMap(relation: widget.relation, measurements: widget.markerList,), + child: ChoroplethMap( + relation: widget.relation, + measurements: widget.markerList, + onSelectionChanged: handleSelection,), ), ), Positioned( // Quick view box layered over map diff --git a/server/__pycache__/consts.cpython-311.pyc b/server/__pycache__/consts.cpython-311.pyc index 3c60371f12da04a05b4df70c07a52110b373b16c..1712cb5077bfd090a3e860cc1dca4556d993186a 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 5653feea29bda21aae6d6ea43c0365f22b8a73c3..6be29cfbf4d5c8d8470413ecead84c6ade7f37b9 100644 --- a/server/consts.py +++ b/server/consts.py @@ -10,4 +10,4 @@ SSL_KEY_PATH = CERT_DIR + "testKey.key" SSL_CERT_PATH = CERT_DIR + "testCert.crt" # Measurement specs -AREA_SIZE = 20 \ No newline at end of file +AREA_SIZE = 20 diff --git a/server/data_processing/__pycache__/area_processing.cpython-311.pyc b/server/data_processing/__pycache__/area_processing.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..633c3ea85b6c7a9ba12a981f1f16c33fb08d67d6 Binary files /dev/null and b/server/data_processing/__pycache__/area_processing.cpython-311.pyc differ diff --git a/server/lake_relations/overpass_query.txt b/server/lake_relations/overpass_query.txt index 4d011e2037b088d1bd4259f5ca7630fcf3318f13..1539aa9503a5f52bac64f8c81ad5584880c51a38 100644 --- a/server/lake_relations/overpass_query.txt +++ b/server/lake_relations/overpass_query.txt @@ -1,11 +1,3 @@ -The following query is used to retrieve the relation(shape) of a lake. -Go to https://overpass-turbo.eu/#, enter the query in the left field, add -the name of the lake you want to add, and press 'run'. -If a text box saying "" appears, press 'continue anyways'. Double check that you have -the correct lake, then press 'Export'. In the 'Export' menu, download the shape data as -GeoJson. Once downloaded, name the file the *lakeName.json, and move the file into -IceMap/server/lake_relations. Once you have added the file, run map division... - [out:json]; ( way["natural"="water"]["name"="lakeName"]; diff --git a/server/main.py b/server/main.py index fd50247f515865ff71fe1c231b5db2ca17b82a5b..18f682438103ab78b592778b6d81b30a99658849 100644 --- a/server/main.py +++ b/server/main.py @@ -6,7 +6,6 @@ from map.get_relation import get_relation from APIs.get_weather import get_weather from map.input_new_data import input_new_Lidar_data import ssl -import keyboard import sqlite3 app = Flask(__name__) diff --git a/server/map/__pycache__/get_relation.cpython-311.pyc b/server/map/__pycache__/get_relation.cpython-311.pyc index e67dc914c928bb26ee9ec375d748ec7eb89d4c7c..1fa84cb826efde8bac6448f030d6da17d236c79f 100644 Binary files a/server/map/__pycache__/get_relation.cpython-311.pyc and b/server/map/__pycache__/get_relation.cpython-311.pyc differ diff --git a/server/map/get_relation.py b/server/map/get_relation.py index ab52cfcf269e442ccc99c5e47b834cca16af9605..9f175ee845665081bfa35be119597a11bd3ec0ee 100644 --- a/server/map/get_relation.py +++ b/server/map/get_relation.py @@ -1,14 +1,11 @@ import geopandas as gpd -from shapely.geometry import Polygon, LineString +from shapely.geometry import Polygon, LineString, MultiLineString from shapely.ops import linemerge, unary_union, polygonize import matplotlib.pyplot as plt import random import json import os -polygon_min_x = None # The left most point of the entire polygon - - # Read a json file with relation data and send to response object def get_relation(self, body_of_water: str): # NB: implement body_of_water # Read relation from GeoJson file and extract all polygons @@ -17,8 +14,7 @@ def get_relation(self, body_of_water: str): # NB: implement body_of_water polygons = [Polygon(polygon.exterior) for polygon in polygon_data['geometry']] if len(polygons) <= 1: - print("Failed to convert to polygons") - return + raise Exception("Failed to convert JSON object to Shapely Polygons") divided_map = [] @@ -32,31 +28,60 @@ def get_relation(self, body_of_water: str): # NB: implement body_of_water divided_map.extend(combine_grid_with_poly(polygon, lines)) + ''' + ####################### PLOTTING ############################ tiles = [gpd.GeoDataFrame(geometry=[tile]) for tile in divided_map] - sub_div_id = 0 - for tile in tiles: - tile['sub_div_id'] = sub_div_id - tile['sub_div_center'] = tile['geometry'].centroid.apply(lambda x: [x.x, x.y]) - sub_div_id += 1 + print("Plotting... This may take some time...") + # NB test plot + fig, ax = plt.subplots() + ax.set_aspect(1.5) + + # Plot each tile + for tile in tiles: # NB temporarily limited to 5 tiles + random_color = "#{:06x}".format(random.randint(0, 0xFFFFFF)) + gpd.GeoSeries(tile.geometry).plot(ax=ax, facecolor=random_color, edgecolor='none') + - tiles_json = {'type': 'FeatureCollection', 'features': []} - for tile in tiles: - feature = { + plt.show() + ##################### PLOTTIND END ########################### + ''' + + features = [] + + sub_div_id = 0 + for tile in divided_map: # NB temporarily limited to 5 tiles + + # Round coordinates to 4 decimals + center = round(tile.centroid.coords[0][0], 4), round(tile.centroid.coords[0][1], 4) + rounded_coordinates = [] + if isinstance(tile, Polygon): + for coords in tile.exterior.coords: + rounded_coords = (round(coords[0], 4), round(coords[1], 4)) + rounded_coordinates.append(rounded_coords) + rounded_tile = Polygon(rounded_coordinates) + + tile_feature = { 'type': 'Feature', - 'geometry': tile.geometry.__geo_interface__, 'properties': { - 'sub_div_id': int(tile['sub_div_id'].iloc[0]), - 'sub_div_center': tile['sub_div_center'].tolist() - } + 'sub_div_id': str(sub_div_id), + 'sub_div_center': center + }, + 'geometry': rounded_tile.__geo_interface__ } - tiles_json['features'].append(feature) + features.append(tile_feature) + sub_div_id += 1 + + feature_collection = { + 'type': 'FeatureCollection', + 'features': features + } self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() - self.wfile.write(json.dumps(tiles_json).encode('utf-8')) + self.wfile.write(json.dumps(feature_collection).encode('utf-8')) def create_grid(poly: Polygon, cell_size): @@ -84,17 +109,17 @@ def create_grid(poly: Polygon, cell_size): def combine_grid_with_poly(polygon, grid): - # Create an empty list to store tiles intersecting the polygon intersecting_tiles = [] - # Iterate through each grid line for line in grid: - # Check if the line intersects with the polygon if line.intersects(polygon): - # If the line intersects, find the intersection points intersection = line.intersection(polygon) - # Add each line to the list - intersecting_tiles.append(intersection) + # Check if intersection is a MultiLineString + if isinstance(intersection, MultiLineString): + # Extend the intersecting tiles with the polygonized results + intersecting_tiles.extend(list(polygonize(intersection))) + else: + intersecting_tiles.append(intersection) return intersecting_tiles