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

Merge branch 'clhp_map' into 'main'

Choropleth map implementation

See merge request !3
parents 4b9f0001 8f31097f
No related branches found
No related tags found
1 merge request!3Choropleth map implementation
Showing
with 322872 additions and 193 deletions
Source diff could not be displayed: it is too large. Options to address this: view the blob.
Source diff could not be displayed: it is too large. Options to address this: view the blob.
......@@ -4,6 +4,8 @@ import 'widgets/map_widget.dart';
import 'marker_handler/marker_data.dart';
import 'consts.dart';
import 'marker_handler/get_markers.dart';
import 'marker_handler/get_relation.dart';
import 'dart:typed_data';
class DefaultPage extends StatefulWidget {
const DefaultPage({super.key});
......@@ -17,14 +19,17 @@ class _DefaultPageState extends State<DefaultPage> {
bool showBar = false;
List<Measurement> markerList = [];
Uint8List relation = Uint8List(0);
// Call fetchMarkerTemplate and await its result before setting the state
Future<void> loadMarkerList() async {
try {
List<Measurement> fetchedMarkers = await fetchMarkerData();
Uint8List fetchedRelation = await fetchRelation();
setState(() {
setState(() { // Initialise markers and relations
markerList = fetchedMarkers;
relation = fetchedRelation;
});
} catch (e) {
showDialog(
......@@ -95,7 +100,7 @@ class _DefaultPageState extends State<DefaultPage> {
),
body: ListView(
children: [ // Add main widget
MapContainerWidget(markerList: markerList),
MapContainerWidget(markerList: markerList, relation: relation),
],
),
),
......
......@@ -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,8 +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()}');
}
}
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import '../consts.dart';
import 'dart:typed_data';
/// Fetch relation data from server
Future<Uint8List> fetchRelation() async {
try {
// Custom HTTP client
HttpClient client = HttpClient()
..badCertificateCallback = // NB: temporary disable SSL certificate validation
(X509Certificate cert, String host, int port) => true;
// Execute request to to get_relation endpoint
var request = await client.getUrl(Uri.parse('${serverURI}get_relation'));
var response = await request.close(); // Close response body at end of function
// Parse body to JSON if request is ok
if (response.statusCode == 200) {
var responseBody = await response.transform(utf8.decoder).join();
if (responseBody.isNotEmpty) {
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}');
}
} catch (e) {
throw Exception('Failed to fetch relation data: ${e.toString()}');
}
}
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;
List <LatLng> corners;
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,
required this.corners
});
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(),
corners: (json['Corners'] as List<dynamic>).map((corner) => LatLng(corner[0], corner[1])).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 +76,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
import 'dart:typed_data';
import 'package:app/pages/consts.dart';
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_maps/maps.dart';
import '../consts.dart';
import 'dart:typed_data';
/// A class containing thickness for each subdivision of the map.
class IceThicknessModel {
IceThicknessModel(this.subDivID, this.thickness);
final int subDivID;
final double thickness;
}
/// A stateful widget which contains a choropleth map.
/// The map data is fetched from the server, and the map is rendered
/// using the Syncfusion Flutter Maps library.
class ChoroplethMap extends StatefulWidget {
const ChoroplethMap({Key? key, required this.relation}) : super(key: key);
final Uint8List relation;
@override
_ChoroplethMapState createState() => _ChoroplethMapState();
}
class _ChoroplethMapState extends State<ChoroplethMap> {
late MapShapeSource mapShapeSource;
List<IceThicknessModel> iceThicknessList = <IceThicknessModel>[];
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return SfMaps(
layers: [
MapShapeLayer(
source: MapShapeSource.memory(
widget.relation,
shapeDataField: 'name',
),
color: Colors.orange,
),
],
);
}
}
......@@ -5,15 +5,19 @@ import 'quick_view_chart.dart';
import 'stat_charts.dart';
import 'sat_layer.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'cloropleth_map.dart';
import 'dart:typed_data';
/// MapContainerWidget is the main widget that contains the map with all
/// its layers, polygons and markers.
class MapContainerWidget extends StatefulWidget {
final List<Measurement> markerList;
final Uint8List relation;
const MapContainerWidget({Key? key, required this.markerList}) : super(key: key);
const MapContainerWidget({Key? key,
required this.markerList,
required this.relation,
}) : super(key: key);
@override
_MapContainerWidgetState createState() => _MapContainerWidgetState();
......@@ -55,81 +59,6 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
borderRadius: BorderRadius.circular(20),
child: Stack( // Stack of quick view, map layer, satellite layer, and buttons
children: [
SizedBox(
width: screenWidth * boxWidth,
height: screenWidth * boxHeight,
child: FlutterMap(
options: MapOptions(
center: mapCenter,
zoom: 9.0,
),
mapController: _mapController,
children: [
TileLayer(
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: const ['a', 'b', 'c'],
),
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
color: Colors.blue.withOpacity(0.5),
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),
builder: (ctx) => GestureDetector(
onTap: () {
setState(() {
selectedMarker = measurement;
});
},
child: const Icon(
Icons.severe_cold,
color: Colors.blue,
size: 30.0,
),
),
);
}).toList(),
),
],
),
),
/*SizedBox(
width: screenWidth * boxWidth,
height: screenWidth * boxHeight,
......@@ -138,11 +67,60 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
SatLayer(markerList: widget.markerList), // Satellite layer
Visibility(
visible: satLayer, // Only show layer if satellite button is toggled on
child: SatLayer(markerList: widget.markerList),
child: FlutterMap(
options: MapOptions(
center: mapCenter,
zoom: 9.0,
),
mapController: _mapController,
children: [
TileLayer(
urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: const ['a', 'b', 'c'],
),
MarkerLayer(
markers: widget.markerList.map((Measurement measurement) {
return Marker(
width: 50,
height: 50,
point: measurement.center, // Set markers at center of measurement
builder: (ctx) => GestureDetector(
onTap: () {
setState(() {
selectedMarker = measurement;
});
},
child: Icon(
Icons.severe_cold,
color: measurement == selectedMarker ? Colors.green : Colors.blue,
size: measurement == selectedMarker ? 40.0 : 30.0,
),
),
);
}).toList(),
),
],
),
),
],
),
),*/
SizedBox( // Colored box behind map
width: screenWidth * boxWidth,
height: screenWidth * boxHeight,
child: Container(
color: const Color(0x883366ff)
),
),
SizedBox( // Lake map
width: screenWidth * boxWidth,
height: screenWidth * boxHeight,
child: Padding(
padding: const EdgeInsets.all(15.0), // Padding around map
child: ChoroplethMap(relation: widget.relation),
),
),
Positioned( // Quick view box layered over map
bottom: 10,
right: 10,
......@@ -268,7 +246,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),
......
......@@ -119,10 +119,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482"
url: "https://pub.dev"
source: hosted
version: "0.13.6"
version: "0.13.5"
http_parser:
dependency: transitive
description:
......@@ -147,6 +147,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.8.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
url: "https://pub.dev"
source: hosted
version: "10.0.0"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
url: "https://pub.dev"
source: hosted
version: "2.0.1"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
url: "https://pub.dev"
source: hosted
version: "2.0.1"
lints:
dependency: transitive
description:
......@@ -167,26 +191,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16"
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.8.0"
meta:
dependency: transitive
description:
name: meta
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
url: "https://pub.dev"
source: hosted
version: "1.10.0"
version: "1.11.0"
mgrs_dart:
dependency: transitive
description:
......@@ -207,10 +231,10 @@ packages:
dependency: transitive
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
version: "1.9.0"
path_drawing:
dependency: transitive
description:
......@@ -360,6 +384,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
syncfusion_flutter_core:
dependency: transitive
description:
name: syncfusion_flutter_core
sha256: "3979f0b1c5a97422cadae52d476c21fa3e0fb671ef51de6cae1d646d8b99fe1f"
url: "https://pub.dev"
source: hosted
version: "20.4.54"
syncfusion_flutter_maps:
dependency: "direct main"
description:
name: syncfusion_flutter_maps
sha256: "1c95924e2dee5bbad922c2d2f1d5fb1b13ce4548c34a6d30fe6bf8821dfcfcd6"
url: "https://pub.dev"
source: hosted
version: "20.4.54"
term_glyph:
dependency: transitive
description:
......@@ -408,14 +448,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
web:
vm_service:
dependency: transitive
description:
name: web
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
name: vm_service
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
url: "https://pub.dev"
source: hosted
version: "0.3.0"
version: "13.0.0"
win32:
dependency: transitive
description:
......
name: app
description: "A new Flutter project."
description: "IceMap Application"
publish_to: 'none'
version: 0.1.0
......@@ -15,6 +15,7 @@ dependencies:
provider: ^5.0.0
fl_chart: ^0.20.0-nullsafety1
google_fonts: any
syncfusion_flutter_maps: ^20.4.41
dev_dependencies:
flutter_test:
......@@ -25,3 +26,5 @@ flutter:
uses-material-design: true
assets:
- assets/icons/
- assets/mjosa.geojson
- assets/australia.json
No preview for this file type
......@@ -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
File added
......@@ -2,11 +2,13 @@ from flask import Flask
from http.server import HTTPServer, BaseHTTPRequestHandler
from consts import SSL_CERT_PATH, SSL_KEY_PATH, HOST, PORT
from map.get_markers import get_all_markers
from map.get_relation import get_relation
from APIs.get_weather import get_weather
import ssl
import keyboard
import sqlite3
app = Flask(__name__)
terminate_server = 0
......@@ -41,6 +43,8 @@ class IceHTTP(BaseHTTPRequestHandler):
elif self.path == '/get_valid_markers': # NB: should be POST?
get_all_markers(self, self.cursor, True, 'Mjosa') # Get only valid markers
# NB: temporary hardcoded waterBodyName
elif self.path == '/get_relation':
get_relation(self, 'Mjosa') # NB temp hardcoded value
def do_POST(self):
if self.path == '/get_weather_data':
......@@ -60,6 +64,7 @@ def on_key_press(server, event, cursor, conn):
# Start a server on port 8443 using self defined HTTP class
if __name__ == "__main__":
try:
# Initialize database connection
conn = sqlite3.connect('server/sql_db/icedb')
......
No preview for this file type
File added
import json
from math import pi, cos
# get_markers requests all marker data or valid markers, converts the data to json, and writes
......@@ -6,7 +7,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,18 +36,18 @@ 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
if measurement_id in measurement_data:
# Check if the data object already exists in the list
# Create new subdivision within measurement if it does not already exist
if sub_division not in measurement_data[measurement_id]['Subdivisions']:
measurement_data[measurement_id]['Subdivisions'].append(sub_division)
......@@ -55,12 +56,15 @@ 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
'Corners': calculate_corners(row[3], row[4]), # Return list of corners calculated based on center coordinates
}
# Convert dictionary values to list of measurements
......@@ -87,3 +91,37 @@ def get_all_markers(self, cursor, valid: bool, waterBodyName):
# Write marker data to response object
self.wfile.write(marker_data.encode('utf-8'))
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):
"""Calculate corners of polygon based on a center coordinate
Arguments:
lat -- center latitude
lng -- center longitude
"""
# 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)
] '''
return [(60.7798, 10.7062), # NB: temporary hardcoded values
(60.7553, 10.7433),
(60.7718, 10.7975),
(60.7966, 10.7405)]
import string
import json
import geopandas as gpd
import matplotlib.pyplot as plt
# get_relation returns the geojson data for a selected body of water
def get_relation(self, body_of_water: string):
# Load GeoJSON data using geopandas
geo_data = gpd.read_file("server/map/mjosa.geojson")
# Filter only polygons, exclude points
polygon_data = geo_data[geo_data['geometry'].geom_type == 'Polygon']
# Plot polygons
polygon_data.plot()
plt.title('Polygons in Mjøsa GeoJSON')
plt.xlabel('Longitude')
plt.ylabel('Latitude')
plt.show()
# Convert GeoDataFrame to dictionary
geojson_dict = json.loads(polygon_data.to_json())
# Convert response data to JSON string
response_json = json.dumps(geojson_dict)
# Set headers
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
# Write coordinates to response object
self.wfile.write(response_json.encode('utf-8'))
Source diff could not be displayed: it is too large. Options to address this: view the blob.
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