...
 
Commits (7)
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_blue","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\flutter_blue-0.7.2\\\\","dependencies":[]},{"name":"geolocator","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\geolocator-5.2.1\\\\","dependencies":["google_api_availability","location_permissions"]},{"name":"google_api_availability","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_api_availability-2.0.2\\\\","dependencies":[]},{"name":"location_permissions","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location_permissions-2.0.4+1\\\\","dependencies":[]},{"name":"path_provider","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\path_provider-1.6.0\\\\","dependencies":[]},{"name":"permission_handler","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler-4.2.0+hotfix.3\\\\","dependencies":[]},{"name":"shared_preferences","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences-0.5.7+2\\\\","dependencies":[]},{"name":"sqflite","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\sqflite-1.2.0\\\\","dependencies":[]}],"android":[{"name":"flutter_blue","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\flutter_blue-0.7.2\\\\","dependencies":[]},{"name":"geolocator","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\geolocator-5.2.1\\\\","dependencies":["google_api_availability","location_permissions"]},{"name":"google_api_availability","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_api_availability-2.0.2\\\\","dependencies":[]},{"name":"location_permissions","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location_permissions-2.0.4+1\\\\","dependencies":[]},{"name":"path_provider","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\path_provider-1.6.0\\\\","dependencies":[]},{"name":"permission_handler","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler-4.2.0+hotfix.3\\\\","dependencies":[]},{"name":"shared_preferences","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences-0.5.7+2\\\\","dependencies":[]},{"name":"sqflite","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\sqflite-1.2.0\\\\","dependencies":[]}],"macos":[{"name":"shared_preferences_macos","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences_macos-0.0.1+8\\\\","dependencies":[]},{"name":"sqflite","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\sqflite-1.2.0\\\\","dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"shared_preferences_web","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences_web-0.1.2+5\\\\","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_blue","dependencies":[]},{"name":"geolocator","dependencies":["google_api_availability","location_permissions"]},{"name":"google_api_availability","dependencies":[]},{"name":"location_permissions","dependencies":[]},{"name":"path_provider","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_macos","shared_preferences_web"]},{"name":"shared_preferences_macos","dependencies":[]},{"name":"shared_preferences_web","dependencies":[]},{"name":"sqflite","dependencies":[]}],"date_created":"2020-05-15 10:37:30.582661","version":"1.19.0-2.0.pre.71"}
\ No newline at end of file
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_blue","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\flutter_blue-0.7.2\\\\","dependencies":[]},{"name":"geolocator","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\geolocator-5.2.1\\\\","dependencies":["google_api_availability","location_permissions"]},{"name":"google_api_availability","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_api_availability-2.0.2\\\\","dependencies":[]},{"name":"location_permissions","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location_permissions-2.0.4+1\\\\","dependencies":[]},{"name":"path_provider","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\path_provider-1.6.0\\\\","dependencies":[]},{"name":"permission_handler","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler-4.2.0+hotfix.3\\\\","dependencies":[]},{"name":"shared_preferences","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences-0.5.7+2\\\\","dependencies":[]},{"name":"sqflite","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\sqflite-1.2.0\\\\","dependencies":[]}],"android":[{"name":"flutter_blue","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\flutter_blue-0.7.2\\\\","dependencies":[]},{"name":"geolocator","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\geolocator-5.2.1\\\\","dependencies":["google_api_availability","location_permissions"]},{"name":"google_api_availability","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\google_api_availability-2.0.2\\\\","dependencies":[]},{"name":"location_permissions","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\location_permissions-2.0.4+1\\\\","dependencies":[]},{"name":"path_provider","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\path_provider-1.6.0\\\\","dependencies":[]},{"name":"permission_handler","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\permission_handler-4.2.0+hotfix.3\\\\","dependencies":[]},{"name":"shared_preferences","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences-0.5.7+2\\\\","dependencies":[]},{"name":"sqflite","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\sqflite-1.2.0\\\\","dependencies":[]}],"macos":[{"name":"shared_preferences_macos","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences_macos-0.0.1+8\\\\","dependencies":[]},{"name":"sqflite","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\sqflite-1.2.0\\\\","dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"shared_preferences_web","path":"C:\\\\src\\\\flutter\\\\.pub-cache\\\\hosted\\\\pub.dartlang.org\\\\shared_preferences_web-0.1.2+5\\\\","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_blue","dependencies":[]},{"name":"geolocator","dependencies":["google_api_availability","location_permissions"]},{"name":"google_api_availability","dependencies":[]},{"name":"location_permissions","dependencies":[]},{"name":"path_provider","dependencies":[]},{"name":"permission_handler","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_macos","shared_preferences_web"]},{"name":"shared_preferences_macos","dependencies":[]},{"name":"shared_preferences_web","dependencies":[]},{"name":"sqflite","dependencies":[]}],"date_created":"2020-07-13 03:41:01.914636","version":"1.19.0-2.0.pre.49"}
\ No newline at end of file
......@@ -46,6 +46,10 @@ Integrated Geolocation to the app and visualization on the app.
### [0.0.6] Added
- (UI) Map
- (UI) Settings
- ()
### [0.0.6] Changed
- `BTLEHandler`
......
......@@ -29,8 +29,20 @@ class DataHeader {
/// @Getter for _headerSet;
bool get headerSet => this._headerSet;
Stream<bool> getWhenHeaderSet() async* {
while (true) {
yield this._headerSet;
await Future.delayed(Duration(seconds: 1));
}
}
bool isEqual(List<int> that) => ListEquality().equals(_headerData, that);
// TODO
Map<String, dynamic> jsonify() => { 'device_id' : this._headerData[0], 'url': '000000000000' };
void dispose() {
this._headerSet = false;
this._headerData = [];
}
}
\ No newline at end of file
......@@ -6,6 +6,7 @@ import 'package:logair_application/utils/utils.dart';
/// and contains methods for:
/// quick lookup,
/// conversion to POST format,
// TODO Firmware@v2 : Extra data
class DataPacket {
List<int> _data;
int _timestamp;
......@@ -14,7 +15,7 @@ class DataPacket {
double _latitude;
double _longitude;
int _altitude;
double _heading;
int _heading;
double _speed;
/* Ambient metrics */
......@@ -28,11 +29,14 @@ class DataPacket {
double _pm4;
double _pm10;
// TODO Firmware v2
String _extraData;
int timestamp() => _timestamp;
double latitude() => _latitude;
double longitude() => _longitude;
int altitude() => _altitude;
double heading() => _heading;
int heading() => _heading;
double speed() => _speed;
double temperature() => _temperature;
double pressure() => _pressure;
......@@ -48,7 +52,7 @@ class DataPacket {
this._latitude = (acquiredData[0] != '') ? parseDoubleWrapper(acquiredData[0]) : null;
this._longitude = (acquiredData[1] != '') ? parseDoubleWrapper(acquiredData[1]) : null;
this._altitude = (acquiredData[2] != '') ? int.tryParse(acquiredData[2]) : null;
this._heading = (acquiredData[3] != '') ? parseDoubleWrapper(acquiredData[3]) : null;
this._heading = (acquiredData[3] != '') ? int.tryParse(acquiredData[3]) : null;
this._speed = (acquiredData[4] != '') ? parseDoubleWrapper(acquiredData[4]) : null;
this._temperature = (acquiredData[5] != '') ? parseDoubleWrapper(acquiredData[5]) : null;
......@@ -66,11 +70,30 @@ class DataPacket {
this._latitude = position.latitude;
this._longitude = position.longitude;
this._altitude = position.altitude.toInt();
this._heading = position.heading;
this._heading = position.heading.round() % 360;
this._speed = position.speed;
}
}
}
DataPacket.fromData(this._timestamp, this._latitude, this._longitude, this._altitude, this._speed, this._heading, this._temperature, this._pressure, this._relativeHumidity, this._pm1, this._pm2_5, this._pm4, this._pm10, this._extraData);
// TODO Guard clause
DataPacket.fromMappedData(Map<String, dynamic> data) {
this._timestamp = data['timestamp_nix'];
this._latitude = data['latitude'];
this._longitude = data['longitude'];
this._altitude = data['altitude'];
this._speed = data['speed'];
this._heading = data['heading'];
this._temperature = data['temperature'];
this._relativeHumidity = data['relative_humidity'];
this._pressure = data['pressure'];
this._pm1 = data['pm_1'];
this._pm2_5 = data['pm_2_5'];
this._pm4 = data['pm_4'];
this._pm10 = data['pm_10'];
this._extraData = data['extra'];
}
String stringify() => '${this._latitude}, ${this._longitude}, ${this._temperature}, ${this._pressure}, ${this._relativeHumidity}, ${this._pm2_5}, ${this._pm10}';
......@@ -90,4 +113,28 @@ class DataPacket {
this._pm4,
this._pm10
];
Map<String, dynamic> toMap() {
return {
'timestamp_nix': this._timestamp,
'latitude': this._latitude,
'longitude': this._longitude,
'altitude': this._altitude,
'speed': this._speed,
'heading': this._heading,
'temperature': this._temperature,
'relative_humidity': this._temperature,
'pressure': this._pressure,
'pm_1': this._pm1,
'pm_2_5': this._pm2_5,
'pm_4': this._pm4,
'pm_10': this._pm10,
'extra': ''
};
}
@override
String toString() {
return 'DataPacket{time: $_timestamp, lat: $_latitude, long: $_longitude, pm1: $_pm1, pm2.5: $_pm2_5, pm4: $_pm4, pm10: $_pm10}';
}
}
\ No newline at end of file
......@@ -80,7 +80,7 @@ class BTLEHandler {
_characteristic = null;
_device = null;
_bluetoothConnectionStatus = BluetoothConnectionStatus.BTSTATUS_NOT_STREAMING;
DataHandler().dispose();
return it.disconnect();
});
......
import 'package:logair_application/logic/handlers/bluetooth_le_handler.dart';
import 'package:logair_application/logic/data_header.dart';
import 'package:logair_application/logic/data_packet.dart';
import 'package:logair_application/logic/handlers/database_handler.dart';
class DataHandler {
factory DataHandler() => _singleton;
......@@ -8,7 +9,7 @@ class DataHandler {
DataHandler._internal();
static const HEADER_SIZE = 11;
static final DataHandler _singleton = new DataHandler._internal();
static final DataHandler _singleton = new DataHandler._internal();
/// [List] containing all of the data received by this.
List<int> _data = [];
......@@ -49,11 +50,18 @@ class DataHandler {
/// If it's a [DataHeader] instance.
if (packet[0] == 123) {
print('HEADER ${String.fromCharCodes(packet)}');
bool wasSet = DataHeader().headerSet;
DataHeader().setHeader(packet);
/// Retroactively add all previous packets
if (!wasSet && DataHeader().headerSet)
_sortedData.forEach((packet) => DatabaseHandler().insertData(packet, DataHeader().deviceID));
/// Or if it is a [DataPacket] instance.
} else if (packet[0] == 91) {
_sortedData.add(DataPacket(packet));
print('PACKET ${String.fromCharCodes(packet)}');
if (DataHeader().headerSet)
DatabaseHandler().insertData(_sortedData.last, DataHeader().deviceID);
}
}
/// TODO Used for debugging, later
......@@ -89,11 +97,13 @@ class DataHandler {
}
}
@deprecated
/// Used to monitor the amount of unsent [DataPacket]s.
Stream<int> getUnsentPacketsLength() async* {
while (true) {
yield (_sortedData.length);
await Future.delayed(Duration(seconds: 1));
}
}
void dispose() {
DataHeader().dispose();
this._data.removeWhere((element) => true);
}
}
\ No newline at end of file
import 'package:logair_application/logic/data_header.dart';
import 'package:logair_application/logic/data_packet.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';
import 'package:tuple/tuple.dart';
class DatabaseHandler {
factory DatabaseHandler() => _singleton;
Future<Database> _db;
bool shouldCache;
Future<Database> get _database async {
if (_db == null)
this._db = openDatabase(
join((await getDatabasesPath()), 'logair_data.db'),
version: 1,
onCreate: (db, version) => db.execute('''CREATE TABLE data(
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT,
timestamp_nix INTEGER,
latitude REAL,
longitude REAL,
altitude INTEGER,
speed REAL,
heading INTEGER,
temperature FLOAT,
relative_humidity FLOAT,
pressure FLOAT,
pm_1 FLOAT,
pm_2_5 FLOAT,
pm_4 FLOAT,
pm_10 FLOAT,
extra TEXT,
exported TINYINT DEFAULT 0
)''')
);
return this._db;
}
// TODO REMOVE !
Future<void> recreateDB() async {
final Database database = await _database;
await database.rawQuery('DROP TABLE IF EXISTS data');
await database.rawQuery('''CREATE TABLE data(
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT,
timestamp_nix INTEGER,
latitude REAL,
longitude REAL,
altitude INTEGER,
speed REAL,
heading INTEGER,
temperature FLOAT,
relative_humidity FLOAT,
pressure FLOAT,
pm_1 FLOAT,
pm_2_5 FLOAT,
pm_4 FLOAT,
pm_10 FLOAT,
extra TEXT,
exported TINYINT DEFAULT 0
)''');
}
DatabaseHandler._internal();
static final DatabaseHandler _singleton = new DatabaseHandler._internal();
Future<void> insertData(DataPacket packet, String deviceId) async {
final Database database = await _database;
Map<String, dynamic> insert = packet.toMap();
insert['device_id'] = deviceId;
await database.insert(
'data',
insert,
conflictAlgorithm: ConflictAlgorithm.replace
);
}
Future<List<DataPacket>> get data async {
final Database database = await _database;
final List<Map<String, dynamic>> data = await database.query('data');
return List.generate(data.length, (i) => DataPacket.fromMappedData(data[i]));
}
Future<int> deleteAll() async => await (await this._database).delete('data');
Future<void> close() async => (await this._database).close();
Stream<int> getDBSize() async* {
final Database database = await _database;
while (true) {
final List<Map<String, dynamic>> data = await database.rawQuery('SELECT COUNT(*) AS count FROM data');
yield data[0]['count'] ?? 0;
await Future.delayed(Duration(seconds: 5));
}
}
Future<List<Tuple2<int, DataPacket>>> getUnsent(int limit) async {
final Database database = await _database;
final List<Map<String, dynamic>> data = await database.query('data',
where: 'exported = ?',
whereArgs: [0],
limit: limit,
orderBy: 'timestamp_nix ASC'
);
print(data);
return List.generate(data.length, (i) => Tuple2(data[i]['id'], DataPacket.fromMappedData(data[i])));
}
Future<void> setExported(List<int> ids) async {
final Database database = await _database;
print('UPDATE data SET exported = 1 WHERE id IN (${ids.join(', ')})');
int count = await database.rawUpdate('UPDATE data SET exported = 1 WHERE id IN (${ids.join(', ')})');
print('Exported $count');
}
/// Used to monitor the amount of unsent [DataPacket]s.
Stream<int> getUnsentPacketsLength() async* {
final Database database = await _database;
while (true) {
final List<Map<String, dynamic>> data = await database.rawQuery('SELECT COUNT(*) AS count FROM data WHERE exported = 0');
yield data[0]['count'] ?? 0;
await Future.delayed(Duration(seconds: 1));
}
}
Stream<Map<String, dynamic>> getLatest() async* {
final Database database = await _database;
while (true) {
final List<Map<String, dynamic>> data = await database.rawQuery('SELECT MIN(timestamp_nix) AS start, AVG(pm_1) AS pm_1, AVG(pm_2_5) AS pm_2_5, AVG(pm_4) AS pm_4, AVG(pm_10) AS pm_10 FROM (SELECT * FROM data ORDER BY id DESC LIMIT 5)');
yield data[0] ?? Map();
await Future.delayed(Duration(seconds: 5));
}
}
}
\ No newline at end of file
......@@ -3,13 +3,13 @@ import 'dart:convert';
import 'dart:collection';
import 'package:flutter_map/flutter_map.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:latlong/latlong.dart';
import 'package:logair_application/logic/data_header.dart';
import 'package:logair_application/logic/data_packet.dart';
import 'package:logair_application/logic/handlers/data_handler.dart';
import 'package:logair_application/logic/handlers/database_handler.dart';
import 'package:tuple/tuple.dart';
/// Singleton data structure that contains services enabling the application to send data to a webserver.
/// TODO Make URL user configurable.
......@@ -17,6 +17,7 @@ class NetworkHandler {
factory NetworkHandler() => _singleton;
NetworkHandler._internal() {
// TODO Preferences
/// Define the interval between attempts to send the data to the server. (default: 1 minute).
const duration = const Duration(minutes: 1);
new Timer.periodic(duration, (Timer t) => this._sendDataToServer());
......@@ -28,6 +29,7 @@ class NetworkHandler {
/// Method to send acquired data packets by using a POST requests.
void _sendDataToServer() async {
// TODO Preferences
/// The default endpoint.
String url = 'https://api.logair.unige.ch/v1/service';
......@@ -38,8 +40,10 @@ class NetworkHandler {
/// Get the map representation of a [DataHeader].
Map<String, dynamic> headerData = DataHeader().jsonify();
List<Tuple2<int, DataPacket>> databaseData = await DatabaseHandler().getUnsent(100);
/// Get all the [DataPacket]s to be sent (maximum 100).
List<DataPacket> packets = DataHandler().getDataRange(100);
List<DataPacket> packets = databaseData.map((x) => x.item2).toList();
/// Convert the [DataPacket] list to a [Map] of their [List] representations.
Map<String, List<List<dynamic>>> packetList = {
......@@ -64,7 +68,8 @@ class NetworkHandler {
if (response != null && response.statusCode == 200) {
for (int i = 0; i < packets.length; i++) {
DataHandler().pop();
}
}
await DatabaseHandler().setExported(databaseData.map((x) => x.item1).toList());
print('RESPONSE ${response.statusCode}');
this._lastServerTransmission = DateTime.now();
}
......
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tuple/tuple.dart';
/// Handler enabling the storage and use of preferences.
class PreferencesHandler {
Map<String, Tuple2<dynamic, dynamic>> _defaultValues = {
'BT.USING_ADVANCED': Tuple2(bool, false),
'BT.FREQUENT': Tuple2(String, '[ ]'),
'BT.SERVICE_UUID': Tuple2(String, '0000ffe0-0000-1000-8000-00805f9b34fb'),
'BT.CHARACTERISTIC_UUID': Tuple2(String, '0000ffe1-0000-1000-8000-00805f9b34fb'),
'MAP.USING_ADVANCED': Tuple2(bool, false),
'MAP.PROX.MAX_PER_QUERY': Tuple2(int, 200),
'MAP.PROX.MAX_AGE_SECONDS': Tuple2(int, 200)
};
static final PreferencesHandler _singleton = new PreferencesHandler._internal();
SharedPreferences _sharedPreferences;
SharedPreferences _sharedPreferencesInternal;
PreferencesHandler._internal() {
SharedPreferences.getInstance().then((SharedPreferences prefs) {
this._sharedPreferences = prefs;
Future<SharedPreferences> get _sharedPreferences async {
if (_sharedPreferencesInternal == null) {
this._sharedPreferencesInternal = await SharedPreferences.getInstance();
/// Initial Setup
if (!prefs.containsKey('BT.USING_ADVANCED')) prefs.setBool('BT.USING_ADVANCED', false);
if (!prefs.containsKey('BT.USING_ADVANCED_DEFAULT')) prefs.setBool('BT.USING_ADVANCED_DEFAULT', false);
if (!prefs.containsKey('BT.FREQUENT')) prefs.setString('BT.FREQUENT', '[ ]');
if (!prefs.containsKey('BT.FREQUENT_DEFAULT')) prefs.setString('BT.FREQUENT_DEFAULT', '[ ]');
_defaultValues.forEach((key, value) {
Function setter;
switch (value.item1) {
case int:
setter = this._sharedPreferencesInternal.setInt;
break;
case bool:
setter = this._sharedPreferencesInternal.setBool;
break;
case String:
setter = this._sharedPreferencesInternal.setString;
break;
default:
throw ArgumentError('Unexpected property type');
}
if (!this._sharedPreferencesInternal.containsKey(key)) {
setter(key, value.item2);
setter(key + '_DEFAULT', value.item2);
}
});
print('Setup preferences \n ${List.generate(this._sharedPreferencesInternal.getKeys().length, (i) => this._sharedPreferencesInternal.getKeys().elementAt(i) + ' => ' + this._sharedPreferencesInternal.get(this._sharedPreferencesInternal.getKeys().elementAt(i)).toString()).join("\n")}');
}
if (!prefs.containsKey('BT.SERVICE_UUID')) prefs.setString('BT.SERVICE_UUID', '0000ffe0-0000-1000-8000-00805f9b34fb');
if (!prefs.containsKey('BT.SERVICE_UUID_DEFAULT')) prefs.setString('BT.SERVICE_UUID_DEFAULT', '0000ffe0-0000-1000-8000-00805f9b34fb');
return this._sharedPreferencesInternal;
}
if (!prefs.containsKey('BT.CHARACTERISTIC_UUID')) prefs.setString('BT.CHARACTERISTIC_UUID', '0000ffe1-0000-1000-8000-00805f9b34fb');
if (!prefs.containsKey('BT.CHARACTERISTIC_UUID_DEFAULT')) prefs.setString('BT.CHARACTERISTIC_UUID_DEFAULT', '0000ffe1-0000-1000-8000-00805f9b34fb');
PreferencesHandler._internal();
if (!prefs.containsKey('MAP.USING_ADVANCED')) prefs.setBool('MAP.USING_ADVANCED', false);
if (!prefs.containsKey('MAP.USING_ADVANCED_DEFAULT')) prefs.setBool('MAP.USING_ADVANCED_DEFAULT', false);
Future<int> getPreferencesInt(String key) async => (await this._sharedPreferences).getInt(key);
Future<String> getPreferencesString(String key) async => (await this._sharedPreferences).getString(key);
Future<bool> getPreferencesBool(String key) async => (await this._sharedPreferences).getBool(key);
if (!prefs.containsKey('MAP.PROX.MAX_PER_QUERY')) prefs.setInt('MAP.PROX.MAX_PER_QUERY', 200);
if (!prefs.containsKey('MAP.PROX.MAX_PER_QUERY_DEFAULT')) prefs.setInt('MAP.PROX.MAX_PER_QUERY_DEFAULT', 200);
Future<dynamic> getPreferences(String key) async => (await this._sharedPreferences).get(key);
if (!prefs.containsKey('MAP.PROX.MAX_AGE_SECONDS')) prefs.setInt('MAP.PROX.MAX_AGE_SECONDS', 200);
if (!prefs.containsKey('MAP.PROX.MAX_AGE_SECONDS_DEFAULT')) prefs.setInt('MAP.PROX.MAX_AGE_SECONDS_DEFAULT', 200);
Future<void> setPreferencesInt(String key, int value) async => (await this._sharedPreferences).setInt(key, value);
Future<void> setPreferencesString(String key, String value) async => (await this._sharedPreferences).setString(key, value);
Future<void> setPreferencesBool(String key, bool value) async => (await this._sharedPreferences).setBool(key, value);
Future<void> resetPreferences<T>(String key) async {
Function getter, setter;
switch (T) {
case int:
getter = getPreferencesInt;
setter = setPreferencesInt;
break;
case bool:
getter = getPreferencesBool;
setter = setPreferencesBool;
break;
case String:
getter = getPreferencesString;
setter = setPreferencesString;
}
print('Setup Prefs ${prefs.getKeys()}');
});
T value = await getter(key + '_DEFAULT');
await setter(key, value);
}
int getPreferencesInt(String key) => this._sharedPreferences.getInt(key);
String getPreferencesString(String key) => this._sharedPreferences.getString(key);
bool getPreferencesBool(String key) => this._sharedPreferences.getBool(key);
void setPreferencesInt(String key, int value) => this._sharedPreferences.setInt(key, value);
void setPreferencesString(String key, String value) => this._sharedPreferences.setString(key, value);
void setPreferencesBool(String key, bool value) => this._sharedPreferences.setBool(key, value);
void resetAllPreferences() async {
await (await this._sharedPreferences).setBool('BT.USING_ADVANCED', false);
await (await this._sharedPreferences).setString('BT.FREQUENT', '[ ]');
await (await this._sharedPreferences).setString('BT.SERVICE_UUID', '0000ffe0-0000-1000-8000-00805f9b34fb');
await (await this._sharedPreferences).setString('BT.CHARACTERISTIC_UUID', '0000ffe1-0000-1000-8000-00805f9b34fb');
await (await this._sharedPreferences).setBool('MAP.USING_ADVANCED', false);
await (await this._sharedPreferences).setInt('MAP.PROX.MAX_PER_QUERY', 200);
await (await this._sharedPreferences).setInt('MAP.PROX.MAX_AGE_SECONDS', 200);
}
factory PreferencesHandler() => _singleton;
}
\ No newline at end of file
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:device_preview/device_preview.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:logair_application/logic/controllers/home_controller.dart';
import 'package:logair_application/logic/controllers/map_display_controller.dart';
import 'package:logair_application/logic/handlers/data_handler.dart';
import 'package:logair_application/logic/handlers/network_handler.dart';
import 'package:logair_application/logic/handlers/position_handler.dart';
import 'package:logair_application/logic/handlers/preference_handler.dart';
import 'package:logair_application/logic/handlers/bluetooth_le_handler.dart';
import 'package:logair_application/ui/routes/home.dart';
import 'package:logair_application/localization/localization_delegate.dart';
......@@ -26,16 +17,6 @@ void main() {
runApp(displayMode ? DevicePreview(child: LogAirApplication()) : LogAirApplication());
}
Future<void> _startHandlers() async {
HomeController();
BTLEHandler();
NetworkHandler();
DataHandler();
PreferencesHandler();
PositionHandler();
MapDisplayController();
}
class LogAirApplication extends StatelessWidget {
static const String _title = 'LogAir Connector';
......
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logair_application/services/battery_service.dart';
import 'package:logair_application/ui/base/custom_paint.dart';
import 'dart:math';
class BatteryVisualizerWidget extends StatelessWidget {
BatteryVisualizerWidget(this.isLeft, {Key key}) : super(key: key);
Stream<int> getBatteryLevel2() async* {
Random r = new Random();
while (true) {
yield 100; //r.nextInt(100);
await Future.delayed(Duration(seconds: 10));
}
}
final bool isLeft;
Color getColor(int value) {
if (value <= 10)
return Colors.red;
else if (10 < value && value <= 20)
return Colors.yellow;
else if (20 < value)
return Colors.green;
else
return Colors.grey[100];
}
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: this.isLeft ? BatteryService().getBatteryLevel() : getBatteryLevel2(),
initialData: 0,
builder: (context, snapshot) {
int data = snapshot.data;
return Stack(
children: [
CustomPaint(
size: Size.infinite,
painter: CardPainter(isLeft: this.isLeft, battery: data, color: getColor(data))
),
Container(
alignment: Alignment.center,
child: Text("Battery $data"),
)
]
);
}
);
}
}
\ No newline at end of file
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logair_application/ui/animation/forward_animation.dart';
import 'package:logair_application/logic/handlers/bluetooth_le_handler.dart';
import 'package:logair_application/ui/bluetooth_dialog/dialog.dart';
import 'package:logair_application/utils/enums/bluetooth_connection_status.dart';
/// Button enabling the app to connect to a PM detector device.
class BluetoothButton extends StatefulWidget {
BluetoothButton({Key key}) : super(key: key);
@override
_BluetoothButtonState createState() => _BluetoothButtonState();
}
class _BluetoothButtonState extends State<BluetoothButton> {
_BluetoothButtonState({Key key});
Color _color;
BluetoothConnectionStatus _status;
// Can ignore dart(cancel_subscriptions) because linter error, subscription canceled in dispose.
// ignore: cancel_subscriptions
StreamSubscription<BluetoothConnectionStatus> _connectionStatusListener;
/// This [Function] enables non-dirty transitions, only on value change, reducing overhead.
void connectionStatusStateChanger(BluetoothConnectionStatus newStatus) {
if (mounted && newStatus != _status) {
Color newColor;
switch (newStatus) {
case BluetoothConnectionStatus.BTSTATUS_NOT_STREAMING:
newColor = Colors.grey[350];
break;
case BluetoothConnectionStatus.BTSTATUS_DISCOVERING_SERVICES:
newColor = Colors.blue[100];
break;
case BluetoothConnectionStatus.BTSTATUS_GETTING_CHARACTERISTICS:
newColor = Colors.blue[200];
break;
case BluetoothConnectionStatus.BTSTATUS_SETTING_MTU:
newColor = Colors.blue[300];
break;
case BluetoothConnectionStatus.BTSTATUS_STREAMING:
newColor = Colors.blueAccent;
break;
default:
}
setState(() {
_color = newColor;
_status = newStatus;
});
}
}
@override
void initState() {
super.initState();
this._connectionStatusListener = BTLEHandler().getConnectionStatus().listen(connectionStatusStateChanger);
}
@override
Future<void> dispose() async {
this._connectionStatusListener.cancel();
this._connectionStatusListener = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: StreamBuilder<BluetoothConnectionStatus>(
stream: BTLEHandler().getConnectionStatus(),
initialData: BluetoothConnectionStatus.BTSTATUS_NOT_STREAMING,
builder: (context, snapshot) {
BluetoothConnectionStatus _bluetoothStatus = snapshot.data;
return Ink(
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
width: 2.0,
),
shape: BoxShape.circle,
),
child: Ink(
decoration: BoxDecoration(
border: Border.all(
color: Colors.indigoAccent,
width: 2.0,
),
color: _color,
shape: BoxShape.circle,
),
child: InkWell(
borderRadius: BorderRadius.circular(100.0),
onTap: () {
print("onTap");
if (_bluetoothStatus != BluetoothConnectionStatus.BTSTATUS_STREAMING) {
print("Tap On");
Navigator.push(
context,
PopForwardRoute(
page: BluetoothDialog(),
),
);
return true;
} else {
BTLEHandler().disconnect();
print('Tap off');
return true;
}
},
child: Padding(
padding: EdgeInsets.all(10),
child: Icon(
Icons.bluetooth,
color: Colors.white,
size: 30.0,
),
),
)
)
);
}
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
class CardPainter extends CustomPainter {
CardPainter({@required this.isLeft, @required this.battery, @required this.color});
final bool isLeft;
int battery;
Color color;
setColor(Color newColor) {
this.color = newColor;
}
@override
void paint(Canvas canvas, Size size) {
final shapeBounds = Rect.fromLTRB(0, 0, size.width, size.height);
_drawBackground(canvas, shapeBounds);
}
@override
bool shouldRepaint(CardPainter oldDelegate) {
return color != oldDelegate.color;
}
void _drawBackground(Canvas canvas, Rect shapeBounds) {
final Paint paint = Paint()..color = color;
final rectWidth = shapeBounds.width * this.battery / 100;
final backgroundPath = (this.isLeft) ?
(Path()
..moveTo(shapeBounds.left, shapeBounds.top)
..lineTo(shapeBounds.bottomLeft.dx, shapeBounds.bottomLeft.dy)
..lineTo(shapeBounds.bottomLeft.dx + rectWidth, shapeBounds.bottomRight.dy)
..lineTo(shapeBounds.bottomLeft.dx + rectWidth, shapeBounds.topRight.dy) //7
..close()) :
(Path()
..moveTo(shapeBounds.right, shapeBounds.top) //3
..lineTo(shapeBounds.bottomRight.dx, shapeBounds.bottomRight.dy)
..lineTo(shapeBounds.bottomRight.dx - rectWidth, shapeBounds.bottomRight.dy)
..lineTo(shapeBounds.topRight.dx - rectWidth, shapeBounds.topRight.dy) //7
..close()); //8
canvas.drawPath(backgroundPath, paint);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:logair_application/ui/base/battery_visualizer.dart';
import 'package:logair_application/ui/base/bluetooth_button.dart';
class HeaderWidget extends StatelessWidget {
HeaderWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
Row(
children: [
Expanded(
child: BatteryVisualizerWidget(true),
),
Expanded(
child: BatteryVisualizerWidget(false),
)
],
),
BluetoothButton(),
],
);
}
}
\ No newline at end of file
......@@ -3,9 +3,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_blue/flutter_blue.dart';
import 'package:logair_application/logic/handlers/bluetooth_le_handler.dart';
import 'package:logair_application/ui/components/bluetooth_adapter_off.dart';
import 'package:logair_application/ui/components/bluetooth_adapter_unavailable.dart';
import 'package:logair_application/ui/dialog/bluetooth_selection_dialog.dart';
import 'package:logair_application/ui/bluetooth_dialog/bluetooth_adapter_off.dart';
import 'package:logair_application/ui/bluetooth_dialog/adapter_unavailable.dart';
import 'package:logair_application/ui/bluetooth_dialog/selection_dialog.dart';
class BluetoothDialog extends StatefulWidget {
BluetoothDialog({Key key}) : super(key: key);
......@@ -61,10 +61,12 @@ class _BluetoothDialogState extends State<BluetoothDialog> {
),
Expanded(
flex: 1,
child: RaisedButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text("Back"),
)
child: Center(
child: RaisedButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text("Back"),
),
),
),
],
),
......
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logair_application/services/battery_service.dart';
class LocalBatteryVisualizerWidget extends StatelessWidget {
LocalBatteryVisualizerWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: BatteryService().getBatteryLevel(),
initialData: null,
builder: (context, snapshot) {
int data = snapshot.data;
return Container(color: Colors.blue.shade100, alignment: Alignment.center, child: Text("Battery $data"),);
},
);
}
}
\ No newline at end of file
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logair_application/ui/animation/forward_animation.dart';
import 'package:logair_application/logic/handlers/bluetooth_le_handler.dart';
import 'package:logair_application/ui/dialog/bluetooth_dialog.dart';
class BluetoothButton extends StatefulWidget {
BluetoothButton({Key key}) : super(key: key);
@override
_BluetoothButtonState createState() => _BluetoothButtonState();
}
class _BluetoothButtonState extends State<BluetoothButton> {
_BluetoothButtonState({Key key});
@override
Widget build(BuildContext context) {
return Material(
child: StreamBuilder<bool>(
stream: BTLEHandler().isDeviceConnected(),
initialData: false,
builder: (context, snapshot) {
bool _bluetoothConnected = snapshot.data;
return Ink(
decoration: BoxDecoration(
border: Border.all(
color: Colors.indigoAccent,
width: 2.0,
),
color: _bluetoothConnected ? Colors.blue[200] : Colors.white,
shape: BoxShape.circle,
),
child: InkWell(
borderRadius: BorderRadius.circular(100.0),
onTap: () {
print("onTap");
if (!_bluetoothConnected) {
print("Tap On");
Navigator.push(
context,
PopForwardRoute(
page: BluetoothDialog(),
),
);
return true;
} else {
BTLEHandler().disconnect();
print('Tap off');
return true;
}
},
child: Padding(
padding: EdgeInsets.all(10),
child: Icon(
Icons.bluetooth,
color: _bluetoothConnected ? Colors.grey : Colors.red,
size: 30.0,
),
),
)
);
}
),
);
}
}
\ No newline at end of file
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logair_application/logic/handlers/preference_handler.dart';
/// This [Widget] is a [Form] that enables users to change settings.
/// It interacts with the [PreferencesHandler] to affect changes that will be maintained on app restart.
class SettingsForm extends StatefulWidget {
SettingsForm({Key key});
@override
State<StatefulWidget> createState() => _SettingsFormState(key: key);
}
class _SettingsFormState extends State<SettingsForm> {
_SettingsFormState({Key key}) {
PreferencesHandler().getPreferencesBool('BT.USING_ADVANCED').then((value) {
if (this._advancedBTOptions == null)
setState(() {
this._advancedBTOptions = value;
});
});
PreferencesHandler().getPreferencesBool('MAP.USING_ADVANCED').then((value) {
if (this._advancedMapOptions == null)
setState(() {
this._advancedMapOptions = value;
});
});
}
final _formKey = GlobalKey<FormState>(debugLabel: 'FORM_GENERAL');
bool _advancedBTOptions;
bool _advancedMapOptions;
final _bleServiceController = TextEditingController();
final _bleCharacteristicController = TextEditingController();
final _mapQueryLimitController = TextEditingController();
final _mapQueryAgeController = TextEditingController();
@override
void dispose() {
// Clean up the controller when the widget is disposed.
_bleServiceController.dispose();
_bleCharacteristicController.dispose();
_mapQueryLimitController.dispose();
_mapQueryAgeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
/// The [Form] is comprised of multiple [ExpansionTile]s that represent settings subsections from general app management to more advanced settings.
return Form(
key: _formKey,
child: Column(
children: [
// TODO Fix renderFlex overflow
SingleChildScrollView(
child: Column(
children: [
// NETWORKING
ExpansionTile(
title: Text('Network'),
initiallyExpanded: true,
leading: Icon(Icons.network_check),
children: [
ListTile(title: Text('TODO: Max number of items per push (def 100)')),
ListTile(title: Text('TODO: Push Frequency')),
ListTile(title: Text('TODO: Push Destination')),
ListTile(title: Text('TODO: Push on 3G/4G')),
],
),
Divider(color: Colors.black,),
// CACHING
ExpansionTile(
title: Text('Local Data'),
initiallyExpanded: true,
leading: Icon(Icons.storage),
children: [
ListTile(title: Text('TODO: Delete local data'))
],
),
Divider(color: Colors.black,),
// BLUETOOTH
ExpansionTile(
title: Text('Bluetooth'),
subtitle: Text('(Advanced)'),
leading: Icon(Icons.bluetooth),
children: [
CheckboxListTile(
title: Text('Modify advanced bluetooth settings'),
value: _advancedBTOptions ?? false,
onChanged: (newValue) => setState(() {
_advancedBTOptions = newValue;
}),
controlAffinity: ListTileControlAffinity.leading,
),
Visibility(
visible: _advancedBTOptions ?? false,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Text(
'Changing these settings can break device data acquisition',
style: TextStyle(
color: Colors.red[600],
fontSize: 12
),
textAlign: TextAlign.center,
),
),
),
// TODO Service / Characteristic Explorer ?
_buildInputTile(_bleServiceController, 'BLE Service UUID', 'BT.SERVICE_UUID', _advancedBTOptions, TextInputType.text, <TextInputFormatter>[ ], RegExp('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}')),
_buildInputTile(_bleCharacteristicController, 'BLE Characteristic UUID', 'BT.CHARACTERISTIC_UUID', _advancedBTOptions, TextInputType.text, <TextInputFormatter>[ ], RegExp('[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}')),
],
),
Divider(color: Colors.black,),
// MAP
ExpansionTile(
title: Text('Map'),
subtitle: Text('(Advanced)'),
leading: Icon(Icons.map),
children: [
CheckboxListTile(
title: Text('Modify advanced map settings'),
value: _advancedMapOptions ?? false,
onChanged: (newValue) {
Future.delayed(Duration.zero, () => {
if (newValue != _advancedMapOptions) setState(() {
_advancedMapOptions = newValue;
})
});
},
controlAffinity: ListTileControlAffinity.leading,
),
Visibility(
visible: _advancedMapOptions ?? false,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Text(
'Changing these settings can have serious performance impacts on your device and battery life',
style: TextStyle(
color: Colors.red[600],
fontSize: 12
),
textAlign: TextAlign.center,
),
),
),
_buildInputTile(_mapQueryLimitController, 'Maximum other points', 'MAP.PROX.MAX_PER_QUERY', _advancedMapOptions, TextInputType.number, <TextInputFormatter>[ WhitelistingTextInputFormatter.digitsOnly ], RegExp(r'\d+')),
_buildInputTile(_mapQueryAgeController, 'Maximum age of other points (seconds)', 'MAP.PROX.MAX_AGE_SECONDS', _advancedMapOptions, TextInputType.number, <TextInputFormatter>[ WhitelistingTextInputFormatter.digitsOnly ], RegExp(r'\d+')),
],
),
],
),
),
Spacer(flex: 1), // To force the row at the bottom of the screen
/// Action buttons (Confirm, Reset)
Row(
children: [
Expanded(
flex: 1,
child: RaisedButton(
onPressed: () {
print(_formKey.currentState.validate());
if (_formKey.currentState.validate()) {
PreferencesHandler().setPreferencesBool('BT.USING_ADVANCED', _advancedBTOptions);
PreferencesHandler().setPreferencesString('BT.SERVICE_UUID', _bleServiceController.text);
PreferencesHandler().setPreferencesString('BT.CHARACTERISTIC_UUID', _bleCharacteristicController.text);
PreferencesHandler().setPreferencesBool('MAP.USING_ADVANCED', _advancedMapOptions);
}
},
child: Text('Confirm Changes'),
),
),
Expanded(
flex: 1,
child: RaisedButton(
onPressed: () async {
PreferencesHandler().resetAllPreferences();
_formKey.currentState.reset();
},
child: Text('Reset values'),
),
),
],
)
],
)
);
}
/// This method builds a tile that enables a user to change the value of a setting.
/// TODO Individual field reset button key + '_DEFAULT'.
Widget _buildInputTile(TextEditingController controller, String labelText, String key, bool enabled, TextInputType inputType, List<TextInputFormatter> inputFormatters, RegExp validation) {
PreferencesHandler().getPreferences(key).then((value) => controller.text = value.toString());
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: TextFormField(
controller: controller,
enabled: enabled,
decoration: InputDecoration(
labelText: labelText,
contentPadding: EdgeInsets.zero,
),
keyboardType: inputType,
inputFormatters: inputFormatters,
validator: (value) {
if (value.isEmpty) {
return 'The field cannot be empty';
}
RegExpMatch match = validation.firstMatch(value);
if (match != null && match.input != value) {
return 'This is an invalid value';
}
return null;
}
)
);
}
}
\ No newline at end of file
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:logair_application/logic/handlers/database_handler.dart';
import 'package:logair_application/ui/components/graph/graph_colors.dart';
import 'package:logair_application/ui/components/graph/indicator.dart';
/// WIP
/// TODO Fix display (statefulness ? Yes)
/// TODO Clear FL Spots
/// This graph should display the average quality of the air in 5 second increments.
class Graph extends StatefulWidget {
Graph({Key key}) : super(key: key);
@override
State<StatefulWidget> createState() => _GraphState();
}
class _GraphState extends State<Graph> {
_GraphState({Key key}) {
DatabaseHandler().getLatest().listen((newData) {
this.start = this.start ?? newData['start'].toDouble();
this.now = (newData['start'].toDouble() - this.start) / 1000;
print(newData);
if (newData['pm_1'] != null)
spotsPM1.add(FlSpot(this.now, newData['pm_1']));
if (newData['pm_2_5'] != null)
spotsPM2_5.add(FlSpot(this.now, newData['pm_2_5']));
if (newData['pm_4'] != null)
spotsPM4.add(FlSpot(this.now, newData['pm_4']));
if (newData['pm_10'] != null)
spotsPM10.add(FlSpot(this.now, newData['pm_10']));
});
}
int touchedIndex;
double start;
double now;
static List<FlSpot> spotsPM1 = [FlSpot(0, 0)];
static List<FlSpot> spotsPM2_5 = [FlSpot(0, 0)];
static List<FlSpot> spotsPM4 = [FlSpot(0, 0)];
static List<FlSpot> spotsPM10 = [FlSpot(0, 0)];
final LineChartBarData linePM1 = LineChartBarData(
spots: spotsPM1,
isCurved: false,
colors: [
GraphColors.PM1.color,
],
barWidth: 4,
isStrokeCapRound: true,
dotData: FlDotData(show: false),
);
final LineChartBarData linePM2_5 = LineChartBarData(
spots: spotsPM2_5,
colors: [
GraphColors.PM2_5.color,
],
barWidth: 4,
isStrokeCapRound: true,
dotData: FlDotData(show: false),
);
final LineChartBarData linePM4 = LineChartBarData(
spots: spotsPM4,
colors: [
GraphColors.PM4.color,
],
barWidth: 4,
isStrokeCapRound: true,
dotData: FlDotData(show: false),
);
final LineChartBarData linePM10 = LineChartBarData(
spots: spotsPM10,
colors: [
GraphColors.PM10.color,
],
barWidth: 4,
isStrokeCapRound: true,
dotData: FlDotData(show: false),
);
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
flex: 2,
child: AspectRatio(
aspectRatio: 1.5,
child: Padding(
padding: EdgeInsets.only(top: 10, bottom: 4, right: 4),
child: LineChart(
LineChartData(
lineTouchData: LineTouchData(
touchCallback: (lineTouchResponse) => {
print(lineTouchResponse.lineBarSpots.map((e) => e.props))
},
handleBuiltInTouches: true,
),
gridData: FlGridData(
show: true,
),
// Build from DataPacket would enable getting every 10 seconds add HH:mm:ss
titlesData: FlTitlesData(
bottomTitles: SideTitles(
showTitles: true,
reservedSize: 22,
textStyle: const TextStyle(
color: Color(0xff72719b),
fontSize: 12,
),
margin: 10,
getTitles: (value) {
if (value % 10 == 0)
return value.toString();
return '';
},
),
// TODO Set values for PM
leftTitles: SideTitles(
showTitles: true,
textStyle: const TextStyle(
color: Color(0xff75729e),
fontSize: 12,
),
getTitles: (value) {
if (value % 5 == 0)
return value.toString();
return '';
},
margin: 8,
reservedSize: 30,
),
),
borderData: FlBorderData(
border: const Border(
bottom: BorderSide(
color: Color(0xff4e4965),
width: 4,
),
),
),
minX: (now ?? 0) - 10,
maxX: (now ?? 0),
maxY: 10,
minY: 0,
clipToBorder: true,
lineBarsData: [
linePM1,
linePM2_5,
linePM4,
linePM10
],
)
),
),
),
),
Expanded(
flex: 1,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Indicator(
color: GraphColors.PM1.color,
text: 'PM 1',
isSquare: true,
),
SizedBox(
height: 4,
),
Indicator(
color: GraphColors.PM2_5.color,
text: 'PM 2.5',
isSquare: true,
),
SizedBox(
height: 4,
),
Indicator(
color: GraphColors.PM4.color,
text: 'PM 4',
isSquare: true,
),
SizedBox(
height: 4,
),
Indicator(
color: GraphColors.PM10.color,
text: 'PM 10',
isSquare: true,
),
SizedBox(
height: 18,
),
],
)
)
]
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:logair_application/ui/components/graph/graph.dart';
/// Enumeration of representative colors for the different [Graph] lines.
class GraphColors {
final String _key;
final Color _value;
const GraphColors._internal(this._key, this._value);
toString() => 'Enum.$_key ~> $_value';
Color get color => _value;
static const PM1 = const GraphColors._internal('PM1', Color(0xff0293ee));
static const PM2_5 = const GraphColors._internal('PM2.5', Color(0xfff8b250));
static const PM4 = const GraphColors._internal('PM4', Color(0xff845bef));
static const PM10 = const GraphColors._internal('PM10', Color(0xff13d38e));
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:logair_application/ui/components/graph/graph.dart';
/// Small label [Widget] that serves as Caption for the [Graph]
class Indicator extends StatelessWidget {
final Color color;
final String text;
final bool isSquare;
final double size;
final Color textColor;
const Indicator({
Key key,
this.color,
this.text,
this.isSquare,
this.size = 16,
this.textColor = const Color(0xff505050),
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 5),
child: Row(
children: <Widget>[
Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: isSquare ? BoxShape.rectangle : BoxShape.circle,
color: color,
),
),
const SizedBox(
width: 4,
),
Text(
text,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: textColor),
)
],
)
);
}
}
\ No newline at end of file
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logair_application/logic/handlers/preference_handler.dart';
class MapSettingsForm extends StatefulWidget {
MapSettingsForm({Key key});
@override
State<StatefulWidget> createState() => _MapSettingsFormState(key: key);
}
class _MapSettingsFormState extends State<MapSettingsForm> {
_MapSettingsFormState({Key key});
final _formKey = GlobalKey<FormState>();
bool _advancedBTOptions = PreferencesHandler().getPreferencesBool('BT.USING_ADVANCED');
bool _advancedMapOptions = PreferencesHandler().getPreferencesBool('MAP.USING_ADVANCED');
final _bleServiceController = TextEditingController();
final _bleCharacteristicController = TextEditingController();
final _mapQueryLimitController = TextEditingController();
final _mapQueryAgeController = TextEditingController();
@override
void dispose() {
// Clean up the controller when the widget is disposed.
_bleServiceController.dispose();
_bleCharacteristicController.dispose();
_mapQueryLimitController.dispose();
_mapQueryAgeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {