map_display_controller.dart 8.32 KB
Newer Older
1
2
import 'dart:async';

3
4
5
import 'package:flutter/material.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:latlong/latlong.dart';
6
import 'package:logair_application/logic/handlers/data_handler.dart';
7
import 'package:logair_application/logic/handlers/database_handler.dart';
8
9
import 'package:logair_application/logic/handlers/network_handler.dart';
import 'package:logair_application/logic/data_header.dart';
10
import 'package:logair_application/logic/data_packet.dart';
11
import 'package:logair_application/logic/handlers/position_handler.dart';
12
import 'package:logair_application/utils/enums/pm_symbol.dart';
13

14
/// The [MapDisplayController] is a singleton that handles the mapping of information.
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class MapDisplayController {
  /// Whether or not the user has assumed manual control of the [FlutterMap],  
  bool _manualMotion = false;

  /// The current [Position] of the device.
  LatLng _currentPos;

  /// The previous [Position] of the device, for drawing lines.
  LatLng _lastPos;

  /// The [FlutterMap]'s zoom level, used to keep centering stable.
  double _zoom;

  /// The [FlutterMap]s [MapController], kept unique in order to avoid incessant reloading of resources.
  MapController _controller;

  /// [List] of [Polyline] representing the evolution of the PM2.5 value.
  List<Polyline> _polylines = [];
  
  /// [Marker] representing the user's current [Position].
  Marker _marker;
36
37
38
39
40
41

  Future<void> _rebuildMarkers() async {
    List<DataPacket> packets = await DatabaseHandler().getLastPackets(200);
    packets.forEach((DataPacket packet) {
      switch (_pmSymbol) {
        case PMSymbol.PM1:
42
          this.addToList(this._currentPos, packet.pm1, 200);
43
44
          break;
        case PMSymbol.PM1:
45
          this.addToList(this._currentPos, packet.pm2_5, 200);
46
47
          break;
        case PMSymbol.PM4:
48
          this.addToList(this._currentPos, packet.pm4, 200);
49
50
          break;
        case PMSymbol.PM10:
51
          this.addToList(this._currentPos, packet.pm10, 200);
52
53
54
55
56
57
58
59
          break;
        default:
          break;
      }
    });
  }

  List<CircleMarker> _pmMarkers = <CircleMarker>[];
60
61
62

  /// [Singleton]() instantiation factory.
  factory MapDisplayController() => _singleton;
63
64

  /// @Getter for [_polylines].
65
  List<Polyline> get polylines => this._polylines;
66
67

  /// @Getter for [_marker].
68
  Marker get marker => this._marker != null ? this._marker : _buildLocationMarker(LatLng(0, 0));
69
70
71
72
73

  /// Object used to calculate the real-life distance of two [Position]s
  final Distance _distance = new Distance();


74
75
76
  List<CircleMarker> get pmMarkers => this._pmMarkers;

  PMSymbol _pmSymbol = PMSymbol.PM2_5;
77

78
79
80
81
82
  void setPMKeyIndex(PMSymbol value) { 
    print('PM Index $value gives ${value.key}');
    this._pmSymbol = value;
    this._rebuildMarkers();
  }
83

84
85
86
87
  void addToList(LatLng position, double value, int maxPoints) {
    if (this._pmMarkers.length > maxPoints)
      this._pmMarkers.removeRange(0, this._pmMarkers.length - 200);
    
88
89
    if (value != null)
      this._pmMarkers.add(_buildPMMarker(position, value));
90
  }
91
92
93
94
95
96
97
98
99
  
  /// This function is executed on first initialization of the [MapDisplayController]
  /// TODO: Throttle to 1 in 5 updates for efficiency ?
  MapDisplayController._internal() {
    /// A new [MapController] is used as the global Controller.
    /// This avoids Garbage Collection rules and enables the [FlutterMap] to survive [State] changes, and other such events.
    this._controller = new MapController();

    /// This function is hooked to the [PositionHandler]s progress, ie. when a new [Position] is acquired (once per second), it is broadcasted across the app and can be listened to.
100
    // TODO Fix this to allow external data (bounded map data)
101
    PositionHandler().getCurrentOrLastPosition().listen((posChanged) {
102
103
104
      if (posChanged == null)
        return;
      
105
106
107
108
      /// If a [BluetoothDevice] is connected, then the [DataHandler] will have a [DataPacket] available.
      DataPacket latest = DataHandler().getLatestData();

      /// If the [DataPacket] is older than 5 seconds we invalidate it.
109
      if (latest != null && latest.timestamp <= (DateTime.now().millisecondsSinceEpoch - 5000)) 
110
111
112
113
114
115
116
117
118
119
120
121
122
        latest = null;

      /// Keep track of the last [Position] to draw [Polyline]s.
      this._lastPos = this._currentPos;

      /// Convert the coorodinates to a [LatLng] structure that can be understood by [FlutterMap].
      this._currentPos = LatLng(posChanged.latitude, posChanged.longitude);

      /// If the user has not acquired manual control, then center the map onto the newer position.
      if (!this._manualMotion && this._controller != null && this._controller.ready)
        this._controller.move(this._currentPos, this._zoom);

      /// Move the [Marker] to the current [Position].
123
      // TODO: Use StreamBuilder in Map.
124
125
126
127
      this._marker = this._buildLocationMarker(this._currentPos);

      if ((this._lastPos != null && this._currentPos != null)) {// && this._distance.as(LengthUnit.Meter, this._lastPos, this._currentPos) >= 2)) {
        if (this._polylines.length == 0) {
128
129
130
          this._polylines.add(
            Polyline(
              points: [this._lastPos, this._currentPos],
131
              color: Colors.blue
132
133
            )
          );
134
135
136
137
        } else
          this._polylines.last.points.add(this._currentPos);
        
        if (latest != null) {
138
139
          switch (_pmSymbol) {
            case PMSymbol.PM1:
140
141
              if (latest.pm1 != null)
                this.addToList(this._currentPos, latest.pm1, 200);
142
              break;
143
            case PMSymbol.PM2_5:
144
145
              if (latest.pm2_5 != null)
                this.addToList(this._currentPos, latest.pm2_5, 200);
146
147
              break;
            case PMSymbol.PM4:
148
149
              if (latest.pm4 != null)
                this.addToList(this._currentPos, latest.pm4, 200);
150
151
              break;
            case PMSymbol.PM10:
152
153
              if (latest.pm10 != null)
                this.addToList(this._currentPos, latest.pm10, 200);
154
155
156
157
              break;
            default:
              break;
          }
158
159
160
        }
      }
    });
161
162

    new Timer.periodic(Duration(minutes: 1), (Timer t) {
163
      if (this._controller != null && this._controller.bounds != null) {
164
        //print('${this._controller.bounds.southWest}, ${this._controller.bounds.northEast}');
165
        NetworkHandler().getProximityPoints(DataHeader().deviceID, this._controller.bounds);
166
      }
167
    });
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
  }

  /// Internal static [Singleton]() reference.
  static final MapDisplayController _singleton = new MapDisplayController._internal();

  /// @Getter for [_currentPos]
  LatLng currentPosition() => _currentPos != null ? _currentPos : LatLng(0, 0);

  /// @Getter for [_controller]
  MapController controller() => _controller;

  /// Function called whenever the user interacts with the [FlutterMap], setting the [FlutterMap]'s viewport under their control.
  void setControllerChange(LatLng position, double zoom) {
    /// Avoid triggering full user control when only the zoom value changes.
    if (this._currentPos != null && this._distance.as(LengthUnit.Meter, position, this._currentPos) >= 10)
      this._manualMotion = true;
    this._zoom = zoom;
  }

  /// Make the map respond to automatic events once more.
  void setControllerAutomatic() => this._manualMotion = false;

  /// Enables the user to have a controlled zoom in, zoom out experience.
  void setControllerZoom(bool zoomIn) {
    /// Bound the zoomability.
193
194
195
196
    if (zoomIn && this._zoom <= 19)
      this._zoom += 0.5;
    else if (!zoomIn && this._zoom >= 1)
      this._zoom -= 0.5;
197
198
199
200
201
202
203
204
205
206
207
208
209
    this._controller.move(this.controller().center, this._zoom);
  }

  /// Stream whether or not the [FlutterMap] is under full user control,
  /// Prompt re-center on true.
  Stream<bool> isManualMotionUsed() async* {
    while (true) {
      yield this._manualMotion;
      await Future.delayed(Duration(milliseconds: 500));
    }
  }

  /// Wrapper function to build a [Marker], with default values.
210
  Marker _buildLocationMarker(LatLng position) => Marker(
211
212
213
214
215
216
217
218
    width: 20.0,
    height: 20.0,
    point: position,
    builder: (ctx) => Container(
      child: Icon(Icons.location_on, color: Colors.red,),
    ),
  );

219

220
221
222
223
  CircleMarker _buildPMMarker(LatLng position, double pmValue) { 
    return CircleMarker(
      radius: 8,
      point: position,
224
      color: this._pmSymbol.colorizeValue(pmValue).withAlpha(0xff),
225
226
227
228
      borderStrokeWidth: 1,
      borderColor: Colors.blue[300]
    );
  }
229
}