Skip to content
Snippets Groups Projects
Commit e39107d9 authored by Hoa Ben The Nguyen's avatar Hoa Ben The Nguyen
Browse files

Merge branch 'server' of gitlab.stud.idi.ntnu.no:sarasdj/prog2900

parents 681c0094 bb14fd70
No related branches found
No related tags found
No related merge requests found
Showing
with 424 additions and 207 deletions
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'pages/default_page.dart';
import 'package:latlong2/latlong.dart'; // Import LatLng class from the latlong package
import 'dart:async';
import 'dart:io';
import 'dart:convert';
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() { void main() {
runApp(const App()); runApp(const MyApp());
} }
class App extends StatelessWidget { class MyApp extends StatelessWidget {
const App({super.key}); const MyApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const MaterialApp( return const MaterialApp(
home: DefaultPage(), home: const 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),
],
),
),
),
); );
} }
} }
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
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
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');
}
}
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
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
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