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