altosdroid: Display online/offline maps in same tab
[fw/altos] / altosdroid / src / org / altusmetrum / AltosDroid / AltosMapOffline.java
1 /*
2  * Copyright © 2015 Keith Packard <keithp@keithp.com>
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.*;
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.*;
35
36 class Rocket implements Comparable {
37         AltosLatLon     position;
38         String          name;
39         long            last_packet;
40         AltosMapOffline map_offline;
41
42         void paint() {
43                 map_offline.draw_bitmap(position, map_offline.rocket_bitmap, map_offline.rocket_off_x, map_offline.rocket_off_y);
44                 map_offline.draw_text(position, name, 0, 3*map_offline.rocket_bitmap.getHeight()/4);
45         }
46
47         void set_position(AltosLatLon position, long last_packet) {
48                 this.position = position;
49                 this.last_packet = last_packet;
50         }
51
52         Rocket(String name, AltosMapOffline map_offline) {
53                 this.name = name;
54                 this.map_offline = map_offline;
55         }
56
57         public int compareTo(Object o) {
58                 Rocket other = (Rocket) o;
59
60                 long    diff = last_packet - other.last_packet;
61
62                 if (diff > 0)
63                         return 1;
64                 if (diff < 0)
65                         return -1;
66                 return 0;
67         }
68 }
69
70 public class AltosMapOffline extends View implements ScaleGestureDetector.OnScaleGestureListener, AltosMapInterface, AltosDroidMapInterface {
71         ScaleGestureDetector    scale_detector;
72         boolean                 scaling;
73         AltosMap                map;
74
75         AltosLatLon     here;
76         AltosLatLon     pad;
77
78         Canvas  canvas;
79         Paint   paint;
80
81         Bitmap  pad_bitmap;
82         int     pad_off_x, pad_off_y;
83         Bitmap  rocket_bitmap;
84         int     rocket_off_x, rocket_off_y;
85         Bitmap  here_bitmap;
86         int     here_off_x, here_off_y;
87
88         static  final int       WHITE = 0xffffffff;
89         static  final int       RED   = 0xffff0000;
90         static  final int       PINK  = 0xffff8080;
91         static  final int       YELLOW= 0xffffff00;
92         static  final int       CYAN  = 0xff00ffff;
93         static  final int       BLUE  = 0xff0000ff;
94         static  final int       BLACK = 0xff000000;
95
96         public static final int stateColors[] = {
97                 WHITE,  // startup
98                 WHITE,  // idle
99                 WHITE,  // pad
100                 RED,    // boost
101                 PINK,   // fast
102                 YELLOW, // coast
103                 CYAN,   // drogue
104                 BLUE,   // main
105                 BLACK,  // landed
106                 BLACK,  // invalid
107                 CYAN,   // stateless
108         };
109
110         /* AltosMapInterface */
111         public void debug(String format, Object ... arguments) {
112                 AltosDebug.debug(format, arguments);
113         }
114
115         class MapTile extends AltosMapTile {
116                 public void paint(AltosMapTransform t) {
117                         AltosPointInt           pt = new AltosPointInt(t.screen(upper_left));
118
119                         if (canvas.quickReject(pt.x, pt.y, pt.x + px_size, pt.y + px_size, Canvas.EdgeType.AA))
120                                 return;
121
122                         AltosImage              altos_image = cache.get(this, store, px_size, px_size);
123
124                         MapImage                map_image = (MapImage) altos_image;
125
126                         Bitmap                  bitmap = null;
127
128                         if (map_image != null)
129                                 bitmap = map_image.bitmap;
130
131                         if (bitmap != null) {
132                                 canvas.drawBitmap(bitmap, pt.x, pt.y, paint);
133                         } else {
134                                 paint.setColor(0xff808080);
135                                 canvas.drawRect(pt.x, pt.y, pt.x + px_size, pt.y + px_size, paint);
136                                 if (t.has_location()) {
137                                         String  message = null;
138                                         switch (status) {
139                                         case AltosMapTile.loading:
140                                                 message = "Loading...";
141                                                 break;
142                                         case AltosMapTile.bad_request:
143                                                 message = "Internal error";
144                                                 break;
145                                         case AltosMapTile.failed:
146                                                 message = "Network error, check connection";
147                                                 break;
148                                         case AltosMapTile.forbidden:
149                                                 message = "Too many requests, try later";
150                                                 break;
151                                         }
152                                         if (message != null) {
153                                                 Rect    bounds = new Rect();
154                                                 paint.getTextBounds(message, 0, message.length(), bounds);
155
156                                                 int     width = bounds.right - bounds.left;
157                                                 int     height = bounds.bottom - bounds.top;
158
159                                                 float x = pt.x + px_size / 2.0f;
160                                                 float y = pt.y + px_size / 2.0f;
161                                                 x = x - width / 2.0f;
162                                                 y = y + height / 2.0f;
163                                                 paint.setColor(0xff000000);
164                                                 canvas.drawText(message, 0, message.length(), x, y, paint);
165                                         }
166                                 }
167                         }
168                 }
169
170                 public MapTile(AltosMapTileListener listener, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size) {
171                         super(listener, upper_left, center, zoom, maptype, px_size, 2);
172                 }
173
174         }
175
176         public AltosMapTile new_tile(AltosMapTileListener listener, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size) {
177                 return new MapTile(listener, upper_left, center, zoom, maptype, px_size);
178         }
179
180         public AltosMapPath new_path() {
181                 return null;
182         }
183
184         public AltosMapLine new_line() {
185                 return null;
186         }
187
188         class MapImage implements AltosImage {
189                 public Bitmap   bitmap;
190
191                 public void flush() {
192                         if (bitmap != null) {
193                                 bitmap.recycle();
194                                 bitmap = null;
195                         }
196                 }
197
198                 public MapImage(File file) {
199                         bitmap = BitmapFactory.decodeFile(file.getPath());
200                 }
201         }
202
203         public AltosImage load_image(File file) throws Exception {
204                 return new MapImage(file);
205         }
206
207         class MapMark extends AltosMapMark {
208                 public void paint(AltosMapTransform t) {
209                 }
210
211                 MapMark(double lat, double lon, int state) {
212                         super(lat, lon, state);
213                 }
214         }
215
216         public AltosMapMark new_mark(double lat, double lon, int state) {
217                 return new MapMark(lat, lon, state);
218         }
219
220         public int width() {
221                 return getWidth();
222         }
223
224         public int height() {
225                 return getHeight();
226         }
227
228         public void repaint() {
229                 postInvalidate();
230         }
231
232         public void repaint(AltosRectangle damage) {
233                 postInvalidate(damage.x, damage.y, damage.x + damage.width, damage.y + damage.height);
234         }
235
236         public void set_zoom_label(String label) {
237         }
238
239         class Line {
240                 AltosLatLon     a, b;
241
242                 void paint() {
243                         if (a != null && b != null) {
244                                 AltosPointDouble        a_screen = map.transform.screen(a);
245                                 AltosPointDouble        b_screen = map.transform.screen(b);
246                                 paint.setColor(0xff8080ff);
247                                 canvas.drawLine((float) a_screen.x, (float) a_screen.y,
248                                                     (float) b_screen.x, (float) b_screen.y,
249                                                     paint);
250                         }
251                 }
252
253                 void set_a(AltosLatLon a) {
254                         this.a = a;
255                 }
256
257                 void set_b(AltosLatLon b) {
258                         this.b = b;
259                 }
260
261                 Line() {
262                 }
263         }
264
265         Line line = new Line();
266
267         int     stroke_width = 20;
268
269         void draw_text(AltosLatLon lat_lon, String text, int off_x, int off_y) {
270                 if (lat_lon != null && map != null && map.transform != null) {
271                         AltosPointInt pt = new AltosPointInt(map.transform.screen(lat_lon));
272
273                         Rect    bounds = new Rect();
274                         paint.getTextBounds(text, 0, text.length(), bounds);
275
276                         int     width = bounds.right - bounds.left;
277                         int     height = bounds.bottom - bounds.top;
278
279                         float x = pt.x;
280                         float y = pt.y;
281                         x = x - width / 2.0f - off_x;
282                         y = y + height / 2.0f - off_y;
283                         paint.setColor(0xff000000);
284                         canvas.drawText(text, 0, text.length(), x, y, paint);
285                 }
286         }
287
288         HashMap<Integer,Rocket> rockets = new HashMap<Integer,Rocket>();
289
290         void draw_bitmap(AltosLatLon lat_lon, Bitmap bitmap, int off_x, int off_y) {
291                 if (lat_lon != null && map != null && map.transform != null) {
292                         AltosPointInt pt = new AltosPointInt(map.transform.screen(lat_lon));
293
294                         canvas.drawBitmap(bitmap, pt.x - off_x, pt.y - off_y, paint);
295                 }
296         }
297
298         private void draw_positions() {
299                 line.set_a(map.last_position);
300                 line.set_b(here);
301                 line.paint();
302                 draw_bitmap(pad, pad_bitmap, pad_off_x, pad_off_y);
303
304                 Rocket[]        rocket_array = rockets.values().toArray(new Rocket[0]);
305
306                 Arrays.sort(rocket_array);
307                 for (Rocket rocket : rocket_array)
308                         rocket.paint();
309                 draw_bitmap(here, here_bitmap, here_off_x, here_off_y);
310         }
311
312         @Override public void invalidate() {
313                 Rect r = new Rect();
314                 getDrawingRect(r);
315                 super.invalidate();
316         }
317
318         @Override public void invalidate(int l, int t, int r, int b) {
319                 Rect rect = new Rect();
320                 getDrawingRect(rect);
321                 super.invalidate();
322         }
323
324         @Override
325         protected void onDraw(Canvas view_canvas) {
326                 debug("onDraw");
327                 if (map == null) {
328                         debug("MapView draw without map\n");
329                         return;
330                 }
331                 canvas = view_canvas;
332                 paint = new Paint(Paint.ANTI_ALIAS_FLAG);
333                 paint.setStrokeWidth(stroke_width);
334                 paint.setStrokeCap(Paint.Cap.ROUND);
335                 paint.setStrokeJoin(Paint.Join.ROUND);
336                 paint.setTextSize(40);
337                 map.paint();
338                 draw_positions();
339                 canvas = null;
340         }
341
342         public boolean onScale(ScaleGestureDetector detector) {
343                 float   f = detector.getScaleFactor();
344
345                 if (f <= 0.8) {
346                         map.set_zoom_centre(map.get_zoom() - 1, new AltosPointInt((int) detector.getFocusX(), (int) detector.getFocusY()));
347                         return true;
348                 }
349                 if (f >= 1.2) {
350                         map.set_zoom_centre(map.get_zoom() + 1, new AltosPointInt((int) detector.getFocusX(), (int) detector.getFocusY()));
351                         return true;
352                 }
353                 return false;
354         }
355
356         public boolean onScaleBegin(ScaleGestureDetector detector) {
357                 return true;
358         }
359
360         public void onScaleEnd(ScaleGestureDetector detector) {
361         }
362
363         @Override
364         public boolean dispatchTouchEvent(MotionEvent event) {
365                 scale_detector.onTouchEvent(event);
366
367                 if (scale_detector.isInProgress()) {
368                         scaling = true;
369                 }
370
371                 if (scaling) {
372                         if (event.getAction() == MotionEvent.ACTION_UP) {
373                                 scaling = false;
374                         }
375                         return true;
376                 }
377
378                 if (event.getAction() == MotionEvent.ACTION_DOWN) {
379                         map.touch_start((int) event.getX(), (int) event.getY(), true);
380                 } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
381                         map.touch_continue((int) event.getX(), (int) event.getY(), true);
382                 }
383                 return true;
384         }
385
386         double  mapAccuracy;
387
388         public void center(double lat, double lon, double accuracy) {
389                 if (mapAccuracy < 0 || accuracy < mapAccuracy/10) {
390                         if (map != null)
391                                 map.maybe_centre(lat, lon);
392                         mapAccuracy = accuracy;
393                 }
394         }
395
396         public void set_visible(boolean visible) {
397                 if (visible)
398                         setVisibility(VISIBLE);
399                 else
400                         setVisibility(GONE);
401         }
402
403         public void show(TelemetryState telem_state, AltosState state, AltosGreatCircle from_receiver, Location receiver) {
404                 if (state != null) {
405                         map.show(state, null);
406                         if (state.pad_lat != AltosLib.MISSING && pad == null)
407                                 pad = new AltosLatLon(state.pad_lat, state.pad_lon);
408                 }
409
410                 if (telem_state != null) {
411                         Integer[] old_serial = rockets.keySet().toArray(new Integer[0]);
412                         Integer[] new_serial = telem_state.states.keySet().toArray(new Integer[0]);
413
414                         /* remove deleted keys */
415                         for (int serial : old_serial) {
416                                 if (!telem_state.states.containsKey(serial))
417                                         rockets.remove(serial);
418                         }
419
420                         /* set remaining keys */
421
422                         for (int serial : new_serial) {
423                                 Rocket          rocket;
424                                 AltosState      t_state = telem_state.states.get(serial);
425                                 if (rockets.containsKey(serial))
426                                         rocket = rockets.get(serial);
427                                 else {
428                                         rocket = new Rocket(String.format("%d", serial), this);
429                                         rockets.put(serial, rocket);
430                                 }
431                                 if (t_state.gps != null)
432                                         rocket.set_position(new AltosLatLon(t_state.gps.lat, t_state.gps.lon), t_state.received_time);
433                         }
434                 }
435                 if (receiver != null) {
436                         here = new AltosLatLon(receiver.getLatitude(), receiver.getLongitude());
437                 }
438         }
439
440         public void onCreateView(int map_type) {
441                 map = new AltosMap(this);
442                 map.set_maptype(map_type);
443
444                 pad_bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.pad);
445                 /* arrow at the bottom of the launchpad image */
446                 pad_off_x = pad_bitmap.getWidth() / 2;
447                 pad_off_y = pad_bitmap.getHeight();
448
449                 rocket_bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.rocket);
450                 /* arrow at the bottom of the rocket image */
451                 rocket_off_x = rocket_bitmap.getWidth() / 2;
452                 rocket_off_y = rocket_bitmap.getHeight();
453
454                 here_bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_maps_indicator_current_position);
455                 /* Center of the dot */
456                 here_off_x = here_bitmap.getWidth() / 2;
457                 here_off_y = here_bitmap.getHeight() / 2;
458         }
459
460         public void set_map_type(int map_type) {
461                 if (map != null)
462                         map.set_maptype(map_type);
463         }
464
465         public AltosMapOffline(Context context, AttributeSet attrs) {
466                 super(context, attrs);
467                 scale_detector = new ScaleGestureDetector(context, this);
468         }
469 }