From 7975d088a4ac44c0943134fa41d0e3b88f50b98f Mon Sep 17 00:00:00 2001 From: Keith Packard Date: Tue, 26 May 2015 19:47:04 -0700 Subject: [PATCH] altosdroid: Add offline map tab 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 --- .../altusmetrum/AltosDroid/AltosDroid.java | 1 + .../AltosDroid/AltosViewPager.java | 3 + .../altusmetrum/AltosDroid/TabMapOffline.java | 481 ++++++++++++++++++ altoslib/AltosMap.java | 2 +- altoslib/AltosMapTile.java | 25 +- altoslib/AltosPointInt.java | 10 + 6 files changed, 514 insertions(+), 8 deletions(-) create mode 100644 altosdroid/src/org/altusmetrum/AltosDroid/TabMapOffline.java diff --git a/altosdroid/src/org/altusmetrum/AltosDroid/AltosDroid.java b/altosdroid/src/org/altusmetrum/AltosDroid/AltosDroid.java index eff24b10..dd87614b 100644 --- a/altosdroid/src/org/altusmetrum/AltosDroid/AltosDroid.java +++ b/altosdroid/src/org/altusmetrum/AltosDroid/AltosDroid.java @@ -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); diff --git a/altosdroid/src/org/altusmetrum/AltosDroid/AltosViewPager.java b/altosdroid/src/org/altusmetrum/AltosDroid/AltosViewPager.java index 223ae75a..e8299b09 100644 --- a/altosdroid/src/org/altusmetrum/AltosDroid/AltosViewPager.java +++ b/altosdroid/src/org/altusmetrum/AltosDroid/AltosViewPager.java @@ -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 index 00000000..42d80ad5 --- /dev/null +++ b/altosdroid/src/org/altusmetrum/AltosDroid/TabMapOffline.java @@ -0,0 +1,481 @@ +/* + * Copyright © 2013 Mike Beattie + * + * 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() { + } +} diff --git a/altoslib/AltosMap.java b/altoslib/AltosMap.java index b54c66cf..bdb60f0c 100644 --- a/altoslib/AltosMap.java +++ b/altoslib/AltosMap.java @@ -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); } } diff --git a/altoslib/AltosMapTile.java b/altoslib/AltosMapTile.java index 165f9e6f..ee9206ee 100644 --- a/altoslib/AltosMapTile.java +++ b/altoslib/AltosMapTile.java @@ -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); + } } diff --git a/altoslib/AltosPointInt.java b/altoslib/AltosPointInt.java index e133ae9c..5d884391 100644 --- a/altoslib/AltosPointInt.java +++ b/altoslib/AltosPointInt.java @@ -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); + } } -- 2.30.2