altosdroid: Add map types and map preloading UIs
[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
35 public class TabMapOffline extends AltosDroidTab implements AltosMapInterface {
36
37         AltosDroid mAltosDroid;
38
39         AltosMap map;
40
41         AltosLatLon     here;
42         AltosLatLon     pad;
43
44         Canvas  canvas;
45         Paint   paint;
46
47         Bitmap  pad_bitmap;
48         int     pad_off_x, pad_off_y;
49         Bitmap  rocket_bitmap;
50         int     rocket_off_x, rocket_off_y;
51         Bitmap  here_bitmap;
52         int     here_off_x, here_off_y;
53
54         private boolean pad_set;
55
56         private TextView mDistanceView;
57         private TextView mBearingView;
58         private TextView mTargetLatitudeView;
59         private TextView mTargetLongitudeView;
60         private TextView mReceiverLatitudeView;
61         private TextView mReceiverLongitudeView;
62
63         private double mapAccuracy = -1;
64
65         int     stroke_width = 20;
66
67         private void draw_bitmap(AltosLatLon lat_lon, Bitmap bitmap, int off_x, int off_y) {
68                 if (lat_lon != null) {
69                         AltosPointInt pt = new AltosPointInt(map.transform.screen(lat_lon));
70
71                         canvas.drawBitmap(bitmap, pt.x - off_x, pt.y - off_y, paint);
72                 }
73         }
74
75         class MapView extends View implements ScaleGestureDetector.OnScaleGestureListener {
76
77                 ScaleGestureDetector    scale_detector;
78                 boolean                 scaling;
79
80                 private void draw_positions() {
81                         if (map.last_position != null && here != null) {
82                                 AltosPointDouble        rocket_screen = map.transform.screen(map.last_position);
83                                 AltosPointDouble        here_screen = map.transform.screen(here);
84                                 paint.setColor(0xff8080ff);
85                                 canvas.drawLine((float) rocket_screen.x, (float) rocket_screen.y,
86                                                 (float) here_screen.x, (float) here_screen.y, paint);
87                         }
88                         draw_bitmap(pad, pad_bitmap, pad_off_x, pad_off_y);
89                         draw_bitmap(map.last_position, rocket_bitmap, rocket_off_x, rocket_off_y);
90                         draw_bitmap(here, here_bitmap, here_off_x, here_off_y);
91                 }
92
93                 protected void onDraw(Canvas view_canvas) {
94                         canvas = view_canvas;
95                         paint = new Paint(Paint.ANTI_ALIAS_FLAG);
96                         paint.setStrokeWidth(stroke_width);
97                         paint.setStrokeCap(Paint.Cap.ROUND);
98                         paint.setStrokeJoin(Paint.Join.ROUND);
99                         map.paint();
100                         draw_positions();
101                         canvas = null;
102                 }
103
104                 public boolean onScale(ScaleGestureDetector detector) {
105                         float   f = detector.getScaleFactor();
106                         AltosDebug.debug("onScale %f\n", f);
107                         if (f <= 0.8) {
108                                 map.set_zoom(map.get_zoom() - 1);
109                                 return true;
110                         }
111                         if (f >= 1.2) {
112                                 map.set_zoom(map.get_zoom() + 1);
113                                 return true;
114                         }
115                         return false;
116                 }
117
118                 public boolean onScaleBegin(ScaleGestureDetector detector) {
119                         AltosDebug.debug("onScaleBegin %f\n", detector.getScaleFactor());
120                         return true;
121                 }
122
123                 public void onScaleEnd(ScaleGestureDetector detector) {
124                         AltosDebug.debug("onScaleEnd %f\n", detector.getScaleFactor());
125                 }
126
127                 @Override
128                 public boolean dispatchTouchEvent(MotionEvent event) {
129                         scale_detector.onTouchEvent(event);
130
131                         if (scale_detector.isInProgress()) {
132                                 scaling = true;
133                         }
134
135                         if (scaling) {
136                                 if(AltosDebug.D) AltosDebug.debug("scale in progress\n");
137                                 if (event.getAction() == MotionEvent.ACTION_UP) {
138                                         AltosDebug.debug("scale finished\n");
139                                         scaling = false;
140                                 }
141                                 return true;
142                         }
143
144                         if (event.getAction() == MotionEvent.ACTION_DOWN) {
145                                 AltosDebug.debug("down event %g %g\n", event.getX(), event.getY());
146                                 map.touch_start((int) event.getX(), (int) event.getY(), true);
147                         } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
148                                 AltosDebug.debug("continue event %g %g\n", event.getX(), event.getY());
149                                 map.touch_continue((int) event.getX(), (int) event.getY(), true);
150                         }
151                         return true;
152                 }
153
154                 public MapView(Context context) {
155                         super(context);
156                         scale_detector = new ScaleGestureDetector(this.getContext(), this);
157                 }
158         }
159
160         class MapFragment extends Fragment {
161                 MapView map_view;
162
163                 public View onCreateView(LayoutInflater inflator, ViewGroup container, Bundle savedInstanceState) {
164                         map_view = new MapView(container.getContext());
165                         return map_view;
166                 }
167
168                 public MapFragment() {
169                 }
170         }
171
172         MapFragment map_fragment;
173
174         /* AltosMapInterface */
175
176         static  final int       WHITE = 0xffffffff;
177         static  final int       RED   = 0xffff0000;
178         static  final int       PINK  = 0xffff8080;
179         static  final int       YELLOW= 0xffffff00;
180         static  final int       CYAN  = 0xff00ffff;
181         static  final int       BLUE  = 0xff0000ff;
182         static  final int       BLACK = 0xff000000;
183
184         public static final int stateColors[] = {
185                 WHITE,  // startup
186                 WHITE,  // idle
187                 WHITE,  // pad
188                 RED,    // boost
189                 PINK,   // fast
190                 YELLOW, // coast
191                 CYAN,   // drogue
192                 BLUE,   // main
193                 BLACK,  // landed
194                 BLACK,  // invalid
195                 CYAN,   // stateless
196         };
197
198         class MapPath extends AltosMapPath {
199
200                 boolean line_in(AltosPointDouble a, AltosPointDouble b) {
201                         final Rect bounds = canvas.getClipBounds();
202                         int left = (int) Math.floor (Math.min((float) a.x, (float) b.x) - stroke_width / 2.0f);
203                         int right = (int) Math.ceil(Math.max((float) a.x, (float) b.x) + stroke_width / 2.0f);
204                         int top = (int) Math.floor(Math.min((float) a.y, (float) b.y) - stroke_width / 2.0f);
205                         int bottom = (int) Math.ceil(Math.max((float) a.y, (float) b.y) + stroke_width / 2.0f);
206
207                         return left < bounds.right && bounds.left < right &&
208                                 top < bounds.bottom && bounds.top < bottom;
209                 }
210
211                 public void paint(AltosMapTransform t) {
212                         AltosPointDouble        prev = null;
213                         int                     cur_color = paint.getColor();
214
215                         for (AltosMapPathPoint point : points) {
216                                 AltosPointDouble        cur = t.screen(point.lat_lon);
217
218                                 if (prev != null && line_in(prev, cur)) {
219                                         int color;
220                                         if (0 <= point.state && point.state < stateColors.length)
221                                                 color = stateColors[point.state];
222                                         else
223                                                 color = stateColors[AltosLib.ao_flight_invalid];
224                                         if (color != cur_color) {
225                                                 paint.setColor(color);
226                                                 cur_color = color;
227                                         }
228                                         canvas.drawLine((float) prev.x, (float) prev.y, (float) cur.x, (float) cur.y, paint);
229                                 }
230                                 prev = cur;
231                         }
232                 }
233
234                 public MapPath() {
235                         stroke_width = TabMapOffline.this.stroke_width;
236                 }
237         }
238
239         public AltosMapPath new_path() {
240                 return null;
241         }
242
243         class MapLine extends AltosMapLine {
244                 public void paint(AltosMapTransform t) {
245                 }
246
247                 public MapLine() {
248                 }
249         }
250
251         public AltosMapLine new_line() {
252                 return null;
253         }
254
255         class MapImage implements AltosImage {
256                 public Bitmap   bitmap;
257
258                 public void flush() {
259                         if (bitmap != null) {
260                                 bitmap.recycle();
261                                 bitmap = null;
262                         }
263                 }
264
265                 public MapImage(File file) {
266                         bitmap = BitmapFactory.decodeFile(file.getPath());
267                 }
268         }
269
270         public AltosImage load_image(File file) throws Exception {
271                 return new MapImage(file);
272         }
273
274         class MapMark extends AltosMapMark {
275                 public void paint(AltosMapTransform t) {
276                 }
277
278                 MapMark(double lat, double lon, int state) {
279                         super(lat, lon, state);
280                 }
281         }
282
283         public AltosMapMark new_mark(double lat, double lon, int state) {
284                 return new MapMark(lat, lon, state);
285         }
286
287         class MapTile extends AltosMapTile {
288                 public void paint(AltosMapTransform t) {
289                         AltosPointInt           pt = new AltosPointInt(t.screen(upper_left));
290
291                         if (canvas.quickReject(pt.x, pt.y, pt.x + px_size, pt.y + px_size, Canvas.EdgeType.AA))
292                                 return;
293
294                         AltosImage              altos_image = cache.get(this, store, px_size, px_size);
295
296                         MapImage                map_image = (MapImage) altos_image;
297
298                         Bitmap                  bitmap = null;
299
300                         if (map_image != null)
301                                 bitmap = map_image.bitmap;
302
303                         if (bitmap != null) {
304                                 canvas.drawBitmap(bitmap, pt.x, pt.y, paint);
305                         } else {
306                                 paint.setColor(0xff808080);
307                                 canvas.drawRect(pt.x, pt.y, pt.x + px_size, pt.y + px_size, paint);
308                                 if (t.has_location()) {
309                                         String  message = null;
310                                         switch (status) {
311                                         case AltosMapTile.loading:
312                                                 message = "Loading...";
313                                                 break;
314                                         case AltosMapTile.bad_request:
315                                                 message = "Internal error";
316                                                 break;
317                                         case AltosMapTile.failed:
318                                                 message = "Network error, check connection";
319                                                 break;
320                                         case AltosMapTile.forbidden:
321                                                 message = "Too many requests, try later";
322                                                 break;
323                                         }
324                                         if (message != null) {
325                                                 Rect    bounds = new Rect();
326                                                 paint.getTextBounds(message, 0, message.length(), bounds);
327
328                                                 int     width = bounds.right - bounds.left;
329                                                 int     height = bounds.bottom - bounds.top;
330
331                                                 float x = pt.x + px_size / 2.0f;
332                                                 float y = pt.y + px_size / 2.0f;
333                                                 x = x - width / 2.0f;
334                                                 y = y + height / 2.0f;
335                                                 paint.setColor(0xff000000);
336                                                 canvas.drawText(message, 0, message.length(), x, y, paint);
337                                         }
338                                 }
339                         }
340
341                 }
342
343                 public MapTile(AltosMapTileListener listener, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size) {
344                         super(listener, upper_left, center, zoom, maptype, px_size, 2);
345                 }
346
347         }
348
349         public AltosMapTile new_tile(AltosMapTileListener listener, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size) {
350                 return new MapTile(listener, upper_left, center, zoom, maptype, px_size);
351         }
352
353         public int width() {
354                 if (map_fragment != null && map_fragment.map_view != null)
355                         return map_fragment.map_view.getWidth();
356                 return 500;
357         }
358
359         public int height() {
360                 if (map_fragment != null && map_fragment.map_view != null)
361                         return map_fragment.map_view.getHeight();
362                 return 500;
363         }
364
365         public void repaint() {
366                 this.getActivity().runOnUiThread(new Runnable() {
367                                 public void run() {
368                                         if (map_fragment != null && map_fragment.map_view != null)
369                                                 map_fragment.map_view.invalidate();
370                                 }
371                         });
372         }
373
374         public void repaint(AltosRectangle t_damage) {
375                 final AltosRectangle damage = t_damage;
376                 this.getActivity().runOnUiThread(new Runnable() {
377                                 public void run() {
378                                         if (map_fragment != null && map_fragment.map_view != null)
379                                                 map_fragment.map_view.invalidate(damage.x, damage.y, damage.x + damage.width, damage.y + damage.height);
380                                 }
381                         });
382         }
383
384         public void set_zoom_label(String label) {
385         }
386
387         @Override
388         public void onAttach(Activity activity) {
389                 super.onAttach(activity);
390                 mAltosDroid = (AltosDroid) activity;
391                 mAltosDroid.registerTab(this);
392                 pad_bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.pad);
393                 /* arrow at the bottom of the launchpad image */
394                 pad_off_x = pad_bitmap.getWidth() / 2;
395                 pad_off_y = pad_bitmap.getHeight();
396
397                 rocket_bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.rocket);
398                 /* arrow at the bottom of the rocket image */
399                 rocket_off_x = rocket_bitmap.getWidth() / 2;
400                 rocket_off_y = rocket_bitmap.getHeight();
401
402                 here_bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_maps_indicator_current_position);
403                 /* Center of the dot */
404                 here_off_x = here_bitmap.getWidth() / 2;
405                 here_off_y = here_bitmap.getHeight() / 2;
406         }
407
408         @Override
409         public void onCreate(Bundle savedInstanceState) {
410                 super.onCreate(savedInstanceState);
411         }
412
413         @Override
414         public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
415                 View v = inflater.inflate(R.layout.tab_map, container, false);
416
417                 map_fragment = new MapFragment();
418                 map = new AltosMap(this);
419                 mDistanceView  = (TextView)v.findViewById(R.id.distance_value);
420                 mBearingView   = (TextView)v.findViewById(R.id.bearing_value);
421                 mTargetLatitudeView  = (TextView)v.findViewById(R.id.target_lat_value);
422                 mTargetLongitudeView = (TextView)v.findViewById(R.id.target_lon_value);
423                 mReceiverLatitudeView  = (TextView)v.findViewById(R.id.receiver_lat_value);
424                 mReceiverLongitudeView = (TextView)v.findViewById(R.id.receiver_lon_value);
425                 return v;
426         }
427
428         @Override
429         public void onActivityCreated(Bundle savedInstanceState) {
430                 super.onActivityCreated(savedInstanceState);
431                 getChildFragmentManager().beginTransaction().add(R.id.map, map_fragment).commit();
432         }
433  
434         @Override
435         public void onDestroyView() {
436                 super.onDestroyView();
437
438                 mAltosDroid.unregisterTab(this);
439                 mAltosDroid = null;
440                 map_fragment = null;
441
442 //              Fragment fragment = (getFragmentManager().findFragmentById(R.id.map));
443 //              FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
444 //              ft.remove(fragment);
445 //              ft.commit();
446         }
447
448         private void setupMap() {
449         }
450
451         private void center(double lat, double lon, double accuracy) {
452                 if (mapAccuracy < 0 || accuracy < mapAccuracy/10) {
453                         if (map != null)
454                                 map.maybe_centre(lat, lon);
455                         mapAccuracy = accuracy;
456                 }
457         }
458
459         public String tab_name() { return "offmap"; }
460
461         public void show(AltosState state, AltosGreatCircle from_receiver, Location receiver) {
462                 if (from_receiver != null) {
463                         mBearingView.setText(String.format("%3.0f°", from_receiver.bearing));
464                         set_value(mDistanceView, AltosConvert.distance, 6, from_receiver.distance);
465                 }
466
467                 if (state != null) {
468                         map.show(state, null);
469                         if (state.gps != null) {
470                                 mTargetLatitudeView.setText(AltosDroid.pos(state.gps.lat, "N", "S"));
471                                 mTargetLongitudeView.setText(AltosDroid.pos(state.gps.lon, "E", "W"));
472                                 if (state.gps.locked && state.gps.nsat >= 4)
473                                         center (state.gps.lat, state.gps.lon, 10);
474                         }
475                         if (state.pad_lat != AltosLib.MISSING && pad == null)
476                                 pad = new AltosLatLon(state.pad_lat, state.pad_lon);
477                 }
478
479                 if (receiver != null) {
480                         double accuracy;
481
482                         here = new AltosLatLon(receiver.getLatitude(), receiver.getLongitude());
483                         if (receiver.hasAccuracy())
484                                 accuracy = receiver.getAccuracy();
485                         else
486                                 accuracy = 1000;
487                         mReceiverLatitudeView.setText(AltosDroid.pos(here.lat, "N", "S"));
488                         mReceiverLongitudeView.setText(AltosDroid.pos(here.lon, "E", "W"));
489                         center (here.lat, here.lon, accuracy);
490                 }
491
492         }
493
494         public void set_map_type(int map_type) {
495                 if (map != null)
496                         map.set_maptype(map_type);
497         }
498
499         public TabMapOffline() {
500         }
501 }