package net.osmand.plus.osmedit;
import android.annotation.SuppressLint;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.os.AsyncTask;
import android.support.v7.app.AlertDialog;
import android.util.Xml;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import net.osmand.AndroidUtils;
import net.osmand.PlatformUtil;
import net.osmand.data.LatLon;
import net.osmand.data.PointDescription;
import net.osmand.data.QuadRect;
import net.osmand.data.QuadTree;
import net.osmand.data.RotatedTileBox;
import net.osmand.osm.io.NetworkUtils;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.OsmandSettings;
import net.osmand.plus.R;
import net.osmand.plus.activities.MapActivity;
import net.osmand.plus.osmedit.OsmBugsUtil.OsmBugResult;
import net.osmand.plus.osmedit.OsmPoint.Action;
import net.osmand.plus.views.ContextMenuLayer.IContextMenuProvider;
import net.osmand.plus.views.OsmandMapLayer;
import net.osmand.plus.views.OsmandMapTileView;
import net.osmand.util.Algorithms;
import org.apache.commons.logging.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
public class OsmBugsLayer extends OsmandMapLayer implements IContextMenuProvider {
private static final Log log = PlatformUtil.getLog(OsmBugsLayer.class);
private final static int startZoom = 8;
private final OsmEditingPlugin plugin;
private OsmandMapTileView view;
private Paint paintIcon;
private Bitmap unresolvedNote;
private Bitmap resolvedNote;
private Bitmap unresolvedNoteSmall;
private Bitmap resolvedNoteSmall;
private final MapActivity activity;
private OsmBugsLocalUtil local;
private MapLayerData<List<OpenStreetNote>> data;
public OsmBugsLayer(MapActivity activity, OsmEditingPlugin plugin) {
this.activity = activity;
this.plugin = plugin;
local = plugin.getOsmNotesLocalUtil();
}
public OsmBugsUtil getOsmbugsUtil(OpenStreetNote bug) {
OsmandSettings settings = ((OsmandApplication) activity.getApplication()).getSettings();
if ((bug != null && bug.isLocal()) || settings.OFFLINE_EDITION.get()
|| !settings.isInternetConnectionAvailable(true)) {
return local;
} else {
return plugin.getOsmNotesRemoteUtil();
}
}
@Override
public void initLayer(OsmandMapTileView view) {
this.view = view;
paintIcon = new Paint();
resolvedNote = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_osm_resolved);
unresolvedNote = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_osm_unresolved);
resolvedNoteSmall = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_osm_resolved_small);
unresolvedNoteSmall = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_osm_unresolved_small);
data = new OsmandMapLayer.MapLayerData<List<OpenStreetNote>>() {
{
ZOOM_THRESHOLD = 1;
}
@Override
protected List<OpenStreetNote> calculateResult(RotatedTileBox tileBox) {
QuadRect bounds = tileBox.getLatLonBounds();
return loadingBugs(bounds.top, bounds.left, bounds.bottom, bounds.right);
}
};
}
@Override
public void destroyLayer() {
}
@Override
public boolean drawInScreenPixels() {
return true;
}
@Override
public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) {
if (tileBox.getZoom() >= startZoom) {
// request to load
data.queryNewData(tileBox);
List<OpenStreetNote> objects = data.getResults();
if (objects != null) {
float iconSize = resolvedNote.getWidth() * 3 / 2.5f;
QuadTree<QuadRect> boundIntersections = initBoundIntersections(tileBox);
List<OpenStreetNote> fullObjects = new ArrayList<>();
List<LatLon> fullObjectsLatLon = new ArrayList<>();
List<LatLon> smallObjectsLatLon = new ArrayList<>();
for (OpenStreetNote o : objects) {
float x = tileBox.getPixXFromLatLon(o.getLatitude(), o.getLongitude());
float y = tileBox.getPixYFromLatLon(o.getLatitude(), o.getLongitude());
if (intersects(boundIntersections, x, y, iconSize, iconSize)) {
Bitmap b;
if (o.isOpened()) {
b = unresolvedNoteSmall;
} else {
b = resolvedNoteSmall;
}
canvas.drawBitmap(b, x - b.getWidth() / 2, y - b.getHeight() / 2, paintIcon);
smallObjectsLatLon.add(new LatLon(o.getLatitude(), o.getLongitude()));
} else {
fullObjects.add(o);
fullObjectsLatLon.add(new LatLon(o.getLatitude(), o.getLongitude()));
}
}
for (OpenStreetNote o : fullObjects) {
float x = tileBox.getPixXFromLatLon(o.getLatitude(), o.getLongitude());
float y = tileBox.getPixYFromLatLon(o.getLatitude(), o.getLongitude());
Bitmap b;
if (o.isOpened()) {
b = unresolvedNote;
} else {
b = resolvedNote;
}
canvas.drawBitmap(b, x - b.getWidth() / 2, y - b.getHeight() / 2, paintIcon);
}
this.fullObjectsLatLon = fullObjectsLatLon;
this.smallObjectsLatLon = smallObjectsLatLon;
}
}
}
@Override
public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) {
}
public int getRadiusBug(RotatedTileBox tb) {
int z;
final double zoom = tb.getZoom();
if (zoom < startZoom) {
z = 0;
} else if (zoom <= 12) {
z = 8;
} else if (zoom <= 15) {
z = 10;
} else if (zoom == 16) {
z = 13;
} else if (zoom == 17) {
z = 15;
} else {
z = 16;
}
return (int) (z * tb.getDensity());
}
@Override
public boolean onLongPressEvent(PointF point, RotatedTileBox tileBox) {
return false;
}
public void getBugFromPoint(RotatedTileBox tb, PointF point, List<? super OpenStreetNote> res) {
List<OpenStreetNote> objects = data.getResults();
if (objects != null && view != null) {
int ex = (int) point.x;
int ey = (int) point.y;
final int rad = getRadiusBug(tb);
int radius = rad * 3 / 2;
int small = rad * 3 / 4;
try {
for (int i = 0; i < objects.size(); i++) {
OpenStreetNote n = objects.get(i);
int x = (int) tb.getPixXFromLatLon(n.getLatitude(), n.getLongitude());
int y = (int) tb.getPixYFromLatLon(n.getLatitude(), n.getLongitude());
if (Math.abs(x - ex) <= radius && Math.abs(y - ey) <= radius) {
radius = small;
res.add(n);
}
}
} catch (IndexOutOfBoundsException e) {
// that's really rare case, but is much efficient than introduce synchronized block
}
}
}
public void clearCache() {
if (data != null) {
data.clearCache();
}
}
private static String readText(XmlPullParser parser, String key) throws XmlPullParserException, IOException {
int tok;
String text = "";
while ((tok = parser.next()) != XmlPullParser.END_DOCUMENT) {
if (tok == XmlPullParser.END_TAG && parser.getName().equals(key)) {
break;
} else if (tok == XmlPullParser.TEXT) {
text += parser.getText();
}
}
return text;
}
protected List<OpenStreetNote> loadingBugs(double topLatitude, double leftLongitude, double bottomLatitude, double rightLongitude) {
final int deviceApiVersion = android.os.Build.VERSION.SDK_INT;
String SITE_API;
if (deviceApiVersion >= android.os.Build.VERSION_CODES.GINGERBREAD) {
SITE_API = "https://api.openstreetmap.org/";
} else {
SITE_API = "http://api.openstreetmap.org/";
}
List<OpenStreetNote> bugs = new ArrayList<>();
StringBuilder b = new StringBuilder();
b.append(SITE_API).append("api/0.6/notes?bbox="); //$NON-NLS-1$
b.append(leftLongitude); //$NON-NLS-1$
b.append(",").append(bottomLatitude); //$NON-NLS-1$
b.append(",").append(rightLongitude); //$NON-NLS-1$
b.append(",").append(topLatitude); //$NON-NLS-1$
try {
log.info("Loading bugs " + b); //$NON-NLS-1$
URLConnection connection = NetworkUtils.getHttpURLConnection(b.toString());
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
XmlPullParser parser = Xml.newPullParser();
parser.setInput(reader);
int tok;
OpenStreetNote current = null;
int commentIndex = 0;
while ((tok = parser.next()) != XmlPullParser.END_DOCUMENT) {
if (tok == XmlPullParser.START_TAG) {
if (parser.getName().equals("note")) {
current = new OpenStreetNote();
commentIndex = -1;
current.setLongitude(Double.parseDouble(parser.getAttributeValue("", "lon")));
current.setLatitude(Double.parseDouble(parser.getAttributeValue("", "lat")));
current.setOpened(true);
bugs.add(current);
} else if (parser.getName().equals("status") && current != null) {
current.setOpened("open".equals(readText(parser, "status")));
} else if (parser.getName().equals("id") && current != null) {
current.id = Long.parseLong(readText(parser, "id"));
} else if (parser.getName().equals("comment")) {
commentIndex++;
} else if (parser.getName().equals("user") && current != null) {
if (commentIndex == current.users.size()) {
current.users.add(readText(parser, "user"));
}
} else if (parser.getName().equals("date") && current != null) {
if (commentIndex == current.dates.size()) {
current.dates.add(readText(parser, "date"));
}
} else if (parser.getName().equals("text") && current != null) {
if (commentIndex == current.comments.size()) {
current.comments.add(readText(parser, "text"));
}
}
}
}
reader.close();
for (OpenStreetNote note : bugs) {
note.acquireDescriptionAndType();
}
} catch (IOException | RuntimeException | XmlPullParserException e) {
log.warn("Error loading bugs", e); //$NON-NLS-1$
}
return bugs;
}
private void asyncActionTask(final OpenStreetNote bug, final String text, final Action action) {
AsyncTask<Void, Void, OsmBugResult> task = new AsyncTask<Void, Void, OsmBugResult>() {
private OsmBugsUtil osmbugsUtil;
@Override
protected OsmBugResult doInBackground(Void... params) {
osmbugsUtil = getOsmbugsUtil(bug);
OsmNotesPoint pnt = new OsmNotesPoint();
pnt.setId(bug.getId());
pnt.setLatitude(bug.getLatitude());
pnt.setLongitude(bug.getLongitude());
return osmbugsUtil.commit(pnt, text, action);
}
protected void onPostExecute(OsmBugResult obj) {
if (obj != null && obj.warning == null) {
if (local == osmbugsUtil) {
Toast.makeText(activity, R.string.osm_changes_added_to_local_edits, Toast.LENGTH_LONG).show();
if (obj.local != null) {
PointDescription pd = new PointDescription(PointDescription.POINT_TYPE_OSM_BUG, obj.local.getText());
activity.getContextMenu().show(new LatLon(obj.local.getLatitude(), obj.local.getLongitude()), pd, obj.local);
}
} else {
if (action == Action.REOPEN) {
Toast.makeText(activity, R.string.osn_add_dialog_success, Toast.LENGTH_LONG).show();
} else if (action == Action.MODIFY) {
Toast.makeText(activity, R.string.osb_comment_dialog_success, Toast.LENGTH_LONG).show();
} else if (action == Action.DELETE) {
Toast.makeText(activity, R.string.osn_close_dialog_success, Toast.LENGTH_LONG).show();
} else if (action == Action.CREATE) {
Toast.makeText(activity, R.string.osn_add_dialog_success, Toast.LENGTH_LONG).show();
}
}
clearCache();
} else {
int r = R.string.osb_comment_dialog_error;
if (action == Action.REOPEN) {
r = R.string.osn_add_dialog_error;
reopenBug(bug, text);
} else if (action == Action.DELETE) {
r = R.string.osn_close_dialog_error;
closeBug(bug, text);
} else if (action == Action.CREATE) {
r = R.string.osn_add_dialog_error;
openBug(bug.getLatitude(), bug.getLongitude(), text);
} else {
commentBug(bug, text);
}
Toast.makeText(activity, activity.getResources().getString(r) + "\n" + obj, Toast.LENGTH_LONG).show();
}
}
};
executeTaskInBackground(task);
}
public void openBug(final double latitude, final double longitude, String message) {
OpenStreetNote bug = new OpenStreetNote();
bug.setLatitude(latitude);
bug.setLongitude(longitude);
showBugDialog(bug, Action.CREATE, message);
}
public void openBug(final double latitude, final double longitude, String message, boolean autofill) {
OpenStreetNote bug = new OpenStreetNote();
bug.setLatitude(latitude);
bug.setLongitude(longitude);
if (autofill) asyncActionTask(bug, message, Action.CREATE);
else showBugDialog(bug, Action.CREATE, message);
}
public void closeBug(final OpenStreetNote bug, String txt) {
showBugDialog(bug, Action.DELETE, txt);
}
public void reopenBug(final OpenStreetNote bug, String txt) {
showBugDialog(bug, Action.REOPEN, txt);
}
public void commentBug(final OpenStreetNote bug, String txt) {
showBugDialog(bug, Action.MODIFY, txt);
}
private void showBugDialog(final OpenStreetNote bug, final Action action, String text) {
int title;
if (action == Action.DELETE) {
title = R.string.osn_close_dialog_title;
} else if (action == Action.MODIFY) {
title = R.string.osn_comment_dialog_title;
} else if (action == Action.REOPEN) {
title = R.string.osn_reopen_dialog_title;
} else {
title = R.string.osn_add_dialog_title;
}
OsmBugsUtil util = getOsmbugsUtil(bug);
final boolean offline = util instanceof OsmBugsLocalUtil;
@SuppressLint("InflateParams")
final View view = LayoutInflater.from(activity).inflate(R.layout.open_bug, null);
if (offline) {
view.findViewById(R.id.user_name_field).setVisibility(View.GONE);
view.findViewById(R.id.userNameEditTextLabel).setVisibility(View.GONE);
view.findViewById(R.id.password_field).setVisibility(View.GONE);
view.findViewById(R.id.passwordEditTextLabel).setVisibility(View.GONE);
} else {
((EditText) view.findViewById(R.id.user_name_field)).setText(getUserName());
((EditText) view.findViewById(R.id.password_field)).setText(((OsmandApplication) activity.getApplication()).getSettings().USER_PASSWORD.get());
}
if (!Algorithms.isEmpty(text)) {
((EditText) view.findViewById(R.id.message_field)).setText(text);
}
AndroidUtils.softKeyboardDelayed(view.findViewById(R.id.message_field));
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.shared_string_commit);
builder.setView(view);
builder.setPositiveButton(title, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (bug != null) {
String text = offline ? getMessageText(view) : getTextAndUpdateUserPwd(view);
activity.getContextMenu().close();
asyncActionTask(bug, text, action);
}
}
});
builder.setNegativeButton(R.string.shared_string_cancel, null);
builder.create().show();
}
private String getUserName() {
return ((OsmandApplication) activity.getApplication()).getSettings().USER_NAME.get();
}
private String getTextAndUpdateUserPwd(final View view) {
String text = getMessageText(view);
String author = ((EditText) view.findViewById(R.id.user_name_field)).getText().toString();
String pwd = ((EditText) view.findViewById(R.id.password_field)).getText().toString();
((OsmandApplication) OsmBugsLayer.this.activity.getApplication()).getSettings().USER_NAME.set(author);
((OsmandApplication) OsmBugsLayer.this.activity.getApplication()).getSettings().USER_PASSWORD.set(pwd);
return text;
}
private String getMessageText(final View view) {
return ((EditText) view.findViewById(R.id.message_field)).getText().toString();
}
public void refreshMap() {
if (view != null && view.getLayers().contains(OsmBugsLayer.this)) {
view.refreshMap();
}
}
@Override
public PointDescription getObjectName(Object o) {
if (o instanceof OpenStreetNote) {
OpenStreetNote bug = (OpenStreetNote) o;
String name = bug.description != null ? bug.description : "";
String typeName = bug.typeName != null ? bug.typeName : activity.getString(R.string.osn_bug_name);
return new PointDescription(PointDescription.POINT_TYPE_OSM_NOTE, typeName, name);
}
return null;
}
@Override
public boolean disableSingleTap() {
return false;
}
@Override
public boolean disableLongPressOnMap() {
return false;
}
@Override
public boolean isObjectClickable(Object o) {
return o instanceof OpenStreetNote;
}
@Override
public void collectObjectsFromPoint(PointF point, RotatedTileBox tileBox, List<Object> res) {
if (tileBox.getZoom() >= startZoom) {
getBugFromPoint(tileBox, point, res);
}
}
@Override
public LatLon getObjectLocation(Object o) {
if (o instanceof OpenStreetNote) {
return new LatLon(((OpenStreetNote) o).getLatitude(), ((OpenStreetNote) o).getLongitude());
}
return null;
}
public static class OpenStreetNote implements Serializable {
private boolean local;
private static final long serialVersionUID = -7848941747811172615L;
private double latitude;
private double longitude;
private String description;
private String typeName;
private List<String> dates = new ArrayList<>();
private List<String> comments = new ArrayList<>();
private List<String> users = new ArrayList<>();
private long id;
private boolean opened;
private void acquireDescriptionAndType() {
if (comments.size() > 0) {
StringBuilder sb = new StringBuilder();
if (dates.size() > 0) {
sb.append(dates.get(0)).append(" ");
}
if (users.size() > 0) {
sb.append(users.get(0));
}
description = comments.get(0);
typeName = sb.toString();
}
if (description != null && description.length() < 100) {
if (comments.size() > 0) {
comments.remove(0);
}
if (dates.size() > 0) {
dates.remove(0);
}
if (users.size() > 0) {
users.remove(0);
}
}
}
public double getLatitude() {
return latitude;
}
public void setLatitude(double latitude) {
this.latitude = latitude;
}
public double getLongitude() {
return longitude;
}
public void setLongitude(double longitude) {
this.longitude = longitude;
}
public String getDescription() {
return description;
}
public String getTypeName() {
return typeName;
}
public String getCommentDescription() {
StringBuilder sb = new StringBuilder();
for (String s : getCommentDescriptionList()) {
if (sb.length() > 0) {
sb.append("\n");
}
sb.append(s);
}
return sb.toString();
}
public List<String> getCommentDescriptionList() {
List<String> res = new ArrayList<>(comments.size());
for (int i = 0; i < comments.size(); i++) {
StringBuilder sb = new StringBuilder();
boolean needLineFeed = false;
if (i < dates.size()) {
sb.append(dates.get(i)).append(" ");
needLineFeed = true;
}
if (i < users.size()) {
sb.append(users.get(i)).append(":");
needLineFeed = true;
}
if (needLineFeed) {
sb.append("\n");
}
sb.append(comments.get(i));
res.add(sb.toString());
}
return res;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public boolean isOpened() {
return opened;
}
public void setOpened(boolean opened) {
this.opened = opened;
}
public boolean isLocal() {
return local;
}
public void setLocal(boolean local) {
this.local = local;
}
}
}