package net.osmand.plus.osmedit; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.text.MessageFormat; import java.util.LinkedHashMap; import java.util.Map; import net.osmand.PlatformUtil; import net.osmand.data.Amenity; import net.osmand.osm.PoiType; import net.osmand.osm.edit.Entity; import net.osmand.osm.edit.Entity.EntityId; import net.osmand.osm.edit.Entity.EntityType; import net.osmand.osm.edit.EntityInfo; import net.osmand.osm.edit.Node; import net.osmand.osm.io.Base64; import net.osmand.osm.io.NetworkUtils; import net.osmand.osm.io.OsmBaseStorage; import net.osmand.plus.OsmandApplication; import net.osmand.plus.OsmandSettings; import net.osmand.plus.R; import net.osmand.plus.Version; import net.osmand.util.MapUtils; import org.apache.commons.logging.Log; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import android.util.Xml; import android.widget.Toast; public class OpenstreetmapRemoteUtil implements OpenstreetmapUtil { private static final long NO_CHANGESET_ID = -1; private final OsmandApplication ctx; private EntityInfo entityInfo; private EntityId entityInfoId; // reuse changeset private long changeSetId = NO_CHANGESET_ID; private long changeSetTimeStamp = NO_CHANGESET_ID; public final static Log log = PlatformUtil.getLog(OpenstreetmapRemoteUtil.class); private OsmandSettings settings; public OpenstreetmapRemoteUtil(OsmandApplication app) { this.ctx = app; settings = ctx.getSettings(); } @Override public EntityInfo getEntityInfo(long id) { if(entityInfoId != null && entityInfoId.getId().longValue() == id) { return entityInfo; } return null; } private static String getSiteApi() { final int deviceApiVersion = android.os.Build.VERSION.SDK_INT; String RETURN_API; if (deviceApiVersion >= android.os.Build.VERSION_CODES.GINGERBREAD) { RETURN_API = "https://api.openstreetmap.org/"; } else { RETURN_API = "http://api.openstreetmap.org/"; } // RETURN_API = "http://api06.dev.openstreetmap.org/"; return RETURN_API; } private final static String URL_TO_UPLOAD_GPX = getSiteApi() + "api/0.6/gpx/create"; public String uploadGPXFile(String tagstring, String description, String visibility, File f) { String url = URL_TO_UPLOAD_GPX; Map<String, String> additionalData = new LinkedHashMap<String, String>(); additionalData.put("description", description); additionalData.put("tags", tagstring); additionalData.put("visibility", visibility); return NetworkUtils.uploadFile(url, f, settings.USER_NAME.get() + ":" + settings.USER_PASSWORD.get(), "file", true, additionalData); } private String sendRequest(String url, String requestMethod, String requestBody, String userOperation, boolean doAuthenticate) { log.info("Sending request " + url); //$NON-NLS-1$ try { HttpURLConnection connection = NetworkUtils.getHttpURLConnection(url); connection.setConnectTimeout(15000); connection.setRequestMethod(requestMethod); connection.setRequestProperty("User-Agent", Version.getFullVersion(ctx)); //$NON-NLS-1$ StringBuilder responseBody = new StringBuilder(); if (doAuthenticate) { String token = settings.USER_NAME.get() + ":" + settings.USER_PASSWORD.get(); //$NON-NLS-1$ connection.addRequestProperty("Authorization", "Basic " + Base64.encode(token.getBytes("UTF-8"))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } connection.setDoInput(true); if (requestMethod.equals("PUT") || requestMethod.equals("POST") || requestMethod.equals("DELETE")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ connection.setDoOutput(true); connection.setRequestProperty("Content-type", "text/xml"); //$NON-NLS-1$ //$NON-NLS-2$ OutputStream out = connection.getOutputStream(); if (requestBody != null) { BufferedWriter bwr = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"), 1024); //$NON-NLS-1$ bwr.write(requestBody); bwr.flush(); } out.close(); } connection.connect(); if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { String msg = userOperation + " " + ctx.getString(R.string.failed_op) + " : " + connection.getResponseMessage(); //$NON-NLS-1$//$NON-NLS-2$ log.error(msg); showWarning(msg); } else { log.info("Response : " + connection.getResponseMessage()); //$NON-NLS-1$ // populate return fields. responseBody.setLength(0); InputStream i = connection.getInputStream(); if (i != null) { BufferedReader in = new BufferedReader(new InputStreamReader(i, "UTF-8"), 256); //$NON-NLS-1$ String s; boolean f = true; while ((s = in.readLine()) != null) { if (!f) { responseBody.append("\n"); //$NON-NLS-1$ } else { f = false; } responseBody.append(s); } } return responseBody.toString(); } } catch (NullPointerException e) { // that's tricky case why NPE is thrown to fix that problem httpClient could be used String msg = ctx.getString(R.string.auth_failed); log.error(msg, e); showWarning(msg); } catch (MalformedURLException e) { log.error(userOperation + " " + ctx.getString(R.string.failed_op), e); //$NON-NLS-1$ showWarning(MessageFormat.format(ctx.getResources().getString(R.string.shared_string_action_template) + ": " + ctx.getResources().getString(R.string.shared_string_unexpected_error), userOperation)); } catch (IOException e) { log.error(userOperation + " " + ctx.getString(R.string.failed_op), e); //$NON-NLS-1$ showWarning(MessageFormat.format(ctx.getResources().getString(R.string.shared_string_action_template) + ": " + ctx.getResources().getString(R.string.shared_string_io_error), userOperation)); } return null; } public long openChangeSet(String comment) { long id = -1; StringWriter writer = new StringWriter(256); XmlSerializer ser = Xml.newSerializer(); try { ser.setOutput(writer); ser.startDocument("UTF-8", true); //$NON-NLS-1$ ser.startTag(null, "osm"); //$NON-NLS-1$ ser.startTag(null, "changeset"); //$NON-NLS-1$ if(comment != null) { ser.startTag(null, "tag"); //$NON-NLS-1$ ser.attribute(null, "k", "comment"); //$NON-NLS-1$ //$NON-NLS-2$ ser.attribute(null, "v", comment); //$NON-NLS-1$ ser.endTag(null, "tag"); //$NON-NLS-1$ } ser.startTag(null, "tag"); //$NON-NLS-1$ ser.attribute(null, "k", "created_by"); //$NON-NLS-1$ //$NON-NLS-2$ ser.attribute(null, "v", Version.getFullVersion(ctx)); //$NON-NLS-1$ ser.endTag(null, "tag"); //$NON-NLS-1$ ser.endTag(null, "changeset"); //$NON-NLS-1$ ser.endTag(null, "osm"); //$NON-NLS-1$ ser.endDocument(); writer.close(); } catch (IOException e) { log.error("Unhandled exception", e); //$NON-NLS-1$ } String response = sendRequest( getSiteApi() + "api/0.6/changeset/create/", "PUT", writer.getBuffer().toString(), ctx.getString(R.string.opening_changeset), true); //$NON-NLS-1$ //$NON-NLS-2$ if (response != null && response.length() > 0) { id = Long.parseLong(response); } return id; } private void writeNode(Node n, EntityInfo i, XmlSerializer ser, long changeSetId, String user) throws IllegalArgumentException, IllegalStateException, IOException { ser.startTag(null, "node"); //$NON-NLS-1$ ser.attribute(null, "id", n.getId() + ""); //$NON-NLS-1$ //$NON-NLS-2$ ser.attribute(null, "lat", n.getLatitude() + ""); //$NON-NLS-1$ //$NON-NLS-2$ ser.attribute(null, "lon", n.getLongitude() + ""); //$NON-NLS-1$ //$NON-NLS-2$ if (i != null) { // ser.attribute(null, "timestamp", i.getETimestamp()); // ser.attribute(null, "uid", i.getUid()); // ser.attribute(null, "user", i.getUser()); ser.attribute(null, "visible", i.getVisible()); //$NON-NLS-1$ ser.attribute(null, "version", i.getVersion()); //$NON-NLS-1$ } ser.attribute(null, "changeset", changeSetId + ""); //$NON-NLS-1$ //$NON-NLS-2$ for (String k : n.getTagKeySet()) { String val = n.getTag(k); if (val.length() == 0 || k.length() == 0 || EditPoiData.POI_TYPE_TAG.equals(k) || k.startsWith(EditPoiData.REMOVE_TAG_PREFIX) || n.getTag(EditPoiData.REMOVE_TAG_PREFIX + k) != null) continue; ser.startTag(null, "tag"); //$NON-NLS-1$ ser.attribute(null, "k", k); //$NON-NLS-1$ ser.attribute(null, "v", val); //$NON-NLS-1$ ser.endTag(null, "tag"); //$NON-NLS-1$ } ser.endTag(null, "node"); //$NON-NLS-1$ } private boolean isNewChangesetRequired() { // first commit if (changeSetId == NO_CHANGESET_ID) { return true; } long now = System.currentTimeMillis(); // changeset is idle for more than 30 minutes (1 hour according specification) if (now - changeSetTimeStamp > 30 * 60 * 1000) { return true; } return false; } @Override public Node commitNodeImpl(OsmPoint.Action action, final Node n, EntityInfo info, String comment, boolean closeChangeSet) { if (isNewChangesetRequired()) { changeSetId = openChangeSet(comment); changeSetTimeStamp = System.currentTimeMillis(); } if (changeSetId < 0) { return null; } try { Node newN = n; StringWriter writer = new StringWriter(256); XmlSerializer ser = Xml.newSerializer(); try { ser.setOutput(writer); ser.startDocument("UTF-8", true); //$NON-NLS-1$ ser.startTag(null, "osmChange"); //$NON-NLS-1$ ser.attribute(null, "version", "0.6"); //$NON-NLS-1$ //$NON-NLS-2$ ser.attribute(null, "generator", Version.getAppName(ctx)); //$NON-NLS-1$ ser.startTag(null, OsmPoint.stringAction.get(action)); ser.attribute(null, "version", "0.6"); //$NON-NLS-1$ //$NON-NLS-2$ ser.attribute(null, "generator", Version.getAppName(ctx)); //$NON-NLS-1$ writeNode(n, info, ser, changeSetId, settings.USER_NAME.get()); ser.endTag(null, OsmPoint.stringAction.get(action)); ser.endTag(null, "osmChange"); //$NON-NLS-1$ ser.endDocument(); } catch (IOException e) { log.error("Unhandled exception", e); //$NON-NLS-1$ } String res = sendRequest(getSiteApi() + "api/0.6/changeset/" + changeSetId + "/upload", "POST", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ writer.getBuffer().toString(), ctx.getString(R.string.commiting_node), true); log.debug(res + ""); //$NON-NLS-1$ if (res != null) { if (OsmPoint.Action.CREATE == action) { long newId = n.getId(); int i = res.indexOf("new_id=\""); //$NON-NLS-1$ if (i > 0) { i = i + "new_id=\"".length(); //$NON-NLS-1$ int end = res.indexOf('\"', i); //$NON-NLS-1$ if (end > 0) { newId = Long.parseLong(res.substring(i, end)); // << 1; newN = new Node(n, newId); } } } changeSetTimeStamp = System.currentTimeMillis(); return newN; } return null; } finally { if (closeChangeSet) { closeChangeSet(); } } } @Override public void closeChangeSet() { if (changeSetId != NO_CHANGESET_ID) { String response = sendRequest( getSiteApi() + "api/0.6/changeset/" + changeSetId + "/close", "PUT", "", ctx.getString(R.string.closing_changeset), true); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ log.info("Response : " + response); //$NON-NLS-1$ changeSetId = NO_CHANGESET_ID; } } public EntityInfo loadNode(Node n) { long nodeId = n.getId(); // >> 1; try { String res = sendRequest( getSiteApi() + "api/0.6/node/" + nodeId, "GET", null, ctx.getString(R.string.loading_poi_obj) + nodeId, false); //$NON-NLS-1$ //$NON-NLS-2$ if (res != null) { OsmBaseStorage st = new OsmBaseStorage(); st.setConvertTagsToLC(false); st.parseOSM(new ByteArrayInputStream(res.getBytes("UTF-8")), null, null, true); //$NON-NLS-1$ EntityId id = new Entity.EntityId(EntityType.NODE, nodeId); Node entity = (Node) st.getRegisteredEntities().get(id); // merge non existing tags for (String rtag : entity.getTagKeySet()) { if (!n.getTagKeySet().contains(rtag)) { n.putTagNoLC(rtag, entity.getTag(rtag)); } } if(MapUtils.getDistance(n.getLatLon(), entity.getLatLon()) < 10) { // avoid shifting due to round error n.setLatitude(entity.getLatitude()); n.setLongitude(entity.getLongitude()); } entityInfo = st.getRegisteredEntityInfo().get(id); entityInfoId = id; return entityInfo; } } catch (IOException | XmlPullParserException e) { log.error("Loading node failed " + nodeId, e); //$NON-NLS-1$ Toast.makeText(ctx, ctx.getResources().getString(R.string.shared_string_io_error), Toast.LENGTH_LONG).show(); } return null; } @Override public Node loadNode(Amenity n) { if (n.getId() % 2 == 1) { // that's way id return null; } long nodeId = n.getId() >> 1; try { String res = sendRequest( getSiteApi() + "api/0.6/node/" + nodeId, "GET", null, ctx.getString(R.string.loading_poi_obj) + nodeId, false); //$NON-NLS-1$ //$NON-NLS-2$ if (res != null) { OsmBaseStorage st = new OsmBaseStorage(); st.setConvertTagsToLC(false); st.parseOSM(new ByteArrayInputStream(res.getBytes("UTF-8")), null, null, true); //$NON-NLS-1$ EntityId id = new Entity.EntityId(EntityType.NODE, nodeId); Node entity = (Node) st.getRegisteredEntities().get(id); entityInfo = st.getRegisteredEntityInfo().get(id); entityInfoId = id; // check whether this is node (because id of node could be the same as relation) if (entity != null && MapUtils.getDistance(entity.getLatLon(), n.getLocation()) < 50) { PoiType poiType = n.getType().getPoiTypeByKeyName(n.getSubType()); if(poiType.getOsmValue().equals(entity.getTag(poiType.getOsmTag()))) { entity.removeTag(poiType.getOsmTag()); entity.putTagNoLC(EditPoiData.POI_TYPE_TAG, poiType.getTranslation()); } else { // later we could try to determine tags } return entity; } return null; } } catch (Exception e) { log.error("Loading node failed " + nodeId, e); //$NON-NLS-1$ ctx.runInUIThread(new Runnable() { @Override public void run() { Toast.makeText(ctx, ctx.getResources().getString(R.string.shared_string_io_error), Toast.LENGTH_LONG).show(); } }); } return null; } private void showWarning(final String msg) { ctx.runInUIThread(new Runnable() { @Override public void run() { Toast.makeText(ctx, msg, Toast.LENGTH_LONG).show(); } }); } }