Added Monitorized Areas

This commit is contained in:
Vicente J. Ruiz Jurado 2018-08-06 10:34:32 +02:00
parent 864ee28584
commit e50ff5179b
16 changed files with 223 additions and 19 deletions

View file

@ -12,7 +12,7 @@ This app is developed with [flutter](https://flutter.io/). So if you want to con
You also needs a: You also needs a:
- `assets/private-settings.json` see `assets/private-settings-sample.json`. - `assets/private-settings.json` see `assets/private-settings-sample.json`.
- `google-services.json` provided by firebase console. - `google-services.json` provided by firebase console (see also the sample).
Some json related code is generated via Some json related code is generated via
``` ```

View file

@ -6,6 +6,7 @@ import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import 'package:redux/src/store.dart'; import 'package:redux/src/store.dart';
import 'monitoredAreas.dart';
import 'activeFires.dart'; import 'activeFires.dart';
import 'fireAlert.dart'; import 'fireAlert.dart';
import 'fireNotificationList.dart'; import 'fireNotificationList.dart';
@ -44,6 +45,7 @@ class _FiresAppState extends State<FiresApp> {
SupportPage.routeName: (BuildContext context) => new SupportPage(), SupportPage.routeName: (BuildContext context) => new SupportPage(),
FireNotificationList.routeName: (BuildContext context) => FireNotificationList.routeName: (BuildContext context) =>
new FireNotificationList(), new FireNotificationList(),
MonitoredAreasPage.routeName: (BuildContext context) => new MonitoredAreasPage()
}; };
final Store<AppState> store; final Store<AppState> store;

View file

@ -58,11 +58,14 @@ class S implements WidgetsLocalizations {
String get firesInYourPlaces => "Active fires in your places"; String get firesInYourPlaces => "Active fires in your places";
String get firesNearPlace => "Fires near other place"; String get firesNearPlace => "Fires near other place";
String get getAlertsOfFiresinThatArea => "Get alerts of fires in that area"; String get getAlertsOfFiresinThatArea => "Get alerts of fires in that area";
String get inGreenMonitoredAreas => "In green, the areas monitored by our users currently";
String get isYourUbicationEnabled => "I cannot get your current location. It's your ubication enabled?"; String get isYourUbicationEnabled => "I cannot get your current location. It's your ubication enabled?";
String get itSeemsAControlledBurning => "It's a controlled burning"; String get itSeemsAControlledBurning => "It's a controlled burning";
String get itSeemsAFalseAlarm => "It seems a false alarm"; String get itSeemsAFalseAlarm => "It seems a false alarm";
String get itSeemsAIndustry => "It's an industry"; String get itSeemsAIndustry => "It's an industry";
String get itSeemsNotAtForesFire => "It seems that this is not a forest fire."; String get itSeemsNotAtForesFire => "It seems that this is not a forest fire.";
String get mapPrivacy => "In order to preserve the privacy of our users, the reflected data are randomly altered and are only indicative.";
String get monitoredAreasTitle => "Monitored areas";
String get noConnectivity => "This application needs an Internet connection to work"; String get noConnectivity => "This application needs an Internet connection to work";
String get noFiresAround => "There is no fires"; String get noFiresAround => "There is no fires";
String get notAWildfire => "Isn't that a forest fire?"; String get notAWildfire => "Isn't that a forest fire?";
@ -163,6 +166,8 @@ class es extends S {
@override @override
String get chooseAPlace => "Elige un lugar"; String get chooseAPlace => "Elige un lugar";
@override @override
String get mapPrivacy => "Para preservar la privacidad de nuestros usuarios/as, los datos reflejados están aleatoriamente alterados y son solo orientativos.";
@override
String get firesNearPlace => "Fuegos cercanos a ti"; String get firesNearPlace => "Fuegos cercanos a ti";
@override @override
String get aYear => "un año"; String get aYear => "un año";
@ -219,8 +224,12 @@ class es extends S {
@override @override
String get confirm => "Confirmar"; String get confirm => "Confirmar";
@override @override
String get inGreenMonitoredAreas => "En verde, las zonas vigiladas por nuestros usuari@s actualmente";
@override
String get getAlertsOfFiresinThatArea => "Recibe alertas de fuegos en esa zona"; String get getAlertsOfFiresinThatArea => "Recibe alertas de fuegos en esa zona";
@override @override
String get monitoredAreasTitle => "Zonas vigiladas";
@override
String get warningThisIsAVeryLargeArea => "Cuidado: esta zona es muy grande"; String get warningThisIsAVeryLargeArea => "Cuidado: esta zona es muy grande";
@override @override
String get appMoto => "Fortaleciendo vecindarios contra incendios forestales"; String get appMoto => "Fortaleciendo vecindarios contra incendios forestales";

View file

@ -9,6 +9,7 @@ import 'customBottomAppBar.dart';
import 'customMoment.dart'; import 'customMoment.dart';
import 'generated/i18n.dart'; import 'generated/i18n.dart';
import 'models/appState.dart'; import 'models/appState.dart';
import 'models/falsePositiveTypes.dart';
import 'models/fireMapState.dart'; import 'models/fireMapState.dart';
typedef void OnSave(); typedef void OnSave();
@ -82,19 +83,28 @@ class GenericMapBottom extends StatelessWidget {
S.of(context).itSeemsNotAtForesFire, S.of(context).itSeemsNotAtForesFire,
style: new TextStyle(color: fires600))) style: new TextStyle(color: fires600)))
: null, : null,
new DropdownButton<String>( new DropdownButton<FalsePositiveType>(
style: new TextStyle( style: new TextStyle(
color: Colors.black, color: Colors.black,
// fontSize: 18.0, // fontSize: 18.0,
), ),
hint: new Text(S.of(context).notAWildfire), hint: new Text(S.of(context).notAWildfire),
items: <String>[ items: FalsePositiveType.values
S.of(context).itSeemsAIndustry, .map((FalsePositiveType value) {
S.of(context).itSeemsAControlledBurning, String menuText;
S.of(context).itSeemsAFalseAlarm switch (value) {
].map((String value) { case FalsePositiveType.industry:
return new DropdownMenuItem<String>( menuText = S.of(context).itSeemsAIndustry;
value: value, child: new Text(value)); break;
case FalsePositiveType.controled:
menuText =
S.of(context).itSeemsAControlledBurning;
break;
case FalsePositiveType.falsealarm:
menuText = S.of(context).itSeemsAFalseAlarm;
}
return new DropdownMenuItem<FalsePositiveType>(
value: value, child: new Text(menuText));
}).toList(), }).toList(),
onChanged: (value) async { onChanged: (value) async {
// FIXME api call // FIXME api call

View file

@ -14,6 +14,7 @@ import 'models/appState.dart';
import 'privacyPage.dart'; import 'privacyPage.dart';
import 'sandbox.dart'; import 'sandbox.dart';
import 'supportPage.dart'; import 'supportPage.dart';
import 'monitoredAreas.dart';
@immutable @immutable
class _ViewModel { class _ViewModel {
@ -122,6 +123,16 @@ Widget mainDrawer(BuildContext context) {
Navigator.pushNamed(context, FireNotificationList.routeName); Navigator.pushNamed(context, FireNotificationList.routeName);
}, },
), ),
globals.isDevelopment
? new ListTile(
leading: const Icon(Icons.map),
title: new Text(S.of(context).monitoredAreasTitle),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, MonitoredAreasPage.routeName);
},
)
: null,
new Divider(), new Divider(),
new ListTile( new ListTile(
leading: const Icon(Icons.favorite), leading: const Icon(Icons.favorite),

View file

@ -1,16 +1,18 @@
import 'dart:async'; import 'dart:async';
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:connectivity/connectivity.dart';
import 'package:fires_flutter/models/fireNotification.dart'; import 'package:fires_flutter/models/fireNotification.dart';
import 'package:fires_flutter/models/yourLocation.dart'; import 'package:fires_flutter/models/yourLocation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:connectivity/connectivity.dart';
import 'fireMapState.dart'; import 'fireMapState.dart';
import 'user.dart'; import 'user.dart';
export 'fireMapState.dart'; export 'fireMapState.dart';
import 'package:flutter_map/flutter_map.dart';
part 'appState.g.dart'; part 'appState.g.dart';
@ -36,6 +38,8 @@ class AppState extends Object with _$AppStateSerializerMixin {
final List<YourLocation> yourLocations; final List<YourLocation> yourLocations;
final List<FireNotification> fireNotifications; final List<FireNotification> fireNotifications;
@JsonKey(ignore: true) @JsonKey(ignore: true)
final List<Polyline> monitoredAreas;
@JsonKey(ignore: true)
final int fireNotificationsUnread; final int fireNotificationsUnread;
@JsonKey(ignore: true) @JsonKey(ignore: true)
final FireMapState fireMapState; final FireMapState fireMapState;
@ -58,7 +62,8 @@ class AppState extends Object with _$AppStateSerializerMixin {
this.firesApiKey, this.firesApiKey,
this.firesApiUrl, this.firesApiUrl,
this.serverUrl, this.serverUrl,
this.connectivity, this.connectivity,
this.monitoredAreas,
this.fireMapState: const FireMapState.initial()}); this.fireMapState: const FireMapState.initial()});
AppState copyWith( AppState copyWith(
@ -74,6 +79,7 @@ class AppState extends Object with _$AppStateSerializerMixin {
List<FireNotification> fireNotifications, List<FireNotification> fireNotifications,
int fireNotificationsUnread, int fireNotificationsUnread,
FireMapState fireMapState, FireMapState fireMapState,
List<Polyline> monitoredAreas,
ConnectivityResult connectivity}) { ConnectivityResult connectivity}) {
return new AppState( return new AppState(
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
@ -88,13 +94,15 @@ class AppState extends Object with _$AppStateSerializerMixin {
fireNotifications: fireNotifications ?? this.fireNotifications, fireNotifications: fireNotifications ?? this.fireNotifications,
fireNotificationsUnread: fireNotificationsUnread:
fireNotificationsUnread ?? this.fireNotificationsUnread, fireNotificationsUnread ?? this.fireNotificationsUnread,
monitoredAreas: monitoredAreas ?? this.monitoredAreas,
fireMapState: fireMapState ?? this.fireMapState, fireMapState: fireMapState ?? this.fireMapState,
connectivity: connectivity ?? this.connectivity); connectivity: connectivity ?? this.connectivity);
} }
@override @override
String toString() { String toString() {
return 'AppState{\nuser: ${user}\nconnectivity: ${connectivity.toString()}\nisLoading: $isLoading\nisLoaded: $isLoaded\napiKey: ${ellipse( return 'AppState{\nuser: ${user}\nconnectivity: ${connectivity
.toString()}\nisLoading: $isLoading\nisLoaded: $isLoaded\napiKey: ${ellipse(
firesApiKey, firesApiKey,
8)}\napiUrl: ${firesApiUrl}\nserverUrl: ${serverUrl}\nfireMapState: $fireMapState\nyourLocations count: ${yourLocations 8)}\napiUrl: ${firesApiUrl}\nserverUrl: ${serverUrl}\nfireMapState: $fireMapState\nyourLocations count: ${yourLocations
.length}\nunread notif: ${fireNotificationsUnread}\nfireNotifications: ${fireNotifications}\nyourLocations: ${yourLocations}}'; .length}\nunread notif: ${fireNotificationsUnread}\nfireNotifications: ${fireNotifications}\nyourLocations: ${yourLocations}}';

View file

@ -0,0 +1 @@
enum FalsePositiveType { industry, controled, falsealarm }

View file

@ -1,10 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter/material.dart';
import 'package:bson_objectid/bson_objectid.dart'; import 'package:bson_objectid/bson_objectid.dart';
import 'package:fires_flutter/models/yourLocation.dart'; import 'package:fires_flutter/models/yourLocation.dart';
import 'package:http/http.dart' as ht; import 'package:http/http.dart' as ht;
import 'package:jaguar_resty/jaguar_resty.dart' as resty; import 'package:jaguar_resty/jaguar_resty.dart' as resty;
import 'package:latlong/latlong.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
import '../redux/actions.dart'; import '../redux/actions.dart';
@ -114,7 +117,7 @@ class FiresApi {
} }
Future<UpdateYourLocationMapStatsAction> getFiresInLocation( Future<UpdateYourLocationMapStatsAction> getFiresInLocation(
{AppState state, double lat, double lon, int distance}) async { {AppState state, double lat, double lon, int distance}) async {
assert(state.firesApiUrl != null); assert(state.firesApiUrl != null);
assert(state.firesApiKey != null); assert(state.firesApiKey != null);
var url = '${state.firesApiUrl}fires-in-full/${state var url = '${state.firesApiUrl}fires-in-full/${state
@ -139,7 +142,37 @@ class FiresApi {
fires: fires, fires: fires,
falsePos: falsePos, falsePos: falsePos,
industries: industries); industries: industries);
} else throw Exception('Wrong response trying to get fire data'); } else
throw Exception('Wrong response trying to get fire data');
});
}
Future<List<Polyline>> getMonitoredAreas({AppState state}) async {
assert(state.firesApiUrl != null);
assert(state.firesApiKey != null);
var url = '${state.firesApiUrl}status/subs-public-union/${state
.firesApiKey}';
var color = const Color(0xFF145A32);
return await resty.get(url).go().then((response) {
if (response.statusCode == 200) {
var resultDecoded = json.decode(response.body);
// print(resultDecoded['data']['union']);
List<Polyline> union = [];
final multipolygon =
json.decode(resultDecoded['data']['union']['value'])['geometry']
['coordinates'];
for (List<dynamic> polygon in multipolygon) {
for (List<dynamic> hole in polygon) {
List<LatLng> points = [];
for (List<dynamic> point in hole) {
points.add(new LatLng(point[1].toDouble(), point[0].toDouble()));
}
union.add(new Polyline(points: points, color: color, strokeWidth: 3.0));
}
}
return union;
} else
throw Exception('Wrong response trying to get fire data');
}); });
} }
} }

102
lib/monitoredAreas.dart Normal file
View file

@ -0,0 +1,102 @@
import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:latlong/latlong.dart';
import 'colors.dart';
import 'customBottomAppBar.dart';
import 'generated/i18n.dart';
import 'mainDrawer.dart';
import 'models/appState.dart';
class _ViewModel {
List<Polyline> monitoredAreas;
_ViewModel(this.monitoredAreas);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _ViewModel &&
runtimeType == other.runtimeType &&
monitoredAreas == other.monitoredAreas;
@override
int get hashCode => monitoredAreas.hashCode;
}
class MonitoredAreasPage extends StatelessWidget {
static const String routeName = "monitoredAreasMap";
@override
Widget build(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>(
distinct: true,
converter: (store) {
return new _ViewModel(store.state.monitoredAreas);
},
builder: (context, view) {
return new Scaffold(
appBar:
new AppBar(title: new Text(S
.of(context)
.monitoredAreasTitle)),
drawer: MainDrawer.getDrawer(context),
bottomNavigationBar: new CustomBottomAppBar(
fabLocation: FloatingActionButtonLocation.centerDocked,
showNotch: true,
color: fires100,
mainAxisAlignment: MainAxisAlignment.center,
actions: <Widget>[new Flexible(child:
new Padding(
padding: new EdgeInsets.only(left: 10.0, right: 10.0), child:
new Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
new Text(S
.of(context)
.mapPrivacy,
style: new TextStyle(
fontStyle: FontStyle.italic, color: Colors.black38))
])))
]),
body: !(view.monitoredAreas is List)
? new SpinKitPulse(color: fires600)
: new Padding(
padding: new EdgeInsets.all(10.0),
child: new Column(
children: [
new Padding(
padding: new EdgeInsets.only(
top: 8.0, bottom: 8.0, left: 0.0, right: 0.0),
child: new Text(S
.of(context)
.inGreenMonitoredAreas),
),
new Flexible(
child: new FlutterMap(
options: new MapOptions(
center: new LatLng(53.5775, 3.106111),
zoom: 1.0,
),
layers: [
new TileLayerOptions(
urlTemplate:
"https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png",
subdomains: ['a', 'b', 'c']),
new PolylineLayerOptions(
polylines: view.monitoredAreas,
)
],
),
),
],
),
),
);
});
}
}

View file

@ -2,6 +2,8 @@ import 'package:fires_flutter/models/yourLocation.dart';
import 'package:fires_flutter/models/fireNotification.dart'; import 'package:fires_flutter/models/fireNotification.dart';
import 'dart:async'; import 'dart:async';
import 'package:connectivity/connectivity.dart'; import 'package:connectivity/connectivity.dart';
import 'package:flutter_map/flutter_map.dart';
abstract class AppActions {} abstract class AppActions {}
class FetchYourLocationsAction extends AppActions { class FetchYourLocationsAction extends AppActions {
@ -20,6 +22,8 @@ class FetchYourLocationsSucceededAction extends AppActions {
class FetchFireNotificationsAction extends AppActions {} class FetchFireNotificationsAction extends AppActions {}
class FetchMonitoredAreasAction extends AppActions {}
class FetchYourLocationsFailedAction extends AppActions { class FetchYourLocationsFailedAction extends AppActions {
final Exception error; final Exception error;
@ -55,4 +59,10 @@ class OnConnectivityChanged extends AppActions {
final ConnectivityResult connectivityResult; final ConnectivityResult connectivityResult;
OnConnectivityChanged(this.connectivityResult); OnConnectivityChanged(this.connectivityResult);
} }
class FetchMonitoredAreasSucceededAction extends AppActions {
final List<Polyline> monitoredAreas;
FetchMonitoredAreasSucceededAction(this.monitoredAreas);
}

View file

@ -9,6 +9,9 @@ AppState appReducer(AppState state, action) {
return state.copyWith(fireNotifications: action.fetchedFireNotifications, return state.copyWith(fireNotifications: action.fetchedFireNotifications,
fireNotificationsUnread: action.unreadCount); fireNotificationsUnread: action.unreadCount);
} }
if (action is FetchMonitoredAreasSucceededAction) {
return state.copyWith(monitoredAreas: action.monitoredAreas);
}
if (action is AddedFireNotificationAction) if (action is AddedFireNotificationAction)
return state.copyWith( return state.copyWith(
fireNotificationsUnread: state.fireNotificationsUnread + 1); fireNotificationsUnread: state.fireNotificationsUnread + 1);

View file

@ -214,6 +214,13 @@ void fetchDataMiddleware(Store<AppState> store, action, NextDispatcher next) {
}); });
} }
if (action is FetchMonitoredAreasAction) {
api.getMonitoredAreas(state: store.state).then((result) { // store.dispatch()
store.dispatch(
new FetchMonitoredAreasSucceededAction(result));
});
}
// Make sure our actions continue on to the reducer. // Make sure our actions continue on to the reducer.
next(action); next(action);
} }
@ -243,5 +250,6 @@ void createUser(store, lang, token) {
store.dispatch(new OnUserCreatedAction(userId)); store.dispatch(new OnUserCreatedAction(userId));
store.dispatch(new FetchYourLocationsAction()); store.dispatch(new FetchYourLocationsAction());
store.dispatch(new FetchFireNotificationsAction()); store.dispatch(new FetchFireNotificationsAction());
store.dispatch(new FetchMonitoredAreasAction());
}); });
} }

View file

@ -21,6 +21,7 @@ AppState appStateReducer(AppState prevState, action) {
isLoaded: loadedReducer(state.isLoaded, action), isLoaded: loadedReducer(state.isLoaded, action),
error: errorReducer(state.error, action), error: errorReducer(state.error, action),
fireMapState: fireMapReducer(state.fireMapState, action), fireMapState: fireMapReducer(state.fireMapState, action),
monitoredAreas: state.monitoredAreas,
firesApiKey: state.firesApiKey, firesApiKey: state.firesApiKey,
firesApiUrl: state.firesApiUrl, firesApiUrl: state.firesApiUrl,
serverUrl: state.serverUrl, serverUrl: state.serverUrl,

View file

@ -26,7 +26,7 @@ dependencies:
share: "^0.5.2" share: "^0.5.2"
# data # data
json_annotation: ^0.2.3 json_annotation: "^0.2.3"
shared_preferences: "^0.4.2" shared_preferences: "^0.4.2"
bson_objectid: "^0.1.0" bson_objectid: "^0.1.0"
comunes_flutter: comunes_flutter:

View file

@ -73,5 +73,8 @@
"itSeemsAIndustry": "It's an industry", "itSeemsAIndustry": "It's an industry",
"itSeemsAControlledBurning": "It's a controlled burning", "itSeemsAControlledBurning": "It's a controlled burning",
"itSeemsAFalseAlarm": "It seems a false alarm", "itSeemsAFalseAlarm": "It seems a false alarm",
"thanksForParticipating": "Thanks for participating" "thanksForParticipating": "Thanks for participating",
"inGreenMonitoredAreas": "In green, the areas monitored by our users currently",
"monitoredAreasTitle": "Monitored areas",
"mapPrivacy": "In order to preserve the privacy of our users, the reflected data are randomly altered and are only indicative."
} }

View file

@ -73,5 +73,8 @@
"itSeemsAIndustry": "Es una industria", "itSeemsAIndustry": "Es una industria",
"itSeemsAControlledBurning": "Es una quema controlada", "itSeemsAControlledBurning": "Es una quema controlada",
"itSeemsAFalseAlarm": "Parece una falsa alarma", "itSeemsAFalseAlarm": "Parece una falsa alarma",
"thanksForParticipating": "Gracias por Participar" "thanksForParticipating": "Gracias por Participar",
"inGreenMonitoredAreas": "En verde, las zonas vigiladas por nuestros usuari@s actualmente",
"monitoredAreasTitle": "Zonas vigiladas",
"mapPrivacy": "Para preservar la privacidad de nuestros usuarios/as, los datos reflejados están aleatoriamente alterados y son solo orientativos."
} }