altosdroid: Add offline map tab
[fw/altos] / altosdroid / src / org / altusmetrum / AltosDroid / TabMapOffline.java
1 /*
2  * Copyright © 2013 Mike Beattie <mike@ethernal.org>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 2 of the License.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
16  */
17
18 package org.altusmetrum.AltosDroid;
19
20 import java.util.Arrays;
21 import java.io.*;
22
23 import org.altusmetrum.altoslib_7.*;
24
25 import android.app.Activity;
26 import android.graphics.*;
27 import android.os.Bundle;
28 import android.support.v4.app.Fragment;
29 import android.support.v4.app.FragmentTransaction;
30 import android.view.*;
31 import android.widget.*;
32 import android.location.Location;
33 import android.content.*;
34 import android.util.Log;
35
36 public class TabMapOffline extends AltosDroidTab implements AltosMapInterface {
37         // Debugging
38         static final String TAG = "AltosDroid";
39         static final boolean D = true;
40
41         AltosDroid mAltosDroid;
42
43         AltosMap map;
44
45         Canvas  canvas;
46         Paint   paint;
47
48         private boolean pad_set;
49
50         private TextView mDistanceView;
51         private TextView mBearingView;
52         private TextView mTargetLatitudeView;
53         private TextView mTargetLongitudeView;
54         private TextView mReceiverLatitudeView;
55         private TextView mReceiverLongitudeView;
56
57         private double mapAccuracy = -1;
58
59         int     stroke_width = 20;
60
61         class MapView extends View implements ScaleGestureDetector.OnScaleGestureListener {
62
63                 ScaleGestureDetector    scale_detector;
64                 boolean                 scaling;
65
66                 protected void onDraw(Canvas view_canvas) {
67                         canvas = view_canvas;
68                         paint = new Paint(Paint.ANTI_ALIAS_FLAG);
69                         paint.setStrokeWidth(stroke_width);
70                         paint.setStrokeCap(Paint.Cap.ROUND);
71                         paint.setStrokeJoin(Paint.Join.ROUND);
72                         map.paint();
73                         canvas = null;
74                 }
75
76                 public boolean onScale(ScaleGestureDetector detector) {
77                         float   f = detector.getScaleFactor();
78                         if (D) Log.d(TAG, String.format("onScale %f\n", f));
79                         if (f <= 0.8) {
80                                 map.set_zoom(map.get_zoom() - 1);
81                                 return true;
82                         }
83                         if (f >= 1.2) {
84                                 map.set_zoom(map.get_zoom() + 1);
85                                 return true;
86                         }
87                         return false;
88                 }
89
90                 public boolean onScaleBegin(ScaleGestureDetector detector) {
91                         if (D) Log.d(TAG, String.format("onScaleBegin %f\n", detector.getScaleFactor()));
92                         return true;
93                 }
94
95                 public void onScaleEnd(ScaleGestureDetector detector) {
96                         if (D) Log.d(TAG, String.format("onScaleEnd %f\n", detector.getScaleFactor()));
97                 }
98
99                 @Override
100                 public boolean dispatchTouchEvent(MotionEvent event) {
101                         scale_detector.onTouchEvent(event);
102
103                         if (scale_detector.isInProgress()) {
104                                 scaling = true;
105                         }
106
107                         if (scaling) {
108                                 if(D) Log.d(TAG, "scale in progress\n");
109                                 if (event.getAction() == MotionEvent.ACTION_UP) {
110                                         if (D) Log.d(TAG, "scale finished\n");
111                                         scaling = false;
112                                 }
113                                 return true;
114                         }
115
116                         if (event.getAction() == MotionEvent.ACTION_DOWN) {
117                                 if(D) Log.d(TAG, String.format("down event %g %g\n", event.getX(), event.getY()));
118                                 map.touch_start((int) event.getX(), (int) event.getY(), true);
119                         } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
120                                 if(D) Log.d(TAG, String.format("continue event %g %g\n", event.getX(), event.getY()));
121                                 map.touch_continue((int) event.getX(), (int) event.getY(), true);
122                         }
123                         return true;
124                 }
125
126                 public MapView(Context context) {
127                         super(context);
128                         scale_detector = new ScaleGestureDetector(this.getContext(), this);
129                 }
130         }
131
132         class MapFragment extends Fragment {
133                 MapView map_view;
134
135                 public View onCreateView(LayoutInflater inflator, ViewGroup container, Bundle savedInstanceState) {
136                         map_view = new MapView(container.getContext());
137                         return map_view;
138                 }
139
140                 public MapFragment() {
141                 }
142         }
143
144         MapFragment map_fragment;
145
146         /* AltosMapInterface */
147
148         static  final int       WHITE = 0xffffffff;
149         static  final int       RED   = 0xffff0000;
150         static  final int       PINK  = 0xffff8080;
151         static  final int       YELLOW= 0xffffff00;
152         static  final int       CYAN  = 0xff00ffff;
153         static  final int       BLUE  = 0xff0000ff;
154         static  final int       BLACK = 0xff000000;
155
156         public static final int stateColors[] = {
157                 WHITE,  // startup
158                 WHITE,  // idle
159                 WHITE,  // pad
160                 RED,    // boost
161                 PINK,   // fast
162                 YELLOW, // coast
163                 CYAN,   // drogue
164                 BLUE,   // main
165                 BLACK,  // landed
166                 BLACK,  // invalid
167                 CYAN,   // stateless
168         };
169
170         class MapPath extends AltosMapPath {
171
172                 boolean line_in(AltosPointDouble a, AltosPointDouble b) {
173                         final Rect bounds = canvas.getClipBounds();
174                         int left = (int) Math.floor (Math.min((float) a.x, (float) b.x) - stroke_width / 2.0f);
175                         int right = (int) Math.ceil(Math.max((float) a.x, (float) b.x) + stroke_width / 2.0f);
176                         int top = (int) Math.floor(Math.min((float) a.y, (float) b.y) - stroke_width / 2.0f);
177                         int bottom = (int) Math.ceil(Math.max((float) a.y, (float) b.y) + stroke_width / 2.0f);
178
179                         return left < bounds.right && bounds.left < right &&
180                                 top < bounds.bottom && bounds.top < bottom;
181                 }
182
183                 public void paint(AltosMapTransform t) {
184                         AltosPointDouble        prev = null;
185                         int                     cur_color = paint.getColor();
186
187                         for (AltosMapPathPoint point : points) {
188                                 AltosPointDouble        cur = t.screen(point.lat_lon);
189
190                                 if (prev != null && line_in(prev, cur)) {
191                                         int color;
192                                         if (0 <= point.state && point.state < stateColors.length)
193                                                 color = stateColors[point.state];
194                                         else
195                                                 color = stateColors[AltosLib.ao_flight_invalid];
196                                         if (color != cur_color) {
197                                                 paint.setColor(color);
198                                                 cur_color = color;
199                                         }
200                                         canvas.drawLine((float) prev.x, (float) prev.y, (float) cur.x, (float) cur.y, paint);
201                                 }
202                                 prev = cur;
203                         }
204                 }
205
206                 public MapPath() {
207                         stroke_width = TabMapOffline.this.stroke_width;
208                 }
209         }
210
211         public AltosMapPath new_path() {
212                 return new MapPath();
213         }
214
215         class MapLine extends AltosMapLine {
216                 public void paint(AltosMapTransform t) {
217                 }
218
219                 public MapLine() {
220                 }
221         }
222
223         public AltosMapLine new_line() {
224                 return new MapLine();
225         }
226
227         class MapImage implements AltosImage {
228                 public Bitmap   bitmap;
229
230                 public void flush() {
231                         if (bitmap != null) {
232                                 bitmap.recycle();
233                                 bitmap = null;
234                         }
235                 }
236
237                 public MapImage(File file) {
238                         bitmap = BitmapFactory.decodeFile(file.getPath());
239                 }
240         }
241
242         public AltosImage load_image(File file) throws Exception {
243                 return new MapImage(file);
244         }
245
246         class MapMark extends AltosMapMark {
247                 public void paint(AltosMapTransform t) {
248                 }
249
250                 MapMark(double lat, double lon, int state) {
251                         super(lat, lon, state);
252                 }
253         }
254
255         public AltosMapMark new_mark(double lat, double lon, int state) {
256                 return new MapMark(lat, lon, state);
257         }
258
259         class MapTile extends AltosMapTile {
260                 public void paint(AltosMapTransform t) {
261                         AltosPointInt           pt = new AltosPointInt(t.screen(upper_left));
262
263                         if (canvas.quickReject(pt.x, pt.y, pt.x + px_size, pt.y + px_size, Canvas.EdgeType.AA))
264                                 return;
265
266                         AltosImage              altos_image = cache.get(this, store, px_size, px_size);
267
268                         MapImage                map_image = (MapImage) altos_image;
269
270                         Bitmap                  bitmap = null;
271
272                         if (map_image != null)
273                                 bitmap = map_image.bitmap;
274
275                         if (bitmap != null) {
276                                 canvas.drawBitmap(bitmap, pt.x, pt.y, paint);
277                         } else {
278                                 paint.setColor(0xff808080);
279                                 canvas.drawRect(pt.x, pt.y, pt.x + px_size, pt.y + px_size, paint);
280                                 if (t.has_location()) {
281                                         String  message = null;
282                                         switch (status) {
283                                         case AltosMapTile.loading:
284                                                 message = "Loading...";
285                                                 break;
286                                         case AltosMapTile.bad_request:
287                                                 message = "Internal error";
288                                                 break;
289                                         case AltosMapTile.failed:
290                                                 message = "Network error, check connection";
291                                                 break;
292                                         case AltosMapTile.forbidden:
293                                                 message = "Too many requests, try later";
294                                                 break;
295                                         }
296                                         if (message != null) {
297                                                 Rect    bounds = new Rect();
298                                                 paint.getTextBounds(message, 0, message.length(), bounds);
299
300                                                 int     width = bounds.right - bounds.left;
301                                                 int     height = bounds.bottom - bounds.top;
302
303                                                 float x = pt.x + px_size / 2.0f;
304                                                 float y = pt.y + px_size / 2.0f;
305                                                 x = x - width / 2.0f;
306                                                 y = y + height / 2.0f;
307                                                 paint.setColor(0xff000000);
308                                                 canvas.drawText(message, 0, message.length(), x, y, paint);
309                                         }
310                                 }
311                         }
312
313                 }
314
315                 public MapTile(AltosMapTileListener listener, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size) {
316                         super(listener, upper_left, center, zoom, maptype, px_size, 2);
317                 }
318
319         }
320
321         public AltosMapTile new_tile(AltosMapTileListener listener, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size) {
322                 return new MapTile(listener, upper_left, center, zoom, maptype, px_size);
323         }
324
325         public int width() {
326                 if (map_fragment != null && map_fragment.map_view != null)
327                         return map_fragment.map_view.getWidth();
328                 return 500;
329         }
330
331         public int height() {
332                 if (map_fragment != null && map_fragment.map_view != null)
333                         return map_fragment.map_view.getHeight();
334                 return 500;
335         }
336
337         public void repaint() {
338                 this.getActivity().runOnUiThread(new Runnable() {
339                                 public void run() {
340                                         if (map_fragment != null && map_fragment.map_view != null)
341                                                 map_fragment.map_view.invalidate();
342                                 }
343                         });
344         }
345
346         public void repaint(AltosRectangle t_damage) {
347                 final AltosRectangle damage = t_damage;
348                 this.getActivity().runOnUiThread(new Runnable() {
349                                 public void run() {
350                                         if (map_fragment != null && map_fragment.map_view != null)
351                                                 map_fragment.map_view.invalidate(damage.x, damage.y, damage.x + damage.width, damage.y + damage.height);
352                                 }
353                         });
354         }
355
356         public void set_zoom_label(String label) {
357         }
358
359         @Override
360         public void onAttach(Activity activity) {
361                 super.onAttach(activity);
362                 mAltosDroid = (AltosDroid) activity;
363                 mAltosDroid.registerTab(this);
364         }
365
366         @Override
367         public void onCreate(Bundle savedInstanceState) {
368                 super.onCreate(savedInstanceState);
369         }
370
371         @Override
372         public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
373                 View v = inflater.inflate(R.layout.tab_map, container, false);
374
375                 map_fragment = new MapFragment();
376                 map = new AltosMap(this);
377                 mDistanceView  = (TextView)v.findViewById(R.id.distance_value);
378                 mBearingView   = (TextView)v.findViewById(R.id.bearing_value);
379                 mTargetLatitudeView  = (TextView)v.findViewById(R.id.target_lat_value);
380                 mTargetLongitudeView = (TextView)v.findViewById(R.id.target_lon_value);
381                 mReceiverLatitudeView  = (TextView)v.findViewById(R.id.receiver_lat_value);
382                 mReceiverLongitudeView = (TextView)v.findViewById(R.id.receiver_lon_value);
383                 return v;
384         }
385
386         @Override
387         public void onActivityCreated(Bundle savedInstanceState) {
388                 super.onActivityCreated(savedInstanceState);
389                 getChildFragmentManager().beginTransaction().add(R.id.map, map_fragment).commit();
390         }
391  
392         @Override
393         public void onDestroyView() {
394                 super.onDestroyView();
395
396                 mAltosDroid.unregisterTab(this);
397                 mAltosDroid = null;
398                 map_fragment = null;
399
400 //              Fragment fragment = (getFragmentManager().findFragmentById(R.id.map));
401 //              FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
402 //              ft.remove(fragment);
403 //              ft.commit();
404         }
405
406         private void setupMap() {
407 /*
408                 mMap = mMapFragment.getMap();
409                 if (mMap != null) {
410                         mMap.setMyLocationEnabled(true);
411                         mMap.getUiSettings().setTiltGesturesEnabled(false);
412                         mMap.getUiSettings().setZoomControlsEnabled(false);
413
414                         mRocketMarker = mMap.addMarker(
415                                         // From: http://mapicons.nicolasmollet.com/markers/industry/military/missile-2/
416                                         new MarkerOptions().icon(BitmapDescriptorFactory.fromResource(R.drawable.rocket))
417                                                            .position(new LatLng(0,0))
418                                                            .visible(false)
419                                         );
420
421                         mPadMarker = mMap.addMarker(
422                                         new MarkerOptions().icon(BitmapDescriptorFactory.fromResource(R.drawable.pad))
423                                                            .position(new LatLng(0,0))
424                                                            .visible(false)
425                                         );
426
427                         mPolyline = mMap.addPolyline(
428                                         new PolylineOptions().add(new LatLng(0,0), new LatLng(0,0))
429                                                              .width(3)
430                                                              .color(Color.BLUE)
431                                                              .visible(false)
432                                         );
433
434                         mapLoaded = true;
435                 }
436 */
437         }
438
439         private void center(double lat, double lon, double accuracy) {
440                 if (mapAccuracy < 0 || accuracy < mapAccuracy/10) {
441                         if (map != null)
442                                 map.centre(lat, lon);
443                         mapAccuracy = accuracy;
444                 }
445         }
446
447         public String tab_name() { return "offmap"; }
448
449         public void show(AltosState state, AltosGreatCircle from_receiver, Location receiver) {
450                 if (from_receiver != null) {
451                         mBearingView.setText(String.format("%3.0f°", from_receiver.bearing));
452                         set_value(mDistanceView, AltosConvert.distance, 6, from_receiver.distance);
453                 }
454
455                 if (state != null) {
456                         map.show(state, null);
457                         if (state.gps != null) {
458                                 mTargetLatitudeView.setText(AltosDroid.pos(state.gps.lat, "N", "S"));
459                                 mTargetLongitudeView.setText(AltosDroid.pos(state.gps.lon, "E", "W"));
460                                 if (state.gps.locked && state.gps.nsat >= 4)
461                                         center (state.gps.lat, state.gps.lon, 10);
462                         }
463                 }
464
465                 if (receiver != null) {
466                         double accuracy;
467
468                         if (receiver.hasAccuracy())
469                                 accuracy = receiver.getAccuracy();
470                         else
471                                 accuracy = 1000;
472                         mReceiverLatitudeView.setText(AltosDroid.pos(receiver.getLatitude(), "N", "S"));
473                         mReceiverLongitudeView.setText(AltosDroid.pos(receiver.getLongitude(), "E", "W"));
474                         center (receiver.getLatitude(), receiver.getLongitude(), accuracy);
475                 }
476
477         }
478
479         public TabMapOffline() {
480         }
481 }