From 9b990d403eb2a6c1260f43fc72fd18ba28bc1975 Mon Sep 17 00:00:00 2001
From: Hoa Ben The Nguyen <hbnguye@stud.ntnu.no>
Date: Thu, 21 Mar 2024 12:37:52 +0100
Subject: [PATCH] changes: new assets

---
 app/assets/icons/frozen.png                 | Bin 0 -> 12888 bytes
 app/lib/consts.dart                         |  37 +++
 app/lib/data_classes.dart                   |  88 +++++
 app/lib/server_requests/fetch_markers.dart  |  77 +++++
 app/lib/server_requests/fetch_relation.dart |  60 ++++
 app/lib/widgets/choropleth_map.dart         | 107 ++++++
 app/lib/widgets/main_layout.dart            | 347 ++++++++++++++++++++
 app/lib/widgets/osm_layer.dart              |  38 +++
 app/lib/widgets/quick_view_chart.dart       |  37 +++
 app/lib/widgets/satellite_layer.dart        |  30 ++
 app/lib/widgets/stat_charts.dart            | 102 ++++++
 11 files changed, 923 insertions(+)
 create mode 100644 app/assets/icons/frozen.png
 create mode 100644 app/lib/consts.dart
 create mode 100644 app/lib/data_classes.dart
 create mode 100644 app/lib/server_requests/fetch_markers.dart
 create mode 100644 app/lib/server_requests/fetch_relation.dart
 create mode 100644 app/lib/widgets/choropleth_map.dart
 create mode 100644 app/lib/widgets/main_layout.dart
 create mode 100644 app/lib/widgets/osm_layer.dart
 create mode 100644 app/lib/widgets/quick_view_chart.dart
 create mode 100644 app/lib/widgets/satellite_layer.dart
 create mode 100644 app/lib/widgets/stat_charts.dart

diff --git a/app/assets/icons/frozen.png b/app/assets/icons/frozen.png
new file mode 100644
index 0000000000000000000000000000000000000000..5c224e3c664cdb1628cff2a696e6ed9c045d0bf5
GIT binary patch
literal 12888
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
G{r>}sTY(+`

literal 0
HcmV?d00001

diff --git a/app/lib/consts.dart b/app/lib/consts.dart
new file mode 100644
index 00000000..ff51be45
--- /dev/null
+++ b/app/lib/consts.dart
@@ -0,0 +1,37 @@
+import 'package:latlong2/latlong.dart';
+import 'package:flutter/material.dart';
+import 'package:google_fonts/google_fonts.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 variables
+LatLng mapCenter = LatLng(60.7666, 10.8471);
+DateTime ?lastUpdate; // Last time data was fetched from server
+
+// Font variables
+const textColor = Colors.white;
+final appTitleStyle = GoogleFonts.dmSans(
+  fontSize: 35,
+  color: Colors.black,
+  fontWeight: FontWeight.bold,
+);
+final titleStyle = GoogleFonts.dmSans(
+  fontSize: 35,
+  color: textColor,
+  fontWeight: FontWeight.bold,
+);
+final regTextStyle = GoogleFonts.dmSans(fontSize: 19, color: textColor);
+final chartTextStyle = GoogleFonts.dmSans(fontSize: 14, color: textColor);
+final subHeadingStyle = GoogleFonts.dmSans(fontSize: 22, color: textColor, fontWeight: FontWeight.bold);
+
+// Colors
+const darkBlue = Color(0xFF015E8F);
+const teal = Color(0xFF00B4D8);
+const darkestBlue = Color(0xFF03045E);
+const lightBlue = Color(0xFFCAF0F8);
+const superLightBlue = Color(0xFFCAF0F8);
+const barBlue = Color(0xFF0077B6);
\ No newline at end of file
diff --git a/app/lib/data_classes.dart b/app/lib/data_classes.dart
new file mode 100644
index 00000000..8de99433
--- /dev/null
+++ b/app/lib/data_classes.dart
@@ -0,0 +1,88 @@
+import 'dart:core';
+import 'package:latlong2/latlong.dart';
+import 'package:flutter/material.dart';
+
+class Measurement {
+  int measurementID;
+  DateTime timeMeasured;
+  Sensor sensor;
+  String bodyOfWater;
+  LatLng center;
+  List <SubDiv> subDivs;
+
+  Measurement({
+    required this.measurementID,
+    required this.timeMeasured,
+    required this.sensor,
+    required this.bodyOfWater,
+    required this.center,
+    required this.subDivs,
+  });
+
+  factory Measurement.fromJson(Map<String, dynamic> json) {
+    return Measurement(
+        measurementID: json['MeasurementID'],
+        timeMeasured: DateTime.parse(json['TimeMeasured']),
+        sensor: Sensor.fromJson(json['Sensor']),
+        bodyOfWater: json['BodyOfWater'] ?? 'nil',
+        center: LatLng(json['CenterLat'], json['CenterLon']),
+        subDivs: (json['Subdivisions'] as List<dynamic>).map((data) => SubDiv.fromJson(data)).toList(),
+    );
+  }
+}
+
+class SubDiv {
+  String sub_div_id;
+  int groupID;
+  double minThickness;
+  double avgThickness;
+  LatLng center;
+  double accuracy;
+  Color color;
+  Color savedColor;
+
+  SubDiv({
+    required this.sub_div_id,
+    required this.groupID,
+    required this.minThickness,
+    required this.avgThickness,
+    required this.center,
+    required this.accuracy,
+    required this.color,
+    required this.savedColor
+  });
+
+  factory SubDiv.fromJson(Map<String, dynamic> json) {
+    return SubDiv(
+      sub_div_id: json['SubdivID'].toString(),
+      groupID: json['GroupID'],
+      minThickness: json['MinThickness'],
+      avgThickness: json['AvgThickness'],
+      center: LatLng(json['CenLatitude'], json['CenLongitude']),
+      accuracy: json['Accuracy'],
+      // Set grey as default color
+      color: json['Color'] != null ? Color(json['Color']) : Colors.grey,
+      savedColor: json['Color'] != null ? Color(json['Color']) : Colors.grey,
+    );
+  }
+}
+
+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'] ?? 'nil',
+      active: json['Active'],
+    );
+  }
+}
diff --git a/app/lib/server_requests/fetch_markers.dart b/app/lib/server_requests/fetch_markers.dart
new file mode 100644
index 00000000..410a25df
--- /dev/null
+++ b/app/lib/server_requests/fetch_markers.dart
@@ -0,0 +1,77 @@
+import 'dart:io';
+import 'dart:async';
+import 'dart:convert';
+import 'package:path_provider/path_provider.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import '../../consts.dart';
+import '../data_classes.dart';
+
+class FetchResult {
+  final List<Measurement> measurements;
+  final bool connected;
+
+  FetchResult(this.measurements, this.connected);
+}
+
+/// fetchMarkerData fetches measurement data from the server
+Future<FetchResult> fetchMeasurements() async {
+  try {
+    // Custom HTTP client
+    HttpClient client = HttpClient()
+      ..badCertificateCallback = // NB: temporary disable SSL certificate validation
+          (X509Certificate cert, String host, int port) => true;
+
+    // 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
+
+    // Parse body to JSON if request is ok
+    if (response.statusCode == 200) {
+      var responseBody = await response.transform(utf8.decoder).join();
+
+      if (responseBody.isNotEmpty) {
+        var jsonData = json.decode(responseBody);
+
+        // Attempt to parse response to Measurement object only if the body
+        // contains correctly formatted data
+        if (jsonData != null && jsonData is List) {
+          Directory appDocumentsDirectory = await getApplicationDocumentsDirectory();
+          String filePath = '${appDocumentsDirectory.path}/last_data.json';
+
+          try { // Write most recent time of update to file
+            await File(filePath).writeAsString(responseBody, mode: FileMode.write);
+            print('Lake data written to file');
+          } catch (error) { print('Error in writing to file: $error');}
+
+          // Update local and persistent lastUpdate variable
+          lastUpdate = DateTime.now();
+          final prefs = await SharedPreferences.getInstance();
+          await prefs.setString('lastUpdate', '${DateTime.now()}');
+
+          return FetchResult(jsonData.map((data) => Measurement.fromJson(data)).toList(), true);
+        }
+      }
+    }
+    return loadSavedData();
+  } catch (e) {
+    return loadSavedData();
+  }
+}
+
+Future<FetchResult> loadSavedData() async {
+  // Get latest saved data from file if the server does not respond
+  Directory appDocumentsDirectory = await getApplicationDocumentsDirectory();
+  String filePath = '${appDocumentsDirectory.path}/last_data.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
+    List<Measurement> measurements = jsonData.map((data) => Measurement.fromJson(data)).toList();
+    return FetchResult(measurements, false);
+  } else {
+    throw Exception('File does not exist');
+  }
+}
diff --git a/app/lib/server_requests/fetch_relation.dart b/app/lib/server_requests/fetch_relation.dart
new file mode 100644
index 00000000..ab77dcec
--- /dev/null
+++ b/app/lib/server_requests/fetch_relation.dart
@@ -0,0 +1,60 @@
+import 'dart:io';
+import 'dart:async';
+import 'dart:convert';
+import 'dart:typed_data';
+import 'package:path_provider/path_provider.dart';
+
+import '../../consts.dart';
+
+/// 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
+
+    // Try to parse body to JSON if request is ok
+    if (response.statusCode == 200) {
+      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));
+      }
+    }
+    return loadSavedRelation();
+  } catch (e) {
+    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/widgets/choropleth_map.dart b/app/lib/widgets/choropleth_map.dart
new file mode 100644
index 00000000..56452604
--- /dev/null
+++ b/app/lib/widgets/choropleth_map.dart
@@ -0,0 +1,107 @@
+import 'dart:typed_data';
+import 'package:flutter/material.dart';
+import 'package:syncfusion_flutter_maps/maps.dart';
+
+import 'package:latlong2/latlong.dart';
+
+import '../data_classes.dart';
+
+/// A class containing thickness data for each subdivision of the map.
+class IceThicknessModel {
+  IceThicknessModel(this.sub_div_id, this.thickness, this.color, this.savedColor);
+
+  final String sub_div_id;
+  final int thickness;
+  Color color;
+  final Color savedColor;
+}
+
+/// ChoroplethMap is a stateful widget that contains a choropleth map.
+/// The map is created using the Syncfusion Flutter Maps library and
+/// coordinates fetched from the server.
+class ChoroplethMap extends StatefulWidget {
+  const ChoroplethMap({Key? key,
+    required this.relation,
+    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<SubDiv> subdivisions = <SubDiv>[];
+  int count = 0;
+
+
+  @override
+  void initState() {
+    super.initState();
+    // Create list of all subdivisions
+    for (Measurement measurement in widget.measurements) {
+      for (SubDiv subdivision in measurement.subDivs) {
+        subdivisions.add(subdivision);
+        print("SubDivID: ${subdivision.sub_div_id}");
+        count++;
+      }
+    };
+
+    // NB temporary filler
+    for (var i = count; i < 250; i++) {
+      SubDiv subdivision = SubDiv(
+        sub_div_id: i.toString(),
+        groupID: 0,
+        minThickness: 0.0,
+        avgThickness: 0.0,
+        center: LatLng(0.0, 0.0),
+        accuracy: 0.0,
+        color: Colors.grey,
+        savedColor: Colors.grey,
+      );
+      subdivisions.add(subdivision);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: [
+        SfMaps(
+          layers: [
+            MapShapeLayer(
+              source: MapShapeSource.memory( // Map polygon
+                widget.relation, // JSON coordinates from server
+                shapeDataField: 'sub_div_id',
+                dataCount: 250,
+                primaryValueMapper: (int index) => subdivisions[index].sub_div_id,
+                shapeColorValueMapper: (int index) => subdivisions[index].color,
+              ),
+              zoomPanBehavior: _zoomPanBehavior,
+              strokeColor: Colors.blue.shade50,
+              // Shape selection
+              selectedIndex: selectedIndex,
+              onSelectionChanged: (int index) {
+                setState(() {
+                  selectedIndex = index;
+                  for (int i = 0; i < subdivisions.length; i++) {
+                    subdivisions[i].color = i == index ? Colors.red : subdivisions[i].savedColor;
+                  }
+                });
+                widget.onSelectionChanged(selectedIndex);
+              },
+            ),
+          ],
+        ),
+      ],
+    );
+  }
+}
+
diff --git a/app/lib/widgets/main_layout.dart b/app/lib/widgets/main_layout.dart
new file mode 100644
index 00000000..ae2a233a
--- /dev/null
+++ b/app/lib/widgets/main_layout.dart
@@ -0,0 +1,347 @@
+import 'dart:typed_data';
+import 'package:flutter_map/flutter_map.dart';
+import 'package:flutter/material.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'satellite_layer.dart';
+import 'stat_charts.dart';
+import '../../consts.dart';
+import 'choropleth_map.dart';
+import '../data_classes.dart';
+import 'quick_view_chart.dart';
+
+/// 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;
+  final bool serverConnection;
+
+  const MapContainerWidget({Key? key,
+    required this.markerList,
+    required this.relation,
+    required this.serverConnection,
+  }) : super(key: key);
+
+  @override
+  _MapContainerWidgetState createState() => _MapContainerWidgetState();
+}
+
+class _MapContainerWidgetState extends State<MapContainerWidget> {
+
+  Measurement? selectedTile;  // Containing data for selected marker
+  int selectedTileIndex = 0;
+  bool isMinimized = true;      // Quick view box state tacker
+  bool satLayer = false;        // Satellite layer visibility tracker
+  bool isTapped = false;        // Button tap state tracker
+  final MapController _mapController = MapController(); // Map controller to re-center map view
+
+  // recenterMap moves the map back to its initial view
+  void recenterMap() {
+    _mapController.move(mapCenter, 9.0);
+  }
+
+  // Initialise lastUpdate variable from persistent storage if server fetch fails
+  Future<void> checkAndSetLastUpdate() async {
+    if (lastUpdate == null) {
+      final prefs = await SharedPreferences.getInstance();
+      final updateString = prefs.getString('lastUpdate');
+
+      if (updateString != null && updateString.isNotEmpty) {
+        final updateData = DateTime.parse(updateString);
+        setState(() {
+          lastUpdate = updateData;
+        });
+      }
+    }
+  }
+
+  // Tile selection handler
+  void handleSelection(int index) {
+    setState(() {
+      selectedTileIndex = index;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // Initialise selectedMarker to first element in markerList
+    selectedTile ??= widget.markerList[selectedTileIndex];
+
+    checkAndSetLastUpdate();
+
+    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),
+            /*if (true) NB: add search bar
+              const SearchBar(),
+              const SizedBox(height: contPadding),*/
+            ClipRRect(
+              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: Stack(
+                      children: [
+                        SatLayer(markerList: widget.markerList), // Satellite layer
+                        Visibility(
+                          visible: satLayer, // Only show layer if satellite button is toggled on
+                          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),
+                      child: Column(
+                        crossAxisAlignment: CrossAxisAlignment.center,
+                        children: [
+                          const SizedBox(height: 5),
+                          Text( // Display time of most recent server fetch
+                            'Last updated at ${lastUpdate != null ?
+                            (lastUpdate?.day == DateTime.now().day &&
+                                lastUpdate?.month == DateTime.now().month &&
+                                lastUpdate?.year == DateTime.now().year ?
+                              '${lastUpdate?.hour}:${lastUpdate?.minute}' :
+                              '${lastUpdate?.day}-${lastUpdate?.month}-${lastUpdate?.year}') : ''}',
+                            style: GoogleFonts.dmSans(
+                              fontSize: 14,
+                              color: Colors.black,
+                            ),
+                          ),
+                        ],
+                      ),
+                    ),
+                  ),
+                  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,
+                        measurements: widget.markerList,
+                        onSelectionChanged: handleSelection,),
+                    ),
+                  ),
+                  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( // Graph only visible when box is maximized and a marker is selected
+                              visible: !isMinimized && selectedTile != 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(), // Quick view graph
+                                  ),
+                                ),
+                              ),
+                            ),
+                            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),
+                              ),
+                            ),
+                          ],
+                        ),
+                      ),
+                    ),
+                  ),
+                  Positioned( // Satellite button
+                    top: 10,
+                    right: 10,
+                    child: GestureDetector(
+                      onTap: () {
+                        setState(() {
+                          satLayer = !satLayer; // Toggle satellite layer state on press
+                        });
+                      },
+                      child: Container(
+                        padding: const EdgeInsets.all(8),
+                        decoration: satLayer ? const BoxDecoration( // Add decoration only when pressed
+                          shape: BoxShape.circle,
+                          color: Colors.blue,
+                        ) : null,
+                        child: const Icon(Icons.satellite_alt_outlined),
+                      ),
+                    ),
+                  ),
+                  Positioned( // Back to center button
+                    top: 45,
+                    right: 10,
+                    child: GestureDetector(
+                      onTapDown: (_) {
+                        setState(() {
+                          recenterMap(); // Reset map view
+                          isTapped = true;
+                        });
+                      },
+                      onTapUp: (_) {
+                        setState(() {
+                          isTapped = false;
+                        });
+                      },
+                      onTapCancel: () {
+                        setState(() {
+                          isTapped = false;
+                        });
+                      },
+                      child: Container(
+                        padding: const EdgeInsets.all(8),
+                        decoration: isTapped ? const BoxDecoration( // Add decoration only when pressed
+                          shape: BoxShape.circle,
+                          color: Colors.blue,
+                        ) : null,
+                        child: const Icon(Icons.settings_backup_restore),
+                      ),
+                    ),
+                  ),
+                  Positioned( // No wifi icon
+                    top: 80,
+                    right: 10,
+                    child: GestureDetector(
+                      onTapDown: (_) {
+                        setState(() {
+                          // Add functionality
+                        });
+                      },
+                      onTapUp: (_) {
+                        setState(() {
+                          // Add functionality
+                        });
+                      },
+                      onTapCancel: () {
+                        setState(() {
+                          // Add functionality
+                        });
+                      },
+                      child: Visibility( // Show icon only when no server connection
+                        visible: !widget.serverConnection,
+                        child: Container(
+                          padding: const EdgeInsets.all(8),
+                          decoration: isTapped ? const BoxDecoration(
+                            shape: BoxShape.circle,
+                            color: Colors.blue,
+                          ) : null,
+                          child: const Icon(Icons.perm_scan_wifi, color: Color(0xFF5B0000)),
+                        ),
+                      ),
+                    ),
+                  ),
+                ],
+              ),
+            ),
+            const SizedBox(height: contPadding), // Padding between containers
+            ClipRRect(
+              borderRadius: BorderRadius.circular(20),
+              child: SizedBox(
+                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,
+                        ),
+                        const Divider(),
+                        Text(
+                          'Time of measurement',
+                          style: subHeadingStyle,
+                        ),
+                        Text(
+                          'Date ${(selectedTile?.timeMeasured.day ?? '-')}/${(selectedTile?.timeMeasured.month ?? '-')}/${(selectedTile?.timeMeasured.year ?? '-')}',
+                          style: regTextStyle,
+                        ),
+                        Text(
+                          'Time: ${selectedTile?.timeMeasured.hour}:00',
+                          style: regTextStyle,
+                        ),
+                        const SizedBox(height: contPadding),
+                        Text(
+                          'Measuring point: (${selectedTile?.measurementID}, ${selectedTile?.measurementID})',
+                          style: regTextStyle,
+                        ),
+                        const SizedBox(height: contPadding),
+                        const StatCharts(),
+                      ],
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}
\ No newline at end of file
diff --git a/app/lib/widgets/osm_layer.dart b/app/lib/widgets/osm_layer.dart
new file mode 100644
index 00000000..33b000b6
--- /dev/null
+++ b/app/lib/widgets/osm_layer.dart
@@ -0,0 +1,38 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_map/flutter_map.dart';
+
+import '../../consts.dart';
+import '../data_classes.dart';
+
+class OSM extends StatelessWidget {
+  final List<Measurement> markerList;
+
+  const OSM({
+    Key? key,
+    required this.markerList,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    // Init list of polygons
+    List<Polygon> polygons = [];
+
+    // Map each element from markerList to a measurement object
+
+    return FlutterMap(
+      options: MapOptions(
+        center: mapCenter,
+        zoom: 9.0,
+      ),
+      children: [
+        TileLayer(
+          urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
+          subdomains: const ['a', 'b', 'c'],
+        ),
+        PolygonLayer(
+          polygons: polygons, // Return map with list of polygons included
+        ),
+      ],
+    );
+  }
+}
diff --git a/app/lib/widgets/quick_view_chart.dart b/app/lib/widgets/quick_view_chart.dart
new file mode 100644
index 00000000..51dfa4f1
--- /dev/null
+++ b/app/lib/widgets/quick_view_chart.dart
@@ -0,0 +1,37 @@
+import 'package:flutter/material.dart';
+import 'package:fl_chart/fl_chart.dart';
+
+class QuickViewChart extends StatelessWidget {
+  const QuickViewChart({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return LineChart(
+      LineChartData(
+        titlesData: FlTitlesData(
+          leftTitles: SideTitles(showTitles: true),
+          bottomTitles: SideTitles(showTitles: true),
+        ),
+        borderData: FlBorderData(
+          show: true,
+        ),
+        minX: 0,  // Test data
+        maxX: 4,
+        minY: 0,
+        maxY: 50,
+        lineBarsData: [
+          LineChartBarData(
+            spots: [
+              FlSpot(0, 10),  // Test data
+              FlSpot(1, 20),
+              FlSpot(2, 30),
+              FlSpot(3, 40),
+            ],
+            isCurved: true,
+            colors: [Colors.blue],
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/app/lib/widgets/satellite_layer.dart b/app/lib/widgets/satellite_layer.dart
new file mode 100644
index 00000000..a82614bc
--- /dev/null
+++ b/app/lib/widgets/satellite_layer.dart
@@ -0,0 +1,30 @@
+import 'package:latlong2/latlong.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_map/flutter_map.dart';
+
+import '../data_classes.dart';
+
+class SatelliteLayer extends StatelessWidget {
+  final List<Measurement> markerList;
+
+  const SatelliteLayer({
+    Key? key,
+    required this.markerList,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return FlutterMap(
+      options: MapOptions(
+        center: LatLng(60.7666, 10.8471),
+        zoom: 9.0,
+      ),
+      children: [
+        TileLayer( // Map from OpenStreetMap
+          urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
+          subdomains: const ['a', 'b', 'c'],
+        ),
+      ],
+    );
+  }
+}
diff --git a/app/lib/widgets/stat_charts.dart b/app/lib/widgets/stat_charts.dart
new file mode 100644
index 00000000..86d511e6
--- /dev/null
+++ b/app/lib/widgets/stat_charts.dart
@@ -0,0 +1,102 @@
+import 'package:flutter/material.dart';
+import 'package:fl_chart/fl_chart.dart';
+
+class StatCharts extends StatelessWidget {
+  const StatCharts({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: [
+        LineChart(
+          LineChartData(
+            backgroundColor: Colors.lightBlue,
+            titlesData: FlTitlesData(
+              leftTitles: SideTitles(showTitles: true),
+              bottomTitles: SideTitles(showTitles: true),
+            ),
+            borderData: FlBorderData(show: true),
+            minX: 0,
+            maxX: 4,
+            minY: 0,
+            maxY: 50,
+            lineBarsData: [
+              LineChartBarData(
+                spots: [
+                  FlSpot(0, 10),
+                  FlSpot(1, 20),
+                  FlSpot(2, 30),
+                  FlSpot(3, 40),
+                ],
+                isCurved: true,
+                colors: [Colors.blue],
+              ),
+            ],
+          ),
+        ),
+        const SizedBox(height: 20),
+        SizedBox(
+          width: MediaQuery.of(context).size.width * 0.8,
+          height: 160,
+          child: BarChart(
+            BarChartData(
+              alignment: BarChartAlignment.spaceAround,
+              maxY: 20,
+              barTouchData: BarTouchData(enabled: false),
+              titlesData: FlTitlesData(
+                bottomTitles: SideTitles(
+                  showTitles: true,
+                  getTextStyles: (value) => const TextStyle(color: Colors.black),
+                  margin: 10,
+                  getTitles: (value) {
+                    switch (value.toInt()) {
+                      case 0:
+                        return 'Placeholder1';
+                      case 1:
+                        return 'Placeholder2';
+                      case 2:
+                        return 'Placeholder3';
+                      default:
+                        return '';
+                    }
+                  },
+                ),
+                leftTitles: SideTitles(
+                  showTitles: true,
+                  getTextStyles: (value) => const TextStyle(color: Colors.black),
+                  margin: 10,
+                  reservedSize: 30,
+                  interval: 5,
+                ),
+              ),
+              borderData: FlBorderData(
+                show: true,
+                border: Border.all(color: Colors.white, width: 1),
+              ),
+              barGroups: [
+                BarChartGroupData(
+                  x: 0,
+                  barRods: [
+                    BarChartRodData(y: 15, width: 10),
+                  ],
+                ),
+                BarChartGroupData(
+                  x: 1,
+                  barRods: [
+                    BarChartRodData(y: 10, width: 10),
+                  ],
+                ),
+                BarChartGroupData(
+                  x: 2,
+                  barRods: [
+                    BarChartRodData(y: 18, width: 10),
+                  ],
+                ),
+              ],
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}
-- 
GitLab