Newer
Older
import 'package:flutter/material.dart';
import 'package:flutter_datetime_picker/flutter_datetime_picker.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'databaseconnector.dart';
import 'dart:async';
import 'package:geolocator/geolocator.dart';
const AddEvent({Key? key}) : super(key: key);
@override
State<AddEvent> createState() => _AddEventPageState();
}
class _AddEventPageState extends State<AddEvent> {
late String long = "";
late String lat = "";
late String alt = "";
late StreamSubscription<Position> streamHandler; //For canceling GNSS stream on dispose
final prefs = SharedPreferences.getInstance(); // Is async
late OverlayEntry _overlayEntry; //For event creation success notifications
late Timer _overlayCloseTimer;
Future startGNSS() async {
debugPrint("Check Location Permission");
bool serviceStatus = false;
bool hasPermission = false;
serviceStatus = await Geolocator.isLocationServiceEnabled();
if(serviceStatus){
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
}else if(permission == LocationPermission.deniedForever){
debugPrint('Location permissions are permanently denied');
if(hasPermission){
debugPrint('Location permissions granted');
if(mounted){
setState(() {
//refresh the UI
});}
debugPrint('Starting location stream');
streamHandler = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 0,
)).listen((Position position) {
debugPrint('Get Location: Lat:' + position.latitude.toString() +
' Long:' + position.longitude.toString() +
' Alt:' + position.altitude.toString());
long = position.longitude.toString();
lat = position.latitude.toString();
alt = position.altitude.toString();
accuracy = position.accuracy;
if(mounted){
setState(() {
//refresh UI on update
});}
}
});
debugPrint("GPS Service is not enabled, turn on GPS location");
setState(() {
//refresh the UI
});}
@override
try {
streamHandler.cancel();
debugPrint('Cancel location stream');
}catch(e){
debugPrint('Canceling location stream failed');
}
try {
_overlayEntry.remove();
}catch(e){
debugPrint('Dispose error on overlay remove: ' + e.toString());
}
/*Async update current event configuration to shared preferences*/
final EventStoreInstance event = EventStoreInstance();
event.storeToSharedPrefs();
super.dispose();
}
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
bool _validateLatitude(value){
if (value == ""){
return true; //Empty string is valid
}
var number = num.tryParse(value);
if(number != null){
if (number >= -90.0 && number <= 90.0){
return true; // Latitude valid
}
}
return false;
}
bool _validateLongitude(value){
if (value == ""){
return true; //Empty string is valid
}
var number = num.tryParse(value);
if(number != null){
if (number >= -180.0 && number <= 180.0){
return true; // Longitude valid
}
}
return false;
}
bool _validateElevation(value){
if (value == ""){
return true; //Empty string is valid
}
var number = num.tryParse(value);
if(number != null){
if (RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch(
if (RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch(
event.currentEvent.description) || event.currentEvent.description == '') {
if(_validateLatitude(event.currentEvent.latitude)){
if(_validateLongitude(event.currentEvent.longitude)){
if(_validateElevation(event.currentEvent.elevation)){
}
}
return false;
}
bool _addButtonStatus(){
if((_validateInput() == true) && (_addButtonEnabled == true)){
return true;
}
return false;
}
Future<void> _showResultPopup(BuildContext context, String text, bool error) async {
OverlayState? overlayState = Overlay.of(context);
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
try {
_overlayEntry.remove(); // Allow only one Overlay Popup at a time
}catch(e){
debugPrint('Overlay already removed, during dispose: ' + e.toString());
}
_overlayEntry = OverlayEntry(builder: (context) {
Color backGroundColor;
Color textColor;
if (error == true){
backGroundColor = Colors.redAccent; //Style for error message
textColor = Colors.black;
}
else {
backGroundColor = Colors.greenAccent; //Style for notification
textColor = Colors.black;
}
return Stack(
alignment: Alignment.center,
children: [
Positioned(
// Position at 10% of height from bottom
bottom: MediaQuery.of(context).size.height * 0.1,
child: Material(
borderRadius: BorderRadius.circular(8.0),
color: backGroundColor, //Some transparency remains
child: Container(
padding: const EdgeInsets.all(5.0), // Space between Text and Bubble
width: MediaQuery.of(context).size.width * 0.95,
child: TextFormField(
minLines: 1,
maxLines: 5,
readOnly: true,
autofocus: false,
enabled: false,
style: TextStyle(color: textColor),
controller: TextEditingController(
text: text,
),
),
),
),
),
overlayState?.insert(_overlayEntry);
try {
_overlayCloseTimer.cancel(); // Kill old timers
}catch(e){
debugPrint('Timer cancel error: ' + e.toString());
}
_overlayCloseTimer = Timer(
() {
try {
_overlayEntry.remove(); // Allow only one Overlay Popup. NOTE: Is this a quick an dirty or a proper solution?
}catch(e){
debugPrint('Overlay already removed, during dispose: ' + e.toString());
}
},
);
}
Future<void> _storeCurrentEvent(BuildContext context) async {
final ConfigurationStoreInstance configuration = ConfigurationStoreInstance();
event.currentEvent.typeId = configuration.getEventIdFromName(event.currentEvent.type);
event.currentEvent.status = "PENDING";
await database.addEvent(event.currentEvent);
HapticFeedback.vibrate(); //Feedback that adding event succeeded
_addButtonEnabled = true; //Activate button for add more events
//_showAddSuccessOverlay(context); //Show pop up to indicated adding event succeeded.
_showResultPopup(context, "Successfully created Event !", false );
//Update timestamp in UI
var date = DateTime.now().toUtc();
var isoDate = date.toIso8601String();
event.currentEvent.startDate = isoDate;
event.currentEvent.endDate = isoDate;
@override
Widget build(BuildContext context) {
/* Get singletons to access relevant data here.*/
final ConfigurationStoreInstance configuration = ConfigurationStoreInstance();
String gnssStatusTextLine2 = "";
// Update current event coordinates from GNSS stream
eventStore.currentEvent.latitude = lat;
eventStore.currentEvent.longitude = long;
eventStore.currentEvent.elevation = alt;
var isoDate = date.toIso8601String();
eventStore.currentEvent.startDate = isoDate;
eventStore.currentEvent.endDate = isoDate;
gnssStatusText = "GNSS On";
gnssStatusTextLine2 = "No-Fix";
gnssStatusText = "GNSS On";
gnssStatusTextLine2 = "Precision "+ accuracy.toStringAsFixed(1) +"m";
gnssStatusText = "GNSS Off"; // Just display existing event coordinates
gnssStatusTextLine2 = "";
if (configuration.initialized == true) {
return Scaffold(
appBar: AppBar(
title: const Text("Add Event"),
actions: <Widget>[
Column(
children: [
Text(gnssStatusText, style: const TextStyle(fontStyle: FontStyle.italic)),
Text(gnssStatusTextLine2, style: const TextStyle(fontStyle: FontStyle.italic)),
],
),
Switch( //Enable showing all or only pending events. Default is to show only pending events
value: eventStore.gnssSync,
onChanged: (value) {
eventStore.gnssSync = value;
debugPrint('Switched to:' + eventStore.gnssSync.toString());
setState(() {
//refresh the UI
});
}),
],
)
,
body: SingleChildScrollView(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 5.0),
child:
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
const SizedBox(height: 10.0),
autovalidateMode: AutovalidateMode.always,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Label',
),
onChanged: (value){
setState(() {});
},
validator: (value) {
if (!RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch(
value!)) {
return "Only: a-z , A-Z , _ , 0-9 , ,(Comma) , ( , ) , + , - , . , :";
} else {
return null; // Entered Text is valid
}
},
),
const SizedBox(height: 15.0),
DropdownButtonFormField(
isExpanded: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Event Type',
),
items:
configuration.eventTypes.map((EventType event) {
return DropdownMenuItem(
value: event.name,
child: Text(event.name),
);
}).toList(),
onChanged: (value) {
),
const SizedBox(height: 15.0),
DropdownButtonFormField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
items:
configuration.devices.map((Device device) {
return DropdownMenuItem(
value: device.urn,
child: Text(device.urn),
);
}).toList(),
onChanged: (value) {
eventStore.currentEvent.urn = value.toString();
eventStore.currentEvent.urnId =
configuration.getDeviceIdFromUrn(value.toString());
}
),
const SizedBox(height: 15.0),
TextFormField(
autovalidateMode: AutovalidateMode.always,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Description'
setState(() {});
},
validator: (value) {
if (!RegExp(r'^[a-z A-Z . \- 0-9 , ( ) + - _ :]+$').hasMatch(
value!)) {
if(value == ''){
return null; //An empty description is also allowed.
}
return "Only: a-z , A-Z , _ , 0-9 , ,(Comma) , ( , ) , + , - , . , :";
} else {
return null; // Entered Text is valid
}
},
),
const SizedBox(height: 15.0),
Row(
//mainAxisSize: MainAxisSize.max,
//crossAxisAlignment: CrossAxisAlignment.stretch,
Flexible(
child:
TextFormField(

Maximilian Betz
committed
enabled: !eventStore.gnssSync,
controller: TextEditingController(
text: eventStore.currentEvent.startDate.substring(0, 19) + 'Z' //Do not show microseconds
),
readOnly: true,
decoration: const InputDecoration(
labelText: 'Timestamp',
//helperText: '', //Adds some space below field
border: OutlineInputBorder(),
),
onTap: () {
DatePicker.showDateTimePicker(context,
showTitleActions: true,
onConfirm: (date) {
//Only one field for start and end date.
var isoDate = date.toIso8601String();
eventStore.currentEvent.startDate = isoDate;
eventStore.currentEvent.endDate = isoDate;
debugPrint('Date set to : $isoDate');
setState(() {});
},
currentTime: DateTime.now().toUtc(),
locale: LocaleType.en);
},

Maximilian Betz
committed
),
padding: const EdgeInsets.symmetric(vertical: 20.0), //TODO: find a more dynamic solution without a fixed value

Maximilian Betz
committed
primary: eventStore.gnssSync == false ? Colors.blue : Colors.grey,

Maximilian Betz
committed
if(!eventStore.gnssSync) {
var date = DateTime.now().toUtc();
var isoDate = date.toIso8601String();
eventStore.currentEvent.startDate = isoDate;
eventStore.currentEvent.endDate = isoDate;
debugPrint('Date set to : ' +
eventStore.currentEvent.endDate.toString());
setState(() {});
}
},
child: const Text('Now'),
),
]
const SizedBox(height: 15.0),
TextFormField(
keyboardType: TextInputType.number,
autovalidateMode: AutovalidateMode.onUserInteraction,
controller: TextEditingController(
decoration: const InputDecoration(
labelText: 'Latitude',
border: OutlineInputBorder(),
),
onChanged: (value) {
setState(() {});
}
},
validator: (value) {
if (value == "") {
return null; // Empty value is allowed
}
final number = num.tryParse(value!);
if (number != null){
if (number >= -90.0 && number <= 90.0){
return null; // Latitude valid
}
),
const SizedBox(height: 15.0),
TextFormField(
keyboardType: TextInputType.number,
autovalidateMode: AutovalidateMode.onUserInteraction,
controller: TextEditingController(
decoration: const InputDecoration(
labelText: 'Longitude',
border: OutlineInputBorder(),
setState(() {});
}
},
validator: (value) {
if (value == "") {
return null; // Empty value is allowed
final number = num.tryParse(value!);
if (number != null){
if (number >= -180.0 && number <= 180.0){
return null; // Longitude valid
}
}
return "-180 => Longitude <= +180";
},
const SizedBox(height: 15.0),
TextFormField(
keyboardType: TextInputType.number,
autovalidateMode: AutovalidateMode.onUserInteraction,
controller: TextEditingController(
decoration: const InputDecoration(
labelText: 'Elevation',
border: OutlineInputBorder(),
),
onChanged: (value) {
setState(() {});
}
},
validator: (value) {
if (value == "") {
return null; // Empty value is allowed
final number = num.tryParse(value!);
if (number != null){
return null; // Elevation valid
}
return "Only numerical values for elevation in [m]";
},
bottomNavigationBar:
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [

Maximilian Betz
committed
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
margin: const EdgeInsets.symmetric(vertical: 10.0),
child: _addButtonStatus() ?
FloatingActionButton.extended(
heroTag: null,
tooltip: 'Create new event in local database',
icon: null,
label: const Text('Create Event'),
onPressed: () {
if (_validateInput()) {
_addButtonEnabled = false; //Disable button until event is stored
_storeCurrentEvent(context);
}
setState(() {});
},
):
FloatingActionButton.extended(
heroTag: null,
tooltip: 'Input invalid',
icon: null,
backgroundColor: Colors.grey,
label: const Text('Create Event'),
onPressed: () {
},
),
const SizedBox(width: 5.0),
],
),
);
}else {
return Scaffold(
appBar: AppBar(title: const Text("Add Event")),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Container(
margin: const EdgeInsets.all(10.0),
child:const Text(
'Check Configuration Page for initial setup!',
style: TextStyle(fontSize: 20)
),
),
],
),
);
}
//TODO: The app shall prefill the label with a configurable prefix and optionally a running number. E.g. prefix: (PS122.3-4_) running number: 1 label = PS122.3-4_1