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