Skip to content
Snippets Groups Projects
Commit c410f6cb authored by Sara Savanovic Djordjevic's avatar Sara Savanovic Djordjevic
Browse files

Merge branch 'clhp_map' into 'main'

Clhp map

See merge request !7
parents 35a11e12 dedd71a7
No related branches found
No related tags found
1 merge request!7Clhp map
......@@ -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
......
......@@ -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;
......
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');
}
}
......@@ -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,
),
),
],
),
......
......@@ -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
......
No preview for this file type
......@@ -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
File added
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"];
......
......@@ -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__)
......
No preview for this file type
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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment