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

Merge branch 'clhp_map' into 'main'

Clhp map into main

See merge request !16
parents 53c6f51a cd8d0dea
Branches
No related tags found
1 merge request!16Clhp map into main
Showing
with 29304 additions and 66 deletions
.coverage 0 → 100644
File added
...@@ -11,12 +11,11 @@ const String mapEndpoint = "update_map"; ...@@ -11,12 +11,11 @@ const String mapEndpoint = "update_map";
// Map variables // Map variables
String selectedLake = 'Mjøsa'; // NB should be initialised to last selected lake String selectedLake = 'Mjøsa'; // NB should be initialised to last selected lake
Uint8List selectedRelation = Uint8List(0); Uint8List selectedRelation = Uint8List(0); // Initialised in init_state.dart
List<Measurement> selectedMeasurements = []; List<Measurement> selectedMeasurements = [];
List<SubDiv> selectedSubdivisions = []; List<SubDiv> selectedSubdivisions = [];
SubDiv? selectedSubDiv; SubDiv? selectedSubDiv;
LatLng mapCenter = LatLng(60.8000, 10.8471); // NB may not be necessary
DateTime ?lastUpdate; // Last time data was fetched from server DateTime ?lastUpdate; // Last time data was fetched from server
List<String> lakeSearchOptions = []; List<String> lakeSearchOptions = [];
bool internetConnection = true; bool internetConnection = true;
...@@ -33,6 +32,8 @@ final titleStyle = GoogleFonts.chakraPetch( ...@@ -33,6 +32,8 @@ final titleStyle = GoogleFonts.chakraPetch(
color: Colors.white70, color: Colors.white70,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
); );
final smallTextStyle = GoogleFonts.chakraPetch(fontSize: 13, color: textColor);
final regTextStyle = GoogleFonts.chakraPetch(fontSize: 16, color: textColor); final regTextStyle = GoogleFonts.chakraPetch(fontSize: 16, color: textColor);
final regTextStyleBig = GoogleFonts.chakraPetch(fontSize: 20, color: textColor); final regTextStyleBig = GoogleFonts.chakraPetch(fontSize: 20, color: textColor);
final chartTextStyle = GoogleFonts.chakraPetch(fontSize: 12, color: textColor); final chartTextStyle = GoogleFonts.chakraPetch(fontSize: 12, color: textColor);
......
...@@ -49,8 +49,8 @@ class SubDiv { ...@@ -49,8 +49,8 @@ class SubDiv {
double minThickness; double minThickness;
double avgThickness; double avgThickness;
LatLng center; LatLng center;
double accuracy; int accuracy;
Color color; int color;
List<IceStats> iceStats; List<IceStats> iceStats;
SubDiv({ SubDiv({
...@@ -69,14 +69,14 @@ class SubDiv { ...@@ -69,14 +69,14 @@ class SubDiv {
return SubDiv( return SubDiv(
sub_div_id: json['SubdivID'].toString(), sub_div_id: json['SubdivID'].toString(),
groupID: json['GroupID'] ?? 0, groupID: json['GroupID'] ?? 0,
minThickness: json['MinThickness'] ?? 0, minThickness: (json['MinThickness'] as num?)?.toDouble() ?? 0,
avgThickness: json['AvgThickness'] ?? 0, avgThickness: (json['AvgThickness'] as num?)?.toDouble() ?? 0,
center: json['CenLatitude'] != null && json['CenLongitude'] != null center: json['CenLatitude'] != null && json['CenLongitude'] != null
? LatLng(json['CenLatitude'], json['CenLongitude']) ? LatLng(json['CenLatitude'], json['CenLongitude'])
: LatLng(0.0, 0.0), : LatLng(0.0, 0.0),
accuracy: json['Accuracy'] ?? 0.0, accuracy: json['Accuracy'] ?? 0,
// Set grey as default color // Set grey as default color
color: json['Color'] != null ? Color(json['Color']) : Colors.grey, color: json['Color'] ?? 0,
iceStats: (json['IceStats'] as List<dynamic>?) iceStats: (json['IceStats'] as List<dynamic>?)
?.map((data) => IceStats.fromJson(data)) ?.map((data) => IceStats.fromJson(data))
.toList() ?? [], .toList() ?? [],
......
...@@ -86,6 +86,7 @@ class _DefaultPageState extends State<DefaultPage> { ...@@ -86,6 +86,7 @@ class _DefaultPageState extends State<DefaultPage> {
child: ListView( child: ListView(
children: [ children: [
MapContainerWidget( MapContainerWidget(
subdivisions: selectedSubdivisions,
measurements: selectedMeasurements, measurements: selectedMeasurements,
relation: selectedRelation, relation: selectedRelation,
serverConnection: serverConnection, serverConnection: serverConnection,
......
...@@ -26,13 +26,35 @@ Future<void> initialiseState(bool fetchSearchOptions) async { ...@@ -26,13 +26,35 @@ Future<void> initialiseState(bool fetchSearchOptions) async {
List<Measurement> measurements = fetchResult.measurements; List<Measurement> measurements = fetchResult.measurements;
selectedMeasurements = measurements; selectedMeasurements = measurements;
// Extract all _subdivisions from list of measurements
for (Measurement measurement in measurements) {
for (SubDiv subdivision in measurement.subDivs) {
selectedSubdivisions.add(subdivision);
}
}
// Sort the list of SubDiv objects based on each subdivision id
selectedSubdivisions.sort((a, b) => a.sub_div_id.compareTo(b.sub_div_id));
print("Loaded from files: Meas.len: ${selectedMeasurements.length}, rel.len: ${selectedRelation.length}"); print("Loaded from files: Meas.len: ${selectedMeasurements.length}, rel.len: ${selectedRelation.length}");
} else { // Try to fetch measurement data from server } else { // Try to fetch measurement data from server
markerListFuture = fetchMeasurements().then((fetchResult) { markerListFuture = fetchMeasurements().then((fetchResult) {
List<Measurement> measurements = fetchResult.measurements; List<Measurement> measurements = fetchResult.measurements;
selectedMeasurements = measurements;
// Extract all _subdivisions from list of measurements
for (Measurement measurement in measurements) {
for (SubDiv subdivision in measurement.subDivs) {
selectedSubdivisions.add(subdivision);
}
}
// Sort the list of SubDiv objects based on each subdivision id
selectedSubdivisions.sort((a, b) => a.sub_div_id.compareTo(b.sub_div_id));
serverConnection = fetchResult.connected; serverConnection = fetchResult.connected;
setLastLake(); setLastLake(); // Update persistent value for latest fetched lake
// Return measurements // Return measurements
return measurements; return measurements;
...@@ -52,8 +74,12 @@ Future<void> initialiseState(bool fetchSearchOptions) async { ...@@ -52,8 +74,12 @@ Future<void> initialiseState(bool fetchSearchOptions) async {
initSearchOptions(); initSearchOptions();
} }
//selectedRelation = await relationFuture; // Last lake initialised to last persistent variable, or Mjøsa if the variable is not found
selectedRelation = await relationFuture; // NB update once fixed final prefs = await SharedPreferences.getInstance();
selectedLake = prefs.getString('lasLake') ?? "Mjøsa";
// Set the selected relation
selectedRelation = await relationFuture;
selectedMeasurements = await markerListFuture; selectedMeasurements = await markerListFuture;
} }
} catch (e) { } catch (e) {
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'dart:math';
import '../../consts.dart'; import '../consts.dart';
import '../../utils/format_month.dart'; import '../utils/format_month.dart';
class BarData extends StatefulWidget { class BarData extends StatefulWidget {
const BarData({super.key}); const BarData({super.key});
...@@ -17,7 +18,7 @@ class _BarDataState extends State<BarData> { ...@@ -17,7 +18,7 @@ class _BarDataState extends State<BarData> {
// Allocate bar data dynamically from selected subdivision // Allocate bar data dynamically from selected subdivision
var barData = <int, List<double>>{}; var barData = <int, List<double>>{};
double totalHeight = 0; double totalHeight = 0.5; // Set minimum total height
int touchedIndex = -1; int touchedIndex = -1;
...@@ -30,8 +31,8 @@ class _BarDataState extends State<BarData> { ...@@ -30,8 +31,8 @@ class _BarDataState extends State<BarData> {
var entry = selectedSubDiv?.iceStats[i]; var entry = selectedSubDiv?.iceStats[i];
if (entry != null) { if (entry != null) {
barData[i] = [ barData[i] = [
entry.slushIce,
entry.blackIce, entry.blackIce,
entry.slushIce,
entry.snowDepth, entry.snowDepth,
]; ];
......
...@@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; ...@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_core/theme.dart'; import 'package:syncfusion_flutter_core/theme.dart';
import 'package:syncfusion_flutter_maps/maps.dart'; import 'package:syncfusion_flutter_maps/maps.dart';
import '../consts.dart';
import '../data_classes.dart'; import '../data_classes.dart';
/// ChoroplethMap is a stateful widget that contains a choropleth map. /// ChoroplethMap is a stateful widget that contains a choropleth map.
...@@ -13,11 +14,13 @@ class ChoroplethMap extends StatefulWidget { ...@@ -13,11 +14,13 @@ class ChoroplethMap extends StatefulWidget {
Key? key, Key? key,
required this.relation, required this.relation,
required this.measurements, required this.measurements,
required this.subdivisions,
required this.onSelectionChanged, required this.onSelectionChanged,
}) : super(key: key); }) : super(key: key);
final Uint8List relation; final Uint8List relation;
final List<Measurement> measurements; final List<Measurement> measurements;
final List<SubDiv> subdivisions;
final void Function(int selectedIndex) onSelectionChanged; final void Function(int selectedIndex) onSelectionChanged;
@override @override
...@@ -28,7 +31,6 @@ class ChoroplethMapState extends State<ChoroplethMap> { ...@@ -28,7 +31,6 @@ class ChoroplethMapState extends State<ChoroplethMap> {
int selectedIndex = -1; // Subdivision/map tile index int selectedIndex = -1; // Subdivision/map tile index
late MapShapeSource dataSource; late MapShapeSource dataSource;
late final MapZoomPanBehavior _zoomPanBehavior = MapZoomPanBehavior(); late final MapZoomPanBehavior _zoomPanBehavior = MapZoomPanBehavior();
final List<SubDiv> _subdivisions = <SubDiv>[];
void updateDataSource() { void updateDataSource() {
_initDataSource(); _initDataSource();
...@@ -41,47 +43,33 @@ class ChoroplethMapState extends State<ChoroplethMap> { ...@@ -41,47 +43,33 @@ class ChoroplethMapState extends State<ChoroplethMap> {
} }
void _initDataSource() { void _initDataSource() {
_subdivisions.clear();
// Extract all _subdivisions from list of measurements
for (Measurement measurement in widget.measurements) {
for (SubDiv subdivision in measurement.subDivs) {
_subdivisions.add(subdivision);
}
}
dataSource = MapShapeSource.memory( dataSource = MapShapeSource.memory(
widget.relation, widget.relation,
shapeDataField: 'sub_div_id', shapeDataField: 'sub_div_id',
dataCount: _subdivisions.length, dataCount: widget.subdivisions.length,
primaryValueMapper: (int index) => _subdivisions[index].sub_div_id, primaryValueMapper: (int index) => widget.subdivisions[index].sub_div_id,
shapeColorValueMapper: (int index) => _subdivisions[index].avgThickness, // NB will later be minThickness shapeColorValueMapper: (int index) => widget.subdivisions[index].avgThickness, // NB will later be minThickness
shapeColorMappers: const [ shapeColorMappers: const [
MapColorMapper(
from: -2,
to: -1,
color: Color(0xFF8C8C8C),
text: '>8'),
MapColorMapper( MapColorMapper(
from: 0, from: 0,
to: 4, to: 4,
color: Color(0xFFff0000), color: Color(0xffff0000),
text: '{0},{4}'), text: '{0},{1}'),
MapColorMapper( MapColorMapper(
from: 4, from: 4,
to: 6, to: 8,
color: Color(0xffff6a00), color: Color(0xffff6a00),
text: '6'), text: '2'),
MapColorMapper( MapColorMapper(
from: 6, from: 8,
to: 8, to: 12,
color: Color(0xFFb1ff00), color: Color(0xFFb1ff00),
text: '8'), text: '3'),
MapColorMapper( MapColorMapper(
from: 8, from: 12,
to: 400, to: 400,
color: Color(0xFF00d6ff), color: Color(0xFF00d6ff),
text: '>8'), text: '4'),
], ],
); );
} }
...@@ -101,6 +89,12 @@ class ChoroplethMapState extends State<ChoroplethMap> { ...@@ -101,6 +89,12 @@ class ChoroplethMapState extends State<ChoroplethMap> {
layers: [ layers: [
MapShapeLayer( MapShapeLayer(
source: dataSource, source: dataSource,
legend: MapLegend.bar(
MapElement.shape,
position: MapLegendPosition.bottom,
segmentSize: const Size(70.0, 7.0),
textStyle: smallTextStyle,
),
zoomPanBehavior: _zoomPanBehavior, zoomPanBehavior: _zoomPanBehavior,
strokeColor: Colors.blue.shade50, strokeColor: Colors.blue.shade50,
strokeWidth: 1, strokeWidth: 1,
......
import 'package:flutter/material.dart';
import '../../consts.dart';
class InfoLayer extends StatefulWidget {
const InfoLayer({
Key? key,
}) : super(key: key);
@override
InfoLayerState createState() => InfoLayerState();
}
class InfoLayerState extends State<InfoLayer> {
@override
void initState() {
super.initState();
}
// _buildLegendItem renders a colored circle and text to form a legend
Widget _legendItem(Color color, String text) {
return Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
text,
style: const TextStyle(
fontSize: 14,
color: Colors.white,
),
),
],
);
}
/// Builds an additional legend to explain the colors
Widget _buildLegend() {
return Column(
children: [
_legendItem(const Color(0xffff0000), "Very unsafe"),
const SizedBox(height: 10),
_legendItem(const Color(0xffff6a00), "Unsafe"),
const SizedBox(height: 10),
_legendItem(const Color(0xFFb1ff00), "Safe"),
const SizedBox(height: 10),
_legendItem(const Color(0xFF00d6ff), "Very safe"),
const SizedBox(height: 10),
],
);
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(40),
color: Colors.black.withOpacity(0.8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // Align contents vertically centered
children: [
Text(
'Color categorization',
style: subHeadingStyle,
),
const SizedBox(height: 20),
Text(
'The map shows the safety of applying x kg per y m^2',
style: regTextStyle,
textAlign: TextAlign.center,
),
const SizedBox(height: 30),
_buildLegend(),
const SizedBox(height: 30),
Text(
'Placeholder for other information...',
style: smallTextStyle,
textAlign: TextAlign.center,
),
],
),
);
}
}
...@@ -5,6 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart'; ...@@ -5,6 +5,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'osm_layer.dart'; import 'osm_layer.dart';
import 'stat_charts.dart'; import 'stat_charts.dart';
import 'info_layer.dart';
import '../../consts.dart'; import '../../consts.dart';
import 'choropleth_map.dart'; import 'choropleth_map.dart';
import '../data_classes.dart'; import '../data_classes.dart';
...@@ -14,11 +15,13 @@ import '../utils/format_month.dart'; ...@@ -14,11 +15,13 @@ import '../utils/format_month.dart';
/// MapContainerWidget is the main widget that contains the map with all /// MapContainerWidget is the main widget that contains the map with all
/// its layers, polygons and markers. /// its layers, polygons and markers.
class MapContainerWidget extends StatefulWidget { class MapContainerWidget extends StatefulWidget {
final List<SubDiv> subdivisions;
final List<Measurement> measurements; final List<Measurement> measurements;
final Uint8List relation; final Uint8List relation;
final bool serverConnection; final bool serverConnection;
const MapContainerWidget({Key? key, const MapContainerWidget({Key? key,
required this.subdivisions,
required this.measurements, required this.measurements,
required this.relation, required this.relation,
required this.serverConnection, required this.serverConnection,
...@@ -37,6 +40,8 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -37,6 +40,8 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
bool isSatTapped = false; // Satellite button tap state tracker bool isSatTapped = false; // Satellite button tap state tracker
bool isMapTapped = false; // OSM button tap state tracker bool isMapTapped = false; // OSM button tap state tracker
bool infoLayer = false; // Additional color legend visibility
Measurement? selectedMeasurement = selectedMeasurements[0]; Measurement? selectedMeasurement = selectedMeasurements[0];
// Initialise lastUpdate variable from persistent storage if server fetch fails // Initialise lastUpdate variable from persistent storage if server fetch fails
...@@ -54,14 +59,15 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -54,14 +59,15 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
} }
} }
// Tile selection handler /// Tile selection handler
void handleSelection(int index) { void handleSelection(int index) {
String indexString = index.toString(); String indexString = index.toString();
setState(() { setState(() {
selectedSubDiv = widget.subdivisions[index];
for (Measurement measurement in widget.measurements) { for (Measurement measurement in widget.measurements) {
for (SubDiv subdivision in measurement.subDivs) { for (SubDiv subdivision in measurement.subDivs) {
if (subdivision.sub_div_id == indexString) { if (subdivision.sub_div_id == indexString) {
selectedSubDiv = subdivision;
selectedMeasurement = measurement; selectedMeasurement = measurement;
break; break;
} }
...@@ -105,6 +111,7 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -105,6 +111,7 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
child: ChoroplethMap( child: ChoroplethMap(
relation: widget.relation, relation: widget.relation,
measurements: widget.measurements, measurements: widget.measurements,
subdivisions: widget.subdivisions,
onSelectionChanged: handleSelection, onSelectionChanged: handleSelection,
), ),
), ),
...@@ -117,6 +124,14 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -117,6 +124,14 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
child: OSM(markerList: widget.measurements), child: OSM(markerList: widget.measurements),
), ),
), ),
SizedBox(
width: screenWidth * boxWidth,
height: screenWidth * boxHeight,
child: Visibility(
visible: infoLayer,
child: const InfoLayer(),
),
),
Positioned( // Satellite button Positioned( // Satellite button
top: 10, top: 10,
right: 10, right: 10,
...@@ -212,6 +227,28 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -212,6 +227,28 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
), ),
), ),
), ),
Positioned( // Color info button
top: 130,
right: 10,
child: GestureDetector(
onTap: () {
setState(() {
infoLayer = !infoLayer; // Toggle satellite layer state on press
});
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: infoLayer ? const BoxDecoration( // Add decoration only when pressed
shape: BoxShape.circle,
color: Colors.grey,
) : null,
child: const Icon(
Icons.info,
color: Colors.white54,
),
),
),
),
], ],
), ),
), ),
...@@ -242,7 +279,7 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -242,7 +279,7 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 20, left: 30), // Updated padding padding: const EdgeInsets.only(top: 20, left: 30), // Custom padding
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
...@@ -251,7 +288,12 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -251,7 +288,12 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
style: titleStyle, style: titleStyle,
), ),
const Divider(), const Divider(),
const SizedBox(height: 10), // Reduced padding const SizedBox(height: 10),
Text(
'Tile ID: ${selectedSubDiv?.sub_div_id}',
style: regTextStyle,
),
const SizedBox(height: 20),
Text( Text(
'Measured at ', 'Measured at ',
style: subHeadingStyle, style: subHeadingStyle,
...@@ -266,9 +308,14 @@ class _MapContainerWidgetState extends State<MapContainerWidget> { ...@@ -266,9 +308,14 @@ class _MapContainerWidgetState extends State<MapContainerWidget> {
), ),
const SizedBox(height: contPadding), const SizedBox(height: contPadding),
Text( Text(
'Measuring point: (${selectedMeasurement?.measurementID}, ${selectedMeasurement?.measurementID})', 'Center coordinate: (${selectedSubDiv?.center.latitude}, ${selectedSubDiv?.center.longitude})',
style: regTextStyle, style: regTextStyle,
), ),
const SizedBox(height: contPadding/3),
Text(
'Data certainty: ${selectedSubDiv?.accuracy}/4',
style: subHeadingStyle,
),
], ],
), ),
), ),
......
...@@ -4,7 +4,7 @@ import 'package:flutter_map/flutter_map.dart'; ...@@ -4,7 +4,7 @@ import 'package:flutter_map/flutter_map.dart';
import '../data_classes.dart'; import '../data_classes.dart';
class OSM extends StatelessWidget { class OSM extends StatefulWidget {
final List<Measurement> markerList; final List<Measurement> markerList;
const OSM({ const OSM({
...@@ -13,10 +13,17 @@ class OSM extends StatelessWidget { ...@@ -13,10 +13,17 @@ class OSM extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { OSMState createState() => OSMState();
// Init list of polygons }
List<Polygon> polygons = [];
class OSMState extends State<OSM> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return FlutterMap( return FlutterMap(
options: MapOptions( options: MapOptions(
bounds: LatLngBounds( bounds: LatLngBounds(
...@@ -26,12 +33,9 @@ class OSM extends StatelessWidget { ...@@ -26,12 +33,9 @@ class OSM extends StatelessWidget {
), ),
children: [ children: [
TileLayer( TileLayer(
urlTemplate: "https://{s}.tile.openstreetmap.org/{z/{x}/{y}.png", urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
subdomains: const ['a', 'b', 'c'], subdomains: const ['a', 'b', 'c'],
), ),
PolygonLayer(
polygons: polygons, // Return map with list of polygons included
),
], ],
); );
} }
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'graph_data/bar_data.dart'; import 'bar_data.dart';
import '../../consts.dart'; import '../../consts.dart';
class StatCharts extends StatelessWidget { class StatCharts extends StatelessWidget {
......
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
This diff is collapsed.
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment