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