--- /dev/null
+/*
+ * Copyright © 2015 Keith Packard <keithp@keithp.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+package org.altusmetrum.AltosDroid;
+
+import java.util.*;
+import java.io.*;
+
+import org.altusmetrum.altoslib_13.*;
+
+import android.app.Activity;
+import android.graphics.*;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.view.*;
+import android.widget.*;
+import android.location.Location;
+import android.content.*;
+import android.util.*;
+
+class Rocket implements Comparable {
+ AltosLatLon position;
+ String name;
+ int serial;
+ long last_packet;
+ boolean active;
+ AltosMapOffline map_offline;
+
+ void paint() {
+ map_offline.draw_bitmap(position, map_offline.rocket_bitmap, map_offline.rocket_off_x, map_offline.rocket_off_y);
+ map_offline.draw_text(position, name, 0, 3*map_offline.rocket_bitmap.getHeight()/4);
+ }
+
+ void set_position(AltosLatLon position, long last_packet) {
+ this.position = position;
+ this.last_packet = last_packet;
+ }
+
+ void set_active(boolean active) {
+ this.active = active;
+ }
+
+ public int compareTo(Object o) {
+ Rocket other = (Rocket) o;
+
+ if (active && !other.active)
+ return 1;
+ if (other.active && !active)
+ return -1;
+
+ long diff = last_packet - other.last_packet;
+
+ if (diff > 0)
+ return 1;
+ if (diff < 0)
+ return -1;
+ return 0;
+ }
+
+ Rocket(int serial, AltosMapOffline map_offline) {
+ this.serial = serial;
+ this.name = String.format("%d", serial);
+ this.map_offline = map_offline;
+ }
+}
+
+public class AltosMapOffline extends View implements ScaleGestureDetector.OnScaleGestureListener, AltosMapInterface, AltosDroidMapInterface, AltosMapTypeListener {
+ ScaleGestureDetector scale_detector;
+ boolean scaling;
+ AltosMap map;
+ AltosDroid altos_droid;
+
+ static int scale = 1;
+
+ AltosLatLon here;
+ AltosLatLon there;
+ AltosLatLon pad;
+
+ Canvas canvas;
+ Paint paint;
+
+ Bitmap pad_bitmap;
+ int pad_off_x, pad_off_y;
+ Bitmap rocket_bitmap;
+ int rocket_off_x, rocket_off_y;
+ Bitmap here_bitmap;
+ int here_off_x, here_off_y;
+
+ static final int WHITE = 0xffffffff;
+ static final int RED = 0xffff0000;
+ static final int PINK = 0xffff8080;
+ static final int YELLOW= 0xffffff00;
+ static final int CYAN = 0xff00ffff;
+ static final int BLUE = 0xff0000ff;
+ static final int BLACK = 0xff000000;
+
+ public static final int stateColors[] = {
+ WHITE, // startup
+ WHITE, // idle
+ WHITE, // pad
+ RED, // boost
+ PINK, // fast
+ YELLOW, // coast
+ CYAN, // drogue
+ BLUE, // main
+ BLACK, // landed
+ BLACK, // invalid
+ CYAN, // stateless
+ };
+
+ /* AltosMapInterface */
+ public void debug(String format, Object ... arguments) {
+ AltosDebug.debug(format, arguments);
+ }
+
+ class MapTile extends AltosMapTile {
+ public void paint(AltosMapTransform t) {
+ AltosPointInt pt = new AltosPointInt(t.screen(upper_left));
+
+ if (canvas.quickReject(pt.x, pt.y, pt.x + px_size, pt.y + px_size, Canvas.EdgeType.AA))
+ return;
+
+ AltosImage altos_image = this.get_image();
+
+ MapImage map_image = (MapImage) altos_image;
+
+ Bitmap bitmap = null;
+
+ if (map_image != null)
+ bitmap = map_image.bitmap;
+
+ if (bitmap != null) {
+ canvas.drawBitmap(bitmap, pt.x, pt.y, paint);
+ } else {
+ paint.setColor(0xff808080);
+ canvas.drawRect(pt.x, pt.y, pt.x + px_size, pt.y + px_size, paint);
+ if (t.has_location()) {
+ String message = null;
+ switch (status) {
+ case AltosMapTile.fetching:
+ message = "Fetching...";
+ break;
+ case AltosMapTile.bad_request:
+ message = "Internal error";
+ break;
+ case AltosMapTile.failed:
+ message = "Network error";
+ break;
+ case AltosMapTile.forbidden:
+ message = "Outside of known launch areas";
+ break;
+ }
+ if (message != null) {
+ Rect bounds = new Rect();
+ paint.getTextBounds(message, 0, message.length(), bounds);
+
+ int width = bounds.right - bounds.left;
+ int height = bounds.bottom - bounds.top;
+
+ float x = pt.x + px_size / 2.0f;
+ float y = pt.y + px_size / 2.0f;
+ x = x - width / 2.0f;
+ y = y + height / 2.0f;
+ paint.setColor(0xff000000);
+ canvas.drawText(message, 0, message.length(), x, y, paint);
+ }
+ }
+ }
+ }
+
+ public MapTile(AltosMapCache cache, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
+ super(cache, upper_left, center, zoom, maptype, px_size, scale);
+ }
+
+ }
+
+ public AltosMapTile new_tile(AltosMapCache cache, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
+ return new MapTile(cache, upper_left, center, zoom, maptype, px_size, scale);
+ }
+
+ public AltosMapPath new_path() {
+ return null;
+ }
+
+ public AltosMapLine new_line() {
+ return null;
+ }
+
+ class MapImage implements AltosImage {
+ public Bitmap bitmap;
+
+ public void flush() {
+ if (bitmap != null) {
+ bitmap.recycle();
+ bitmap = null;
+ }
+ }
+
+ public MapImage(File file) {
+ bitmap = BitmapFactory.decodeFile(file.getPath());
+ }
+ }
+
+ public AltosImage load_image(File file) throws Exception {
+ return new MapImage(file);
+ }
+
+ class MapMark extends AltosMapMark {
+ public void paint(AltosMapTransform t) {
+ }
+
+ MapMark(double lat, double lon, int state) {
+ super(lat, lon, state);
+ }
+ }
+
+ public AltosMapMark new_mark(double lat, double lon, int state) {
+ return new MapMark(lat, lon, state);
+ }
+
+ public int width() {
+ return getWidth();
+ }
+
+ public int height() {
+ return getHeight();
+ }
+
+ public void repaint() {
+ postInvalidate();
+ }
+
+ public void repaint(AltosRectangle damage) {
+ postInvalidate(damage.x, damage.y, damage.x + damage.width, damage.y + damage.height);
+ }
+
+ public void set_zoom_label(String label) {
+ }
+
+ public void select_object(AltosLatLon latlon) {
+ if (map.transform == null)
+ return;
+ ArrayList<Integer> near = new ArrayList<Integer>();
+
+ for (Rocket rocket : sorted_rockets()) {
+ if (rocket.position == null) {
+ debug("rocket %d has no position\n", rocket.serial);
+ continue;
+ }
+ double distance = map.transform.hypot(latlon, rocket.position);
+ debug("check select %d distance %g width %d\n", rocket.serial, distance, rocket_bitmap.getWidth());
+ if (distance < rocket_bitmap.getWidth() * 2.0) {
+ debug("selecting %d\n", rocket.serial);
+ near.add(rocket.serial);
+ }
+ }
+ if (near.size() != 0)
+ altos_droid.touch_trackers(near.toArray(new Integer[0]));
+ }
+
+ class Line {
+ AltosLatLon a, b;
+
+ void paint() {
+ if (a != null && b != null) {
+ AltosPointDouble a_screen = map.transform.screen(a);
+ AltosPointDouble b_screen = map.transform.screen(b);
+ paint.setColor(0xff8080ff);
+ canvas.drawLine((float) a_screen.x, (float) a_screen.y,
+ (float) b_screen.x, (float) b_screen.y,
+ paint);
+ }
+ }
+
+ void set_a(AltosLatLon a) {
+ this.a = a;
+ }
+
+ void set_b(AltosLatLon b) {
+ this.b = b;
+ }
+
+ Line() {
+ }
+ }
+
+ Line line = new Line();
+
+ int stroke_width = 20;
+
+ void draw_text(AltosLatLon lat_lon, String text, int off_x, int off_y) {
+ if (lat_lon != null && map != null && map.transform != null) {
+ AltosPointInt pt = new AltosPointInt(map.transform.screen(lat_lon));
+
+ Rect bounds = new Rect();
+ paint.getTextBounds(text, 0, text.length(), bounds);
+
+ int width = bounds.right - bounds.left;
+ int height = bounds.bottom - bounds.top;
+
+ float x = pt.x;
+ float y = pt.y;
+ x = x - width / 2.0f - off_x;
+ y = y + height / 2.0f - off_y;
+ paint.setColor(0xff000000);
+ canvas.drawText(text, 0, text.length(), x, y, paint);
+ }
+ }
+
+ HashMap<Integer,Rocket> rockets = new HashMap<Integer,Rocket>();
+
+ void draw_bitmap(AltosLatLon lat_lon, Bitmap bitmap, int off_x, int off_y) {
+ if (lat_lon != null && map != null && map.transform != null) {
+ AltosPointInt pt = new AltosPointInt(map.transform.screen(lat_lon));
+
+ canvas.drawBitmap(bitmap, pt.x - off_x, pt.y - off_y, paint);
+ }
+ }
+
+ private Rocket[] sorted_rockets() {
+ Rocket[] rocket_array = rockets.values().toArray(new Rocket[0]);
+
+ Arrays.sort(rocket_array);
+ return rocket_array;
+ }
+
+ private void draw_positions() {
+ line.set_a(there);
+ line.set_b(here);
+ line.paint();
+ draw_bitmap(pad, pad_bitmap, pad_off_x, pad_off_y);
+
+ for (Rocket rocket : sorted_rockets())
+ rocket.paint();
+ draw_bitmap(here, here_bitmap, here_off_x, here_off_y);
+ }
+
+ @Override public void invalidate() {
+ Rect r = new Rect();
+ getDrawingRect(r);
+ super.invalidate();
+ }
+
+ @Override public void invalidate(int l, int t, int r, int b) {
+ Rect rect = new Rect();
+ getDrawingRect(rect);
+ super.invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas view_canvas) {
+ if (map == null) {
+ debug("MapView draw without map\n");
+ return;
+ }
+ canvas = view_canvas;
+ paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setStrokeWidth(stroke_width);
+ paint.setStrokeCap(Paint.Cap.ROUND);
+ paint.setStrokeJoin(Paint.Join.ROUND);
+ paint.setTextSize(40);
+ map.paint();
+ draw_positions();
+ canvas = null;
+ }
+
+ public boolean onScale(ScaleGestureDetector detector) {
+ float f = detector.getScaleFactor();
+
+ if (f <= 0.8) {
+ map.set_zoom_centre(map.get_zoom() - 1, new AltosPointInt((int) detector.getFocusX(), (int) detector.getFocusY()));
+ return true;
+ }
+ if (f >= 1.2) {
+ map.set_zoom_centre(map.get_zoom() + 1, new AltosPointInt((int) detector.getFocusX(), (int) detector.getFocusY()));
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onScaleBegin(ScaleGestureDetector detector) {
+ return true;
+ }
+
+ public void onScaleEnd(ScaleGestureDetector detector) {
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ scale_detector.onTouchEvent(event);
+
+ if (scale_detector.isInProgress()) {
+ scaling = true;
+ }
+
+ if (scaling) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ scaling = false;
+ }
+ return true;
+ }
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ map.touch_start((int) event.getX(), (int) event.getY(), true);
+ } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ map.touch_continue((int) event.getX(), (int) event.getY(), true);
+ } else if (event.getAction() == MotionEvent.ACTION_UP) {
+ map.touch_stop((int) event.getX(), (int) event.getY(), true);
+ }
+ return true;
+ }
+
+ double mapAccuracy;
+
+ public void center(double lat, double lon, double accuracy) {
+ if (mapAccuracy <= 0 || accuracy < mapAccuracy/10 || (map != null && !map.has_centre())) {
+ if (map != null)
+ map.maybe_centre(lat, lon);
+ mapAccuracy = accuracy;
+ }
+ }
+
+ public void set_visible(boolean visible) {
+ if (visible)
+ setVisibility(VISIBLE);
+ else
+ setVisibility(GONE);
+ }
+
+ public void show(TelemetryState telem_state, AltosState state, AltosGreatCircle from_receiver, Location receiver) {
+ boolean changed = false;
+
+ if (state != null) {
+ map.show(state, null);
+ if (state.pad_lat != AltosLib.MISSING && pad == null)
+ pad = new AltosLatLon(state.pad_lat, state.pad_lon);
+ }
+
+ if (telem_state != null) {
+ Integer[] old_serial = rockets.keySet().toArray(new Integer[0]);
+ Integer[] new_serial = telem_state.states.keySet().toArray(new Integer[0]);
+
+ /* remove deleted keys */
+ for (int serial : old_serial) {
+ if (!telem_state.states.containsKey(serial))
+ rockets.remove(serial);
+ }
+
+ /* set remaining keys */
+
+ for (int serial : new_serial) {
+ Rocket rocket;
+ AltosState t_state = telem_state.states.get(serial);
+ if (rockets.containsKey(serial))
+ rocket = rockets.get(serial);
+ else {
+ rocket = new Rocket(serial, this);
+ rockets.put(serial, rocket);
+ }
+ if (t_state.gps != null) {
+ AltosLatLon latlon = new AltosLatLon(t_state.gps.lat, t_state.gps.lon);
+ rocket.set_position(latlon, t_state.received_time);
+ if (state.cal_data().serial == serial)
+ there = latlon;
+ }
+ if (state != null)
+ rocket.set_active(state.cal_data().serial == serial);
+ }
+ }
+ if (receiver != null) {
+ AltosLatLon new_here = new AltosLatLon(receiver.getLatitude(), receiver.getLongitude());
+ if (!new_here.equals(here)) {
+ here = new_here;
+ AltosDebug.debug("Location changed, redraw");
+ repaint();
+ }
+ }
+ }
+
+ public void onCreateView(AltosDroid altos_droid) {
+ this.altos_droid = altos_droid;
+ map = new AltosMap(this, scale);
+ AltosPreferences.register_map_type_listener(this);
+ map.set_maptype(AltosPreferences.map_type());
+
+ pad_bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.pad);
+ /* arrow at the bottom of the launchpad image */
+ pad_off_x = pad_bitmap.getWidth() / 2;
+ pad_off_y = pad_bitmap.getHeight();
+
+ rocket_bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.rocket);
+ /* arrow at the bottom of the rocket image */
+ rocket_off_x = rocket_bitmap.getWidth() / 2;
+ rocket_off_y = rocket_bitmap.getHeight();
+
+ here_bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_maps_indicator_current_position);
+ /* Center of the dot */
+ here_off_x = here_bitmap.getWidth() / 2;
+ here_off_y = here_bitmap.getHeight() / 2;
+ }
+
+ public void onDestroyView() {
+ AltosPreferences.unregister_map_type_listener(this);
+ }
+
+ public void map_type_changed(int map_type) {
+ if (map != null)
+ map.set_maptype(map_type);
+ }
+
+ public AltosMapOffline(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ this.altos_droid = altos_droid;
+ scale_detector = new ScaleGestureDetector(context, this);
+ }
+}