altosdroid: Add offline map tab
authorKeith Packard <keithp@keithp.com>
Wed, 27 May 2015 02:47:04 +0000 (19:47 -0700)
committerKeith Packard <keithp@keithp.com>
Wed, 27 May 2015 02:47:04 +0000 (19:47 -0700)
It's not very fancy yet, but it does zoom and pan, and show the path
of the rocket with a line.

Signed-off-by: Keith Packard <keithp@keithp.com>
altosdroid/src/org/altusmetrum/AltosDroid/AltosDroid.java
altosdroid/src/org/altusmetrum/AltosDroid/AltosViewPager.java
altosdroid/src/org/altusmetrum/AltosDroid/TabMapOffline.java [new file with mode: 0644]
altoslib/AltosMap.java
altoslib/AltosMapTile.java
altoslib/AltosPointInt.java

index eff24b10cefb8cc3dc23e0c163905c343d5163e6..dd87614b81552467c75c0088f44876b83a36c180 100644 (file)
@@ -444,6 +444,7 @@ public class AltosDroid extends FragmentActivity implements AltosUnitsListener {
                mTabsAdapter.addTab(mTabHost.newTabSpec("descent").setIndicator(create_tab_view("Descent")), TabDescent.class, null);
                mTabsAdapter.addTab(mTabHost.newTabSpec("landed").setIndicator(create_tab_view("Landed")), TabLanded.class, null);
                mTabsAdapter.addTab(mTabHost.newTabSpec("map").setIndicator(create_tab_view("Map")), TabMap.class, null);
+               mTabsAdapter.addTab(mTabHost.newTabSpec("offmap").setIndicator(create_tab_view("OffMap")), TabMapOffline.class, null);
 
                // Set up the custom title
                mTitle = (TextView) findViewById(R.id.title_left_text);
index 223ae75a0e48f9e99f91f0b85a758ee260e08730..e8299b09aeda4f2de7f6f0d334b7cbda14c545b4 100644 (file)
@@ -34,6 +34,9 @@ public class AltosViewPager extends ViewPager {
 
     @Override
     protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
+       if (v.getClass().getName().endsWith("MapView"))
+           return true;
+
        if(v.getClass() != null &&
           v.getClass().getPackage() != null &&
           v.getClass().getPackage().getName() != null &&
diff --git a/altosdroid/src/org/altusmetrum/AltosDroid/TabMapOffline.java b/altosdroid/src/org/altusmetrum/AltosDroid/TabMapOffline.java
new file mode 100644 (file)
index 0000000..42d80ad
--- /dev/null
@@ -0,0 +1,481 @@
+/*
+ * Copyright © 2013 Mike Beattie <mike@ethernal.org>
+ *
+ * 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; version 2 of the License.
+ *
+ * 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.Arrays;
+import java.io.*;
+
+import org.altusmetrum.altoslib_7.*;
+
+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.Log;
+
+public class TabMapOffline extends AltosDroidTab implements AltosMapInterface {
+       // Debugging
+       static final String TAG = "AltosDroid";
+       static final boolean D = true;
+
+       AltosDroid mAltosDroid;
+
+       AltosMap map;
+
+       Canvas  canvas;
+       Paint   paint;
+
+       private boolean pad_set;
+
+       private TextView mDistanceView;
+       private TextView mBearingView;
+       private TextView mTargetLatitudeView;
+       private TextView mTargetLongitudeView;
+       private TextView mReceiverLatitudeView;
+       private TextView mReceiverLongitudeView;
+
+       private double mapAccuracy = -1;
+
+       int     stroke_width = 20;
+
+       class MapView extends View implements ScaleGestureDetector.OnScaleGestureListener {
+
+               ScaleGestureDetector    scale_detector;
+               boolean                 scaling;
+
+               protected void onDraw(Canvas view_canvas) {
+                       canvas = view_canvas;
+                       paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+                       paint.setStrokeWidth(stroke_width);
+                       paint.setStrokeCap(Paint.Cap.ROUND);
+                       paint.setStrokeJoin(Paint.Join.ROUND);
+                       map.paint();
+                       canvas = null;
+               }
+
+               public boolean onScale(ScaleGestureDetector detector) {
+                       float   f = detector.getScaleFactor();
+                       if (D) Log.d(TAG, String.format("onScale %f\n", f));
+                       if (f <= 0.8) {
+                               map.set_zoom(map.get_zoom() - 1);
+                               return true;
+                       }
+                       if (f >= 1.2) {
+                               map.set_zoom(map.get_zoom() + 1);
+                               return true;
+                       }
+                       return false;
+               }
+
+               public boolean onScaleBegin(ScaleGestureDetector detector) {
+                       if (D) Log.d(TAG, String.format("onScaleBegin %f\n", detector.getScaleFactor()));
+                       return true;
+               }
+
+               public void onScaleEnd(ScaleGestureDetector detector) {
+                       if (D) Log.d(TAG, String.format("onScaleEnd %f\n", detector.getScaleFactor()));
+               }
+
+               @Override
+               public boolean dispatchTouchEvent(MotionEvent event) {
+                       scale_detector.onTouchEvent(event);
+
+                       if (scale_detector.isInProgress()) {
+                               scaling = true;
+                       }
+
+                       if (scaling) {
+                               if(D) Log.d(TAG, "scale in progress\n");
+                               if (event.getAction() == MotionEvent.ACTION_UP) {
+                                       if (D) Log.d(TAG, "scale finished\n");
+                                       scaling = false;
+                               }
+                               return true;
+                       }
+
+                       if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                               if(D) Log.d(TAG, String.format("down event %g %g\n", event.getX(), event.getY()));
+                               map.touch_start((int) event.getX(), (int) event.getY(), true);
+                       } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
+                               if(D) Log.d(TAG, String.format("continue event %g %g\n", event.getX(), event.getY()));
+                               map.touch_continue((int) event.getX(), (int) event.getY(), true);
+                       }
+                       return true;
+               }
+
+               public MapView(Context context) {
+                       super(context);
+                       scale_detector = new ScaleGestureDetector(this.getContext(), this);
+               }
+       }
+
+       class MapFragment extends Fragment {
+               MapView map_view;
+
+               public View onCreateView(LayoutInflater inflator, ViewGroup container, Bundle savedInstanceState) {
+                       map_view = new MapView(container.getContext());
+                       return map_view;
+               }
+
+               public MapFragment() {
+               }
+       }
+
+       MapFragment map_fragment;
+
+       /* AltosMapInterface */
+
+       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
+       };
+
+       class MapPath extends AltosMapPath {
+
+               boolean line_in(AltosPointDouble a, AltosPointDouble b) {
+                       final Rect bounds = canvas.getClipBounds();
+                       int left = (int) Math.floor (Math.min((float) a.x, (float) b.x) - stroke_width / 2.0f);
+                       int right = (int) Math.ceil(Math.max((float) a.x, (float) b.x) + stroke_width / 2.0f);
+                       int top = (int) Math.floor(Math.min((float) a.y, (float) b.y) - stroke_width / 2.0f);
+                       int bottom = (int) Math.ceil(Math.max((float) a.y, (float) b.y) + stroke_width / 2.0f);
+
+                       return left < bounds.right && bounds.left < right &&
+                               top < bounds.bottom && bounds.top < bottom;
+               }
+
+               public void paint(AltosMapTransform t) {
+                       AltosPointDouble        prev = null;
+                       int                     cur_color = paint.getColor();
+
+                       for (AltosMapPathPoint point : points) {
+                               AltosPointDouble        cur = t.screen(point.lat_lon);
+
+                               if (prev != null && line_in(prev, cur)) {
+                                       int color;
+                                       if (0 <= point.state && point.state < stateColors.length)
+                                               color = stateColors[point.state];
+                                       else
+                                               color = stateColors[AltosLib.ao_flight_invalid];
+                                       if (color != cur_color) {
+                                               paint.setColor(color);
+                                               cur_color = color;
+                                       }
+                                       canvas.drawLine((float) prev.x, (float) prev.y, (float) cur.x, (float) cur.y, paint);
+                               }
+                               prev = cur;
+                       }
+               }
+
+               public MapPath() {
+                       stroke_width = TabMapOffline.this.stroke_width;
+               }
+       }
+
+       public AltosMapPath new_path() {
+               return new MapPath();
+       }
+
+       class MapLine extends AltosMapLine {
+               public void paint(AltosMapTransform t) {
+               }
+
+               public MapLine() {
+               }
+       }
+
+       public AltosMapLine new_line() {
+               return new MapLine();
+       }
+
+       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);
+       }
+
+       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 = cache.get(this, store, px_size, px_size);
+
+                       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.loading:
+                                               message = "Loading...";
+                                               break;
+                                       case AltosMapTile.bad_request:
+                                               message = "Internal error";
+                                               break;
+                                       case AltosMapTile.failed:
+                                               message = "Network error, check connection";
+                                               break;
+                                       case AltosMapTile.forbidden:
+                                               message = "Too many requests, try later";
+                                               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(AltosMapTileListener listener, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size) {
+                       super(listener, upper_left, center, zoom, maptype, px_size, 2);
+               }
+
+       }
+
+       public AltosMapTile new_tile(AltosMapTileListener listener, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size) {
+               return new MapTile(listener, upper_left, center, zoom, maptype, px_size);
+       }
+
+       public int width() {
+               if (map_fragment != null && map_fragment.map_view != null)
+                       return map_fragment.map_view.getWidth();
+               return 500;
+       }
+
+       public int height() {
+               if (map_fragment != null && map_fragment.map_view != null)
+                       return map_fragment.map_view.getHeight();
+               return 500;
+       }
+
+       public void repaint() {
+               this.getActivity().runOnUiThread(new Runnable() {
+                               public void run() {
+                                       if (map_fragment != null && map_fragment.map_view != null)
+                                               map_fragment.map_view.invalidate();
+                               }
+                       });
+       }
+
+       public void repaint(AltosRectangle t_damage) {
+               final AltosRectangle damage = t_damage;
+               this.getActivity().runOnUiThread(new Runnable() {
+                               public void run() {
+                                       if (map_fragment != null && map_fragment.map_view != null)
+                                               map_fragment.map_view.invalidate(damage.x, damage.y, damage.x + damage.width, damage.y + damage.height);
+                               }
+                       });
+       }
+
+       public void set_zoom_label(String label) {
+       }
+
+       @Override
+       public void onAttach(Activity activity) {
+               super.onAttach(activity);
+               mAltosDroid = (AltosDroid) activity;
+               mAltosDroid.registerTab(this);
+       }
+
+       @Override
+       public void onCreate(Bundle savedInstanceState) {
+               super.onCreate(savedInstanceState);
+       }
+
+       @Override
+       public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+               View v = inflater.inflate(R.layout.tab_map, container, false);
+
+               map_fragment = new MapFragment();
+               map = new AltosMap(this);
+               mDistanceView  = (TextView)v.findViewById(R.id.distance_value);
+               mBearingView   = (TextView)v.findViewById(R.id.bearing_value);
+               mTargetLatitudeView  = (TextView)v.findViewById(R.id.target_lat_value);
+               mTargetLongitudeView = (TextView)v.findViewById(R.id.target_lon_value);
+               mReceiverLatitudeView  = (TextView)v.findViewById(R.id.receiver_lat_value);
+               mReceiverLongitudeView = (TextView)v.findViewById(R.id.receiver_lon_value);
+               return v;
+       }
+
+       @Override
+       public void onActivityCreated(Bundle savedInstanceState) {
+               super.onActivityCreated(savedInstanceState);
+               getChildFragmentManager().beginTransaction().add(R.id.map, map_fragment).commit();
+       }
+       @Override
+       public void onDestroyView() {
+               super.onDestroyView();
+
+               mAltosDroid.unregisterTab(this);
+               mAltosDroid = null;
+               map_fragment = null;
+
+//             Fragment fragment = (getFragmentManager().findFragmentById(R.id.map));
+//             FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
+//             ft.remove(fragment);
+//             ft.commit();
+       }
+
+       private void setupMap() {
+/*
+               mMap = mMapFragment.getMap();
+               if (mMap != null) {
+                       mMap.setMyLocationEnabled(true);
+                       mMap.getUiSettings().setTiltGesturesEnabled(false);
+                       mMap.getUiSettings().setZoomControlsEnabled(false);
+
+                       mRocketMarker = mMap.addMarker(
+                                       // From: http://mapicons.nicolasmollet.com/markers/industry/military/missile-2/
+                                       new MarkerOptions().icon(BitmapDescriptorFactory.fromResource(R.drawable.rocket))
+                                                          .position(new LatLng(0,0))
+                                                          .visible(false)
+                                       );
+
+                       mPadMarker = mMap.addMarker(
+                                       new MarkerOptions().icon(BitmapDescriptorFactory.fromResource(R.drawable.pad))
+                                                          .position(new LatLng(0,0))
+                                                          .visible(false)
+                                       );
+
+                       mPolyline = mMap.addPolyline(
+                                       new PolylineOptions().add(new LatLng(0,0), new LatLng(0,0))
+                                                            .width(3)
+                                                            .color(Color.BLUE)
+                                                            .visible(false)
+                                       );
+
+                       mapLoaded = true;
+               }
+*/
+       }
+
+       private void center(double lat, double lon, double accuracy) {
+               if (mapAccuracy < 0 || accuracy < mapAccuracy/10) {
+                       if (map != null)
+                               map.centre(lat, lon);
+                       mapAccuracy = accuracy;
+               }
+       }
+
+       public String tab_name() { return "offmap"; }
+
+       public void show(AltosState state, AltosGreatCircle from_receiver, Location receiver) {
+               if (from_receiver != null) {
+                       mBearingView.setText(String.format("%3.0f°", from_receiver.bearing));
+                       set_value(mDistanceView, AltosConvert.distance, 6, from_receiver.distance);
+               }
+
+               if (state != null) {
+                       map.show(state, null);
+                       if (state.gps != null) {
+                               mTargetLatitudeView.setText(AltosDroid.pos(state.gps.lat, "N", "S"));
+                               mTargetLongitudeView.setText(AltosDroid.pos(state.gps.lon, "E", "W"));
+                               if (state.gps.locked && state.gps.nsat >= 4)
+                                       center (state.gps.lat, state.gps.lon, 10);
+                       }
+               }
+
+               if (receiver != null) {
+                       double accuracy;
+
+                       if (receiver.hasAccuracy())
+                               accuracy = receiver.getAccuracy();
+                       else
+                               accuracy = 1000;
+                       mReceiverLatitudeView.setText(AltosDroid.pos(receiver.getLatitude(), "N", "S"));
+                       mReceiverLongitudeView.setText(AltosDroid.pos(receiver.getLongitude(), "E", "W"));
+                       center (receiver.getLatitude(), receiver.getLongitude(), accuracy);
+               }
+
+       }
+
+       public TabMapOffline() {
+       }
+}
index b54c66cf7a9d44e8f496f90b89b71ed3ef114f1b..bdb60f0cb72f239ed5969a50f4fe30cb3756eeac 100644 (file)
@@ -287,7 +287,7 @@ public class AltosMap implements AltosMapTileListener, AltosMapStoreListener {
                                        AltosLatLon     ul = transform.lat_lon(new AltosPointDouble(x, y));
                                        AltosLatLon     center = transform.lat_lon(new AltosPointDouble(x + AltosMap.px_size/2, y + AltosMap.px_size/2));
                                        AltosMapTile tile = map_interface.new_tile(this, ul, center, zoom, maptype,
-                                                                                  AltosMap.px_size);
+                                                                                  px_size);
                                        tiles.put(point, tile);
                                }
                        }
index 165f9e6f96827b7e139da1debc3f39b3b8650c42..ee9206ee09dc34e268506b005d6b85994eb889de 100644 (file)
@@ -26,6 +26,7 @@ public abstract class AltosMapTile implements AltosFontListener {
        public int              px_size;
        int             zoom;
        int             maptype;
+       int             scale;
        public AltosMapStore    store;
        public AltosMapCache    cache;
        public int      status;
@@ -51,23 +52,28 @@ public abstract class AltosMapTile implements AltosFontListener {
                else
                        format_string = "png";
                return new File(AltosPreferences.mapdir(),
-                               String.format("map-%c%.6f,%c%.6f-%s%d.%s",
-                                             chlat, lat, chlon, lon, maptype_string, zoom, format_string));
+                               String.format("map-%c%.6f,%c%.6f-%s%d%s.%s",
+                                             chlat, lat, chlon, lon, maptype_string, zoom, scale == 1 ? "" : String.format("-%d", scale), format_string));
        }
 
        private String map_url() {
                String format_string;
+               int z = zoom;
+
                if (maptype == AltosMap.maptype_hybrid || maptype == AltosMap.maptype_satellite || maptype == AltosMap.maptype_terrain)
                        format_string = "jpg";
                else
                        format_string = "png32";
 
+               for (int s = 1; s < scale; s <<= 1)
+                       z--;
+
                if (AltosVersion.has_google_maps_api_key())
-                       return String.format("http://maps.google.com/maps/api/staticmap?center=%.6f,%.6f&zoom=%d&size=%dx%d&sensor=false&maptype=%s&format=%s&key=%s",
-                                            center.lat, center.lon, zoom, px_size, px_size, AltosMap.maptype_names[maptype], format_string, AltosVersion.google_maps_api_key);
+                       return String.format("http://maps.google.com/maps/api/staticmap?center=%.6f,%.6f&zoom=%d&size=%dx%d&scale=%d&sensor=false&maptype=%s&format=%s&key=%s",
+                                            center.lat, center.lon, z, px_size/scale, px_size/scale, scale, AltosMap.maptype_names[maptype], format_string, AltosVersion.google_maps_api_key);
                else
-                       return String.format("http://maps.google.com/maps/api/staticmap?center=%.6f,%.6f&zoom=%d&size=%dx%d&sensor=false&maptype=%s&format=%s",
-                                            center.lat, center.lon, zoom, px_size, px_size, AltosMap.maptype_names[maptype], format_string);
+                       return String.format("http://maps.google.com/maps/api/staticmap?center=%.6f,%.6f&zoom=%d&size=%dx%d&scale=%d&sensor=false&maptype=%s&format=%s",
+                                            center.lat, center.lon, z, px_size/scale, px_size/scale, AltosMap.maptype_names[maptype], format_string);
        }
 
        public void font_size_changed(int font_size) {
@@ -96,7 +102,7 @@ public abstract class AltosMapTile implements AltosFontListener {
 
        public abstract void paint(AltosMapTransform t);
 
-       public AltosMapTile(AltosMapTileListener listener, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size) {
+       public AltosMapTile(AltosMapTileListener listener, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
                this.listener = listener;
                this.upper_left = upper_left;
                this.cache = listener.cache();
@@ -110,8 +116,13 @@ public abstract class AltosMapTile implements AltosFontListener {
                this.zoom = zoom;
                this.maptype = maptype;
                this.px_size = px_size;
+               this.scale = scale;
 
                status = AltosMapTile.loading;
                store = AltosMapStore.get(map_url(), map_file());
        }
+
+       public AltosMapTile(AltosMapTileListener listener, AltosLatLon upper_left, AltosLatLon center, int zoom, int maptype, int px_size) {
+               this(listener, upper_left, center, zoom, maptype, px_size, 1);
+       }
 }
index e133ae9c81165afde4d98f994b6b951aef83d6f9..5d884391c8b5502d5d0a679cc72e75a047d6ed58 100644 (file)
@@ -28,4 +28,14 @@ public class AltosPointInt {
                this.x = x;
                this.y = y;
        }
+
+       public AltosPointInt(double x, double y) {
+               this.x = (int) (x + 0.5);
+               this.y = (int) (y + 0.5);
+       }
+
+       public AltosPointInt(AltosPointDouble pt_d) {
+               this.x = (int) (pt_d.x + 0.5);
+               this.y = (int) (pt_d.y + 0.5);
+       }
 }