From e50ff5179b1b995119dc0ed182cbe839102b7301 Mon Sep 17 00:00:00 2001 From: "Vicente J. Ruiz Jurado" Date: Mon, 6 Aug 2018 10:34:32 +0200 Subject: [PATCH] Added Monitorized Areas --- README.md | 2 +- lib/firesApp.dart | 2 + lib/generated/i18n.dart | 9 +++ lib/genericMapBottom.dart | 26 +++++--- lib/mainDrawer.dart | 11 ++++ lib/models/appState.dart | 16 +++-- lib/models/falsePositiveTypes.dart | 1 + lib/models/firesApi.dart | 37 ++++++++++- lib/monitoredAreas.dart | 102 +++++++++++++++++++++++++++++ lib/redux/appActions.dart | 12 +++- lib/redux/appReducer.dart | 3 + lib/redux/fetchDataMiddleware.dart | 8 +++ lib/redux/reducers.dart | 1 + pubspec.yaml | 2 +- res/values/strings_en.arb | 5 +- res/values/strings_es.arb | 5 +- 16 files changed, 223 insertions(+), 19 deletions(-) create mode 100644 lib/models/falsePositiveTypes.dart create mode 100644 lib/monitoredAreas.dart diff --git a/README.md b/README.md index b2fa3e8..4865611 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This app is developed with [flutter](https://flutter.io/). So if you want to con You also needs a: - `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 ``` diff --git a/lib/firesApp.dart b/lib/firesApp.dart index 14f5725..cdb5a4a 100644 --- a/lib/firesApp.dart +++ b/lib/firesApp.dart @@ -6,6 +6,7 @@ import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; import 'package:redux/src/store.dart'; +import 'monitoredAreas.dart'; import 'activeFires.dart'; import 'fireAlert.dart'; import 'fireNotificationList.dart'; @@ -44,6 +45,7 @@ class _FiresAppState extends State { SupportPage.routeName: (BuildContext context) => new SupportPage(), FireNotificationList.routeName: (BuildContext context) => new FireNotificationList(), + MonitoredAreasPage.routeName: (BuildContext context) => new MonitoredAreasPage() }; final Store store; diff --git a/lib/generated/i18n.dart b/lib/generated/i18n.dart index 18c1d3e..3c1d726 100644 --- a/lib/generated/i18n.dart +++ b/lib/generated/i18n.dart @@ -58,11 +58,14 @@ class S implements WidgetsLocalizations { String get firesInYourPlaces => "Active fires in your places"; String get firesNearPlace => "Fires near other place"; 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 itSeemsAControlledBurning => "It's a controlled burning"; String get itSeemsAFalseAlarm => "It seems a false alarm"; String get itSeemsAIndustry => "It's an industry"; 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 noFiresAround => "There is no fires"; String get notAWildfire => "Isn't that a forest fire?"; @@ -163,6 +166,8 @@ class es extends S { @override String get chooseAPlace => "Elige un lugar"; @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"; @override String get aYear => "un año"; @@ -219,8 +224,12 @@ class es extends S { @override String get confirm => "Confirmar"; @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"; @override + String get monitoredAreasTitle => "Zonas vigiladas"; + @override String get warningThisIsAVeryLargeArea => "Cuidado: esta zona es muy grande"; @override String get appMoto => "Fortaleciendo vecindarios contra incendios forestales"; diff --git a/lib/genericMapBottom.dart b/lib/genericMapBottom.dart index d641e0e..b50a76b 100644 --- a/lib/genericMapBottom.dart +++ b/lib/genericMapBottom.dart @@ -9,6 +9,7 @@ import 'customBottomAppBar.dart'; import 'customMoment.dart'; import 'generated/i18n.dart'; import 'models/appState.dart'; +import 'models/falsePositiveTypes.dart'; import 'models/fireMapState.dart'; typedef void OnSave(); @@ -82,19 +83,28 @@ class GenericMapBottom extends StatelessWidget { S.of(context).itSeemsNotAtForesFire, style: new TextStyle(color: fires600))) : null, - new DropdownButton( + new DropdownButton( style: new TextStyle( color: Colors.black, // fontSize: 18.0, ), hint: new Text(S.of(context).notAWildfire), - items: [ - S.of(context).itSeemsAIndustry, - S.of(context).itSeemsAControlledBurning, - S.of(context).itSeemsAFalseAlarm - ].map((String value) { - return new DropdownMenuItem( - value: value, child: new Text(value)); + items: FalsePositiveType.values + .map((FalsePositiveType value) { + String menuText; + switch (value) { + case FalsePositiveType.industry: + menuText = S.of(context).itSeemsAIndustry; + break; + case FalsePositiveType.controled: + menuText = + S.of(context).itSeemsAControlledBurning; + break; + case FalsePositiveType.falsealarm: + menuText = S.of(context).itSeemsAFalseAlarm; + } + return new DropdownMenuItem( + value: value, child: new Text(menuText)); }).toList(), onChanged: (value) async { // FIXME api call diff --git a/lib/mainDrawer.dart b/lib/mainDrawer.dart index 8e328da..b2730cc 100644 --- a/lib/mainDrawer.dart +++ b/lib/mainDrawer.dart @@ -14,6 +14,7 @@ import 'models/appState.dart'; import 'privacyPage.dart'; import 'sandbox.dart'; import 'supportPage.dart'; +import 'monitoredAreas.dart'; @immutable class _ViewModel { @@ -122,6 +123,16 @@ Widget mainDrawer(BuildContext context) { 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 ListTile( leading: const Icon(Icons.favorite), diff --git a/lib/models/appState.dart b/lib/models/appState.dart index 05aa9a7..db930eb 100644 --- a/lib/models/appState.dart +++ b/lib/models/appState.dart @@ -1,16 +1,18 @@ import 'dart:async'; import 'package:comunes_flutter/comunes_flutter.dart'; +import 'package:connectivity/connectivity.dart'; import 'package:fires_flutter/models/fireNotification.dart'; import 'package:fires_flutter/models/yourLocation.dart'; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; -import 'package:connectivity/connectivity.dart'; + import 'fireMapState.dart'; import 'user.dart'; export 'fireMapState.dart'; +import 'package:flutter_map/flutter_map.dart'; part 'appState.g.dart'; @@ -36,6 +38,8 @@ class AppState extends Object with _$AppStateSerializerMixin { final List yourLocations; final List fireNotifications; @JsonKey(ignore: true) + final List monitoredAreas; + @JsonKey(ignore: true) final int fireNotificationsUnread; @JsonKey(ignore: true) final FireMapState fireMapState; @@ -58,7 +62,8 @@ class AppState extends Object with _$AppStateSerializerMixin { this.firesApiKey, this.firesApiUrl, this.serverUrl, - this.connectivity, + this.connectivity, + this.monitoredAreas, this.fireMapState: const FireMapState.initial()}); AppState copyWith( @@ -74,6 +79,7 @@ class AppState extends Object with _$AppStateSerializerMixin { List fireNotifications, int fireNotificationsUnread, FireMapState fireMapState, + List monitoredAreas, ConnectivityResult connectivity}) { return new AppState( isLoading: isLoading ?? this.isLoading, @@ -88,13 +94,15 @@ class AppState extends Object with _$AppStateSerializerMixin { fireNotifications: fireNotifications ?? this.fireNotifications, fireNotificationsUnread: fireNotificationsUnread ?? this.fireNotificationsUnread, + monitoredAreas: monitoredAreas ?? this.monitoredAreas, fireMapState: fireMapState ?? this.fireMapState, - connectivity: connectivity ?? this.connectivity); + connectivity: connectivity ?? this.connectivity); } @override 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, 8)}\napiUrl: ${firesApiUrl}\nserverUrl: ${serverUrl}\nfireMapState: $fireMapState\nyourLocations count: ${yourLocations .length}\nunread notif: ${fireNotificationsUnread}\nfireNotifications: ${fireNotifications}\nyourLocations: ${yourLocations}}'; diff --git a/lib/models/falsePositiveTypes.dart b/lib/models/falsePositiveTypes.dart new file mode 100644 index 0000000..4895095 --- /dev/null +++ b/lib/models/falsePositiveTypes.dart @@ -0,0 +1 @@ +enum FalsePositiveType { industry, controled, falsealarm } \ No newline at end of file diff --git a/lib/models/firesApi.dart b/lib/models/firesApi.dart index cc77a5b..e614171 100644 --- a/lib/models/firesApi.dart +++ b/lib/models/firesApi.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter/material.dart'; import 'package:bson_objectid/bson_objectid.dart'; import 'package:fires_flutter/models/yourLocation.dart'; import 'package:http/http.dart' as ht; import 'package:jaguar_resty/jaguar_resty.dart' as resty; +import 'package:latlong/latlong.dart'; import '../globals.dart' as globals; import '../redux/actions.dart'; @@ -114,7 +117,7 @@ class FiresApi { } Future 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.firesApiKey != null); var url = '${state.firesApiUrl}fires-in-full/${state @@ -139,7 +142,37 @@ class FiresApi { fires: fires, falsePos: falsePos, industries: industries); - } else throw Exception('Wrong response trying to get fire data'); + } else + throw Exception('Wrong response trying to get fire data'); + }); + } + + Future> 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 union = []; + final multipolygon = + json.decode(resultDecoded['data']['union']['value'])['geometry'] + ['coordinates']; + for (List polygon in multipolygon) { + for (List hole in polygon) { + List points = []; + for (List 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'); }); } } diff --git a/lib/monitoredAreas.dart b/lib/monitoredAreas.dart new file mode 100644 index 0000000..06305f8 --- /dev/null +++ b/lib/monitoredAreas.dart @@ -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 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( + 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: [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, + ) + ], + ), + ), + ], + ), + ), + ); + }); + } +} diff --git a/lib/redux/appActions.dart b/lib/redux/appActions.dart index 3c474d3..3017b24 100644 --- a/lib/redux/appActions.dart +++ b/lib/redux/appActions.dart @@ -2,6 +2,8 @@ import 'package:fires_flutter/models/yourLocation.dart'; import 'package:fires_flutter/models/fireNotification.dart'; import 'dart:async'; import 'package:connectivity/connectivity.dart'; +import 'package:flutter_map/flutter_map.dart'; + abstract class AppActions {} class FetchYourLocationsAction extends AppActions { @@ -20,6 +22,8 @@ class FetchYourLocationsSucceededAction extends AppActions { class FetchFireNotificationsAction extends AppActions {} +class FetchMonitoredAreasAction extends AppActions {} + class FetchYourLocationsFailedAction extends AppActions { final Exception error; @@ -55,4 +59,10 @@ class OnConnectivityChanged extends AppActions { final ConnectivityResult connectivityResult; OnConnectivityChanged(this.connectivityResult); -} \ No newline at end of file +} + +class FetchMonitoredAreasSucceededAction extends AppActions { + final List monitoredAreas; + + FetchMonitoredAreasSucceededAction(this.monitoredAreas); +} diff --git a/lib/redux/appReducer.dart b/lib/redux/appReducer.dart index 717eab0..65a1add 100644 --- a/lib/redux/appReducer.dart +++ b/lib/redux/appReducer.dart @@ -9,6 +9,9 @@ AppState appReducer(AppState state, action) { return state.copyWith(fireNotifications: action.fetchedFireNotifications, fireNotificationsUnread: action.unreadCount); } + if (action is FetchMonitoredAreasSucceededAction) { + return state.copyWith(monitoredAreas: action.monitoredAreas); + } if (action is AddedFireNotificationAction) return state.copyWith( fireNotificationsUnread: state.fireNotificationsUnread + 1); diff --git a/lib/redux/fetchDataMiddleware.dart b/lib/redux/fetchDataMiddleware.dart index 2c07021..1b3fd30 100644 --- a/lib/redux/fetchDataMiddleware.dart +++ b/lib/redux/fetchDataMiddleware.dart @@ -214,6 +214,13 @@ void fetchDataMiddleware(Store 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. next(action); } @@ -243,5 +250,6 @@ void createUser(store, lang, token) { store.dispatch(new OnUserCreatedAction(userId)); store.dispatch(new FetchYourLocationsAction()); store.dispatch(new FetchFireNotificationsAction()); + store.dispatch(new FetchMonitoredAreasAction()); }); } diff --git a/lib/redux/reducers.dart b/lib/redux/reducers.dart index 3517b81..c867825 100644 --- a/lib/redux/reducers.dart +++ b/lib/redux/reducers.dart @@ -21,6 +21,7 @@ AppState appStateReducer(AppState prevState, action) { isLoaded: loadedReducer(state.isLoaded, action), error: errorReducer(state.error, action), fireMapState: fireMapReducer(state.fireMapState, action), + monitoredAreas: state.monitoredAreas, firesApiKey: state.firesApiKey, firesApiUrl: state.firesApiUrl, serverUrl: state.serverUrl, diff --git a/pubspec.yaml b/pubspec.yaml index 5574c31..9bcbcdf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: share: "^0.5.2" # data - json_annotation: ^0.2.3 + json_annotation: "^0.2.3" shared_preferences: "^0.4.2" bson_objectid: "^0.1.0" comunes_flutter: diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index ee9cbde..4d9e29d 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -73,5 +73,8 @@ "itSeemsAIndustry": "It's an industry", "itSeemsAControlledBurning": "It's a controlled burning", "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." } \ No newline at end of file diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 5872b8b..b716575 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -73,5 +73,8 @@ "itSeemsAIndustry": "Es una industria", "itSeemsAControlledBurning": "Es una quema controlada", "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." } \ No newline at end of file