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