package net.osmand.plus.distancecalculator;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.PointF;
import android.os.AsyncTask;
import android.support.v7.app.AlertDialog;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import net.osmand.CallbackWithObject;
import net.osmand.IndexConstants;
import net.osmand.data.LatLon;
import net.osmand.data.PointDescription;
import net.osmand.data.RotatedTileBox;
import net.osmand.plus.ApplicationMode;
import net.osmand.plus.ContextMenuAdapter;
import net.osmand.plus.ContextMenuItem;
import net.osmand.plus.GPXUtilities;
import net.osmand.plus.GPXUtilities.GPXFile;
import net.osmand.plus.GPXUtilities.Route;
import net.osmand.plus.GPXUtilities.Track;
import net.osmand.plus.GPXUtilities.TrkSegment;
import net.osmand.plus.GPXUtilities.WptPt;
import net.osmand.plus.OsmAndFormatter;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.OsmandPlugin;
import net.osmand.plus.OsmandSettings.CommonPreference;
import net.osmand.plus.R;
import net.osmand.plus.activities.MapActivity;
import net.osmand.plus.helpers.GpxUiHelper;
import net.osmand.plus.views.ContextMenuLayer;
import net.osmand.plus.views.MapInfoLayer;
import net.osmand.plus.views.OsmandMapLayer;
import net.osmand.plus.views.OsmandMapTileView;
import net.osmand.plus.views.mapwidgets.TextInfoWidget;
import net.osmand.util.MapUtils;
import java.io.File;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import gnu.trove.list.array.TIntArrayList;
public class DistanceCalculatorPlugin extends OsmandPlugin {
private static final String ID = "osmand.distance";
private OsmandApplication app;
private DistanceCalculatorLayer distanceCalculatorLayer;
private TextInfoWidget distanceControl;
private List<LinkedList<WptPt>> measurementPoints = new ArrayList<>();
private GPXFile originalGPX;
private String distance = null;
private int distanceMeasurementMode = 0;
public DistanceCalculatorPlugin(OsmandApplication app) {
this.app = app;
ApplicationMode.regWidgetVisibility("distance.measurement", ApplicationMode.DEFAULT);
}
@Override
public String getId() {
return ID;
}
@Override
public String getDescription() {
return app.getString(R.string.osmand_distance_planning_plugin_description);
}
@Override
public String getName() {
return app.getString(R.string.osmand_distance_planning_plugin_name);
}
@Override
public String getHelpFileName() {
return "feature_articles/distance-calculator-and-planning-tool.html";
}
@Override
public void updateLayers(OsmandMapTileView mapView, MapActivity activity) {
if(isActive()) {
if(distanceCalculatorLayer == null) {
registerLayers(activity);
}
if(!mapView.isLayerVisible(distanceCalculatorLayer)) {
activity.getMapView().addLayer(distanceCalculatorLayer, 4.5f);
}
if(distanceControl == null) {
registerWidget(activity);
}
} else {
MapInfoLayer mapInfoLayer = activity.getMapLayers().getMapInfoLayer();
if(distanceCalculatorLayer != null) {
activity.getMapView().removeLayer(distanceCalculatorLayer);
}
if (mapInfoLayer != null && distanceControl != null ) {
mapInfoLayer.removeSideWidget(distanceControl);
mapInfoLayer.recreateControls();
distanceControl = null;
}
}
}
@Override
public void registerLayers(MapActivity activity) {
// remove old if existing
if(distanceCalculatorLayer != null) {
activity.getMapView().removeLayer(distanceCalculatorLayer);
}
distanceCalculatorLayer = new DistanceCalculatorLayer();
activity.getMapView().addLayer(distanceCalculatorLayer, 4.5f);
registerWidget(activity);
}
private void registerWidget(MapActivity activity) {
MapInfoLayer mapInfoLayer = activity.getMapLayers().getMapInfoLayer();
if (mapInfoLayer != null ) {
distanceControl = createDistanceControl(activity);
mapInfoLayer.registerSideWidget(distanceControl,
R.drawable.ic_action_ruler, R.string.map_widget_distancemeasurement, "distance.measurement", false, 35);
mapInfoLayer.recreateControls();
updateText();
}
}
private void updateText() {
if (distanceControl != null) {
String ds = distance;
if (ds == null) {
if(distanceMeasurementMode == 0) {
distanceControl.setText(app.getString(R.string.shared_string_control_start), "");
} else {
distanceControl.setText("0", ""); //$NON-NLS-1$
}
} else {
int ls = ds.lastIndexOf(' ');
if (ls == -1) {
distanceControl.setText(ds, null);
} else {
distanceControl.setText(ds.substring(0, ls), ds.substring(ls + 1));
}
}
}
}
private void showDialog(final MapActivity activity) {
AlertDialog.Builder bld = new AlertDialog.Builder(activity);
final TIntArrayList list = new TIntArrayList();
if(distanceMeasurementMode == 0) {
list.add(R.string.distance_measurement_start_editing);
} else {
list.add(R.string.distance_measurement_finish_editing);
}
if(measurementPoints.size() > 0) {
list.add(R.string.distance_measurement_finish_subtrack);
list.add(R.string.distance_measurement_clear_route);
list.add(R.string.shared_string_save_as_gpx);
}
list.add(R.string.distance_measurement_load_gpx);
String[] items = new String[list.size()];
for(int i = 0; i < items.length; i++) {
items[i] = activity.getString(list.get(i));
}
bld.setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int id = list.get(which);
if (id == R.string.distance_measurement_start_editing) {
distanceMeasurementMode = 1;
startEditingHelp(activity) ;
} else if (id == R.string.distance_measurement_finish_editing) {
distanceMeasurementMode = 0;
} else if (id == R.string.distance_measurement_finish_subtrack) {
measurementPoints.add(new LinkedList<GPXUtilities.WptPt>());
} else if (id == R.string.distance_measurement_clear_route) {
distanceMeasurementMode = 0;
originalGPX = null;
measurementPoints.clear();
calculateDistance();
activity.getContextMenu().close();
} else if (id == R.string.shared_string_save_as_gpx) {
saveGpx(activity);
} else if (id == R.string.distance_measurement_load_gpx) {
loadGpx(activity);
}
activity.getMapView().refreshMap();
updateText();
}
});
bld.show();
}
protected void loadGpx(final MapActivity activity) {
GpxUiHelper.selectGPXFile(activity, false, false, new CallbackWithObject<GPXUtilities.GPXFile[]>() {
@Override
public boolean processResult(GPXFile[] res) {
measurementPoints.clear();
if (res.length > 0 && res[0] != null) {
GPXFile result = res[0];
originalGPX = result;
for (Track t : result.tracks) {
for (TrkSegment s : t.segments) {
if (s.points.size() > 0) {
LinkedList<WptPt> l = new LinkedList<WptPt>(s.points);
measurementPoints.add(l);
}
}
}
for (Route r : result.routes) {
LinkedList<WptPt> l = new LinkedList<WptPt>(r.points);
measurementPoints.add(l);
}
for (WptPt p : result.points) {
LinkedList<WptPt> l = new LinkedList<WptPt>();
l.add(p);
measurementPoints.add(l);
}
WptPt pt = result.findPointToShow();
OsmandMapTileView mapView = activity.getMapView();
if(pt != null){
mapView.getAnimatedDraggingThread().startMoving(pt.lat, pt.lon,
mapView.getZoom(), true);
}
}
calculateDistance();
return true;
}
});
}
protected void saveGpx(final MapActivity activity) {
AlertDialog.Builder b = new AlertDialog.Builder(activity);
final File dir = app.getAppPath(IndexConstants.GPX_INDEX_DIR);
LinearLayout ll = new LinearLayout(activity);
ll.setOrientation(LinearLayout.VERTICAL);
ll.setPadding(7, 7, 7, 7);
final TextView tv = new TextView(activity);
tv.setText("");
tv.setTextColor(Color.RED);
ll.addView(tv);
final EditText editText = new EditText(activity);
editText.setHint(R.string.gpx_file_name);
if(originalGPX != null && originalGPX.path != null){
String p = originalGPX.path;
int li = p.lastIndexOf('/');
if(li >= 0) {
p = p.substring(li + 1);
}
int pi = p.lastIndexOf('.');
if(pi >= 0) {
p = p.substring(0, pi);
}
editText.setText(p);
}
editText.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable s) {
boolean e = false;
try {
e = new File(dir, s.toString()).exists() || new File(dir, s.toString() +".gpx").exists();
} catch (Exception e1) {
}
if (e) {
tv.setText(R.string.file_with_name_already_exists);
} else {
tv.setText("");
}
}
});
ll.addView(editText);
b.setView(ll);
b.setPositiveButton(R.string.shared_string_save, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String newName = editText.getText().toString();
if(!newName.endsWith(".gpx")){
newName += ".gpx";
}
saveGpx(activity, newName);
}
});
b.setNegativeButton(R.string.shared_string_cancel, null);
b.show();
}
private void saveGpx(final MapActivity activity,
final String fileNameSave) {
final AsyncTask<Void, Void, String> exportTask = new AsyncTask<Void, Void, String>() {
private ProgressDialog dlg;
private File toSave;
@Override
protected String doInBackground(Void... params) {
toSave = new File(app.getAppPath(IndexConstants.GPX_INDEX_DIR), fileNameSave);
GPXFile gpx;
boolean saveTrackToRte = measurementPoints.size() <= 1;
if (originalGPX != null) {
gpx = originalGPX;
saveTrackToRte = originalGPX.routes.size() > 0 && originalGPX.tracks.size() == 0;
gpx.tracks.clear();
gpx.routes.clear();
gpx.points.clear();
} else {
gpx = new GPXFile();
}
for (int i = 0; i < measurementPoints.size(); i++) {
LinkedList<WptPt> lt = measurementPoints.get(i);
if (lt.size() == 1) {
gpx.points.add(lt.getFirst());
} else if (lt.size() > 1) {
if (saveTrackToRte) {
Route rt = new Route();
gpx.routes.add(rt);
rt.points.addAll(lt);
} else {
if (gpx.tracks.size() == 0) {
gpx.tracks.add(new Track());
}
Track ts = gpx.tracks.get(gpx.tracks.size() - 1);
TrkSegment sg = new TrkSegment();
ts.segments.add(sg);
sg.points.addAll(lt);
}
}
}
return GPXUtilities.writeGpxFile(toSave, gpx, app);
}
@Override
protected void onPreExecute() {
dlg = new ProgressDialog(activity);
dlg.setMessage(app.getString(R.string.saving_gpx_tracks));
dlg.show();
};
@Override
protected void onPostExecute(String warning) {
if (warning == null) {
Toast.makeText(activity,
MessageFormat.format(app.getString(R.string.gpx_saved_sucessfully), toSave.getAbsolutePath()),
Toast.LENGTH_LONG).show();
} else {
Toast.makeText(activity, warning, Toast.LENGTH_LONG).show();
}
if(dlg != null && dlg.isShowing()) {
dlg.dismiss();
}
};
};
exportTask.execute();
}
private void startEditingHelp(MapActivity ctx) {
final CommonPreference<Boolean> pref = app.getSettings().registerBooleanPreference("show_measurement_help_first_time", true);
pref.makeGlobal();
if(pref.get()) {
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setMessage(R.string.use_distance_measurement_help);
builder.setNegativeButton(R.string.shared_string_do_not_show_again, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
pref.set(false);
}
});
builder.setPositiveButton(R.string.shared_string_ok, null);
builder.show();
}
}
private TextInfoWidget createDistanceControl(final MapActivity activity) {
final TextInfoWidget distanceControl = new TextInfoWidget(activity);
distanceControl.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showDialog(activity);
}
});
distanceControl.setIcons(R.drawable.widget_distance_day, R.drawable.widget_distance_night);
return distanceControl;
}
private void calculateDistance() {
float dist = 0;
if (measurementPoints.size() == 0 && distanceMeasurementMode == 0 ) {
distance = null;
} else {
for (int j = 0; j < measurementPoints.size(); j++) {
List<WptPt> ls = measurementPoints.get(j);
for (int i = 1; i < ls.size(); i++) {
dist += MapUtils.getDistance(ls.get(i - 1).lat, ls.get(i - 1).lon, ls.get(i).lat, ls.get(i).lon);
}
}
distance = OsmAndFormatter.getFormattedDistance(dist, app);
}
updateText();
}
public class DistanceCalculatorLayer extends OsmandMapLayer implements ContextMenuLayer.IContextMenuProvider {
private OsmandMapTileView view;
private Bitmap originIcon;
private Bitmap destinationIcon;
private Paint bitmapPaint;
private Path path;
private Paint paint;
private Paint paint2;
public DistanceCalculatorLayer() {
}
@Override
public void initLayer(OsmandMapTileView view) {
this.view = view;
originIcon = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_pin_origin);
destinationIcon = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_pin_destination);
bitmapPaint = new Paint();
bitmapPaint.setDither(true);
bitmapPaint.setAntiAlias(true);
bitmapPaint.setFilterBitmap(true);
path = new Path();
int distanceColor = view.getResources().getColor(R.color.color_distance);
paint = new Paint();
paint.setStyle(Style.STROKE);
paint.setStrokeWidth(7 * view.getDensity());
paint.setAntiAlias(true);
paint.setStrokeCap(Cap.ROUND);
paint.setStrokeJoin(Join.ROUND);
paint.setColor(distanceColor);
paint2 = new Paint();
paint2.setStyle(Style.FILL_AND_STROKE);
paint2.setAntiAlias(true);
paint2.setColor(distanceColor);
}
@Override
public boolean onSingleTap(PointF point, RotatedTileBox tileBox) {
if(distanceMeasurementMode == 1) {
LatLon l = tileBox.getLatLonFromPixel(point.x, point.y);
if(measurementPoints.size() == 0) {
measurementPoints.add(new LinkedList<GPXUtilities.WptPt>());
}
WptPt pt = new WptPt();
pt.lat = l.getLatitude();
pt.lon = l.getLongitude();
measurementPoints.get(measurementPoints.size() - 1).add(pt);
calculateDistance();
view.refreshMap();
updateText();
return true;
}
return false;
}
@Override
public boolean onLongPressEvent(PointF point, RotatedTileBox tileBox) {
List<Object> s = new ArrayList<>();
collectObjectsFromPoint(point, tileBox, s);
if (s.size() == 0 && distanceMeasurementMode == 1 && measurementPoints.size() > 0) {
LinkedList<WptPt> lt = measurementPoints.get(measurementPoints.size() - 1);
if (lt.size() > 0) {
lt.removeLast();
}
calculateDistance();
view.refreshMap();
updateText();
return true;
}
return false;
}
@Override
public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) {
if (measurementPoints.size() > 0) {
path.reset();
int marginY = originIcon.getHeight();
int marginX = originIcon.getWidth() / 2;
for (int i = 0; i < measurementPoints.size(); i++) {
Iterator<WptPt> it = measurementPoints.get(i).iterator();
boolean first = true;
while (it.hasNext()) {
WptPt point = it.next();
int locationX = tileBox.getPixXFromLonNoRot(point.lon);
int locationY = tileBox.getPixYFromLatNoRot(point.lat);
if (first) {
path.moveTo(locationX, locationY);
first = false;
} else {
path.lineTo(locationX, locationY);
}
}
}
canvas.drawPath(path, paint);
for (int i = 0; i < measurementPoints.size(); i++) {
Iterator<WptPt> it = measurementPoints.get(i).iterator();
boolean first = true;
while(it.hasNext()) {
WptPt pt = it.next();
if (tileBox.containsLatLon(pt.lat, pt.lon)) {
int locationX = tileBox.getPixXFromLonNoRot(pt.lon);
int locationY = tileBox.getPixYFromLatNoRot(pt.lat);
if(first || !it.hasNext() || pt.desc != null) {
canvas.rotate(-view.getRotate(), locationX, locationY);
canvas.drawBitmap(distanceMeasurementMode == 1? originIcon : destinationIcon,
locationX - marginX, locationY - marginY, bitmapPaint);
canvas.rotate(view.getRotate(), locationX, locationY);
} else if(tileBox.getZoom() >= 16){
canvas.drawCircle(locationX, locationY, 10 * tileBox.getDensity(), paint2);
}
}
first = false;
}
}
}
}
@Override
public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) {
}
@Override
public void destroyLayer() {
}
@Override
public boolean drawInScreenPixels() {
return false;
}
@Override
public boolean disableSingleTap() {
return distanceMeasurementMode == 1;
}
@Override
public boolean disableLongPressOnMap() {
return distanceMeasurementMode == 1;
}
@Override
public boolean isObjectClickable(Object o) {
return false;
}
@Override
public void collectObjectsFromPoint(PointF point, RotatedTileBox tileBox, List<Object> o) {
getMPointsFromPoint(tileBox, point, o);
}
public void getMPointsFromPoint(RotatedTileBox tb, PointF point, List<? super WptPt> res) {
int r = (int) (14 * tb.getDensity());
int rs = (int) (10 * tb.getDensity());
int ex = (int) point.x;
int ey = (int) point.y;
for (int i = 0; i < measurementPoints.size(); i++) {
Iterator<WptPt> it = measurementPoints.get(i).iterator();
boolean first = true;
while (it.hasNext()) {
WptPt pt = it.next();
int x = (int) tb.getPixXFromLatLon(pt.lat, pt.lon);
int y = (int) tb.getPixYFromLatLon(pt.lat, pt.lon);
if (pt.desc != null || !it.hasNext() || first) {
if (calculateBelongsBig(ex, ey, x, y, r)) {
res.add(pt);
}
} else {
if (calculateBelongsSmall(ex, ey, x, y, rs)) {
res.add(pt);
}
}
first = false;
}
}
}
private boolean calculateBelongsBig(int ex, int ey, int objx, int objy, int radius) {
return Math.abs(objx - ex) <= radius && (ey - objy) <= radius / 2 && (objy - ey) <= 3 * radius ;
}
private boolean calculateBelongsSmall(int ex, int ey, int objx, int objy, int radius) {
return Math.abs(objx - ex) <= radius && Math.abs(ey - objy) <= radius ;
}
@Override
public LatLon getObjectLocation(Object o) {
if (o instanceof WptPt) {
return new LatLon(((WptPt) o).lat, ((WptPt) o).lon);
}
return null;
}
@Override
public void populateObjectContextMenu(LatLon latLon, Object o, ContextMenuAdapter adapter, MapActivity mapActivity) {
if (o != null && o instanceof WptPt) {
final WptPt p = (WptPt) o;
boolean containsPoint = false;
for (int i = 0; i < measurementPoints.size(); i++) {
for (WptPt wptPt : measurementPoints.get(i)) {
if (wptPt == p) {
containsPoint = true;
break;
}
}
}
if (containsPoint) {
ContextMenuAdapter.ItemClickListener listener = new ContextMenuAdapter.ItemClickListener() {
@Override
public boolean onContextMenuClick(ArrayAdapter<ContextMenuItem> adapter, int itemId, int pos, boolean isChecked) {
if (itemId == R.string.delete_point) {
for (int i = 0; i < measurementPoints.size(); i++) {
Iterator<WptPt> it = measurementPoints.get(i).iterator();
while (it.hasNext()) {
if (it.next() == p) {
it.remove();
}
}
}
calculateDistance();
if (adapter.getContext() instanceof MapActivity) {
((MapActivity)adapter.getContext()).getContextMenu().close();
}
}
return true;
}
};
adapter.addItem(new ContextMenuItem.ItemBuilder()
.setTitleId(R.string.delete_point, mapActivity)
.setIcon(R.drawable.ic_action_delete_dark)
.setListener(listener).createItem());
}
}
}
@Override
public PointDescription getObjectName(Object o) {
if(o instanceof WptPt) {
if(((WptPt) o).desc == null) {
return new PointDescription(PointDescription.POINT_TYPE_MARKER, app.getString(R.string.plugin_distance_point));
}
return new PointDescription(PointDescription.POINT_TYPE_MARKER, ((WptPt) o).desc );
}
return null;
}
}
@Override
public Class<? extends Activity> getSettingsActivity() {
return null;
}
@Override
public int getAssetResourceName() {
return R.drawable.distance_calculator;
}
@Override
public int getLogoResourceId() {
return R.drawable.ic_action_marker_dark;
}
}