Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
josm-plugins/DirectUpload/src/org/openstreetmap/josm/plugins/DirectUpload/UploadDataGui.java /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
523 lines (457 sloc)
19.7 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // License: GPL. For details, see LICENSE file. | |
| package org.openstreetmap.josm.plugins.DirectUpload; | |
| import static org.openstreetmap.josm.tools.I18n.tr; | |
| import java.awt.GridBagLayout; | |
| import java.awt.event.ActionEvent; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.IOException; | |
| import java.net.MalformedURLException; | |
| import java.net.URL; | |
| import java.nio.charset.StandardCharsets; | |
| import java.text.DecimalFormat; | |
| import java.text.SimpleDateFormat; | |
| import java.util.Date; | |
| import javax.swing.JComboBox; | |
| import javax.swing.JLabel; | |
| import javax.swing.JPanel; | |
| import org.openstreetmap.josm.data.gpx.GpxConstants; | |
| import org.openstreetmap.josm.data.gpx.GpxData; | |
| import org.openstreetmap.josm.data.gpx.IGpxTrack; | |
| import org.openstreetmap.josm.gui.ExtendedDialog; | |
| import org.openstreetmap.josm.gui.MainApplication; | |
| import org.openstreetmap.josm.gui.PleaseWaitRunnable; | |
| import org.openstreetmap.josm.gui.progress.ProgressMonitor; | |
| import org.openstreetmap.josm.gui.util.GuiHelper; | |
| import org.openstreetmap.josm.gui.widgets.HistoryComboBox; | |
| import org.openstreetmap.josm.gui.widgets.JMultilineLabel; | |
| import org.openstreetmap.josm.gui.widgets.UrlLabel; | |
| import org.openstreetmap.josm.io.GpxWriter; | |
| import org.openstreetmap.josm.io.OsmApi; | |
| import org.openstreetmap.josm.io.OsmTransferException; | |
| import org.openstreetmap.josm.spi.preferences.Config; | |
| import org.openstreetmap.josm.tools.GBC; | |
| import org.openstreetmap.josm.tools.HttpClient; | |
| import org.openstreetmap.josm.tools.HttpClient.Response; | |
| import org.openstreetmap.josm.tools.Logging; | |
| /** | |
| * | |
| * @author subhodip, xeen, ax | |
| */ | |
| public class UploadDataGui extends ExtendedDialog { | |
| /** | |
| * This enum contains the possible values for the visibility field and their | |
| * explanation. Provides some methods for easier handling. | |
| */ | |
| private enum visibility { | |
| PRIVATE (tr("Private (only shared as anonymous, unordered points)")), | |
| PUBLIC (tr("Public (shown in trace list and as anonymous, unordered points)")), | |
| TRACKABLE (tr("Trackable (only shared as anonymous, ordered points with timestamps)")), | |
| IDENTIFIABLE (tr("Identifiable (shown in trace list and as identifiable, ordered points with timestamps)")); | |
| public final String description; | |
| visibility(String description) { | |
| this.description = description; | |
| } | |
| /** | |
| * "Converts" a given description into the actual enum. Returns null if no matching description | |
| * is found. | |
| * @param desc The description to look for | |
| * @return visibility or null | |
| */ | |
| public static visibility desc2visi(Object desc) { | |
| for (visibility v : visibility.values()) { | |
| if(desc.equals(v.description)) | |
| return v; | |
| } | |
| return null; | |
| } | |
| @Override | |
| public String toString() { | |
| return this.name().toLowerCase(); | |
| } | |
| } | |
| // Fields are declared here for easy access | |
| // Do not remove the space in JMultilineLabel. Otherwise the label will be empty | |
| // as we don't know its contents yet and therefore have a height of 0. This will | |
| // lead to unnecessary scrollbars. | |
| private JMultilineLabel outputDisplay = new JMultilineLabel(" "); | |
| private HistoryComboBox descriptionField; | |
| private HistoryComboBox tagsField; | |
| private JComboBox<String> visibilityCombo; | |
| // Constants used when generating upload request | |
| private static final String BOUNDARY = "----------------------------d10f7aa230e8"; | |
| private static final String LINE_END = "\r\n"; | |
| private static final String uploadTraceText = tr("Upload Trace"); | |
| private boolean canceled = false; | |
| public UploadDataGui() { | |
| // Initalizes ExtendedDialog | |
| super(MainApplication.getMainFrame(), | |
| tr("Upload Traces"), | |
| new String[] {uploadTraceText, tr("Cancel")}, | |
| true | |
| ); | |
| JPanel content = initComponents(); | |
| GpxData gpxData = UploadOsmConnection.getInstance().autoSelectTrace(); | |
| initTitleAndDescriptionFromGpxData(gpxData); // this is changing some dialog elements, so it (probably) must be before the following | |
| setContent(content); | |
| setButtonIcons(new String[] { "UploadAction", "cancel" }); | |
| setupDialog(); | |
| buttons.get(0).setEnabled(gpxData != null); | |
| } | |
| /** | |
| * Sets up the dialog window elements | |
| * @return JPanel with components | |
| */ | |
| private JPanel initComponents() { | |
| // visibilty | |
| JLabel visibilityLabel = new JLabel(tr("Visibility")); | |
| visibilityLabel.setToolTipText(tr("Defines the visibility of your trace for other OSM users.")); | |
| visibilityCombo = new JComboBox<>(); | |
| visibilityCombo.setEditable(false); | |
| for(visibility v : visibility.values()) { | |
| visibilityCombo.addItem(v.description); | |
| } | |
| visibilityCombo.setSelectedItem(visibility.valueOf(Config.getPref().get("directupload.visibility.last-used", visibility.PRIVATE.name())).description); | |
| /* I18n: either copy the link verbose or replace it by the translated version from the wiki for already translated languages */ | |
| UrlLabel visiUrl = new UrlLabel(tr("https://wiki.openstreetmap.org/wiki/Visibility_of_GPS_traces"), tr("(What does that mean?)"), 2); | |
| // description | |
| JLabel descriptionLabel = new JLabel(tr("Description")); | |
| descriptionField = new HistoryComboBox(); | |
| descriptionField.setToolTipText(tr("Please enter Description about your trace.")); | |
| descriptionField.getModel().prefs().load("directupload.description.history"); | |
| // tags | |
| JLabel tagsLabel = new JLabel(tr("Tags (comma delimited)")); | |
| tagsField = new HistoryComboBox(); | |
| tagsField.setToolTipText(tr("Please enter tags about your trace.")); | |
| tagsField.getModel().prefs().load("directupload.tags.history"); | |
| JPanel p = new JPanel(new GridBagLayout()); | |
| outputDisplay.setMaxWidth(findMaxDialogSize().width-10); | |
| p.add(outputDisplay, GBC.eol()); | |
| p.add(tagsLabel, GBC.eol().insets(0,10,0,0)); | |
| p.add(tagsField, GBC.eol().fill(GBC.HORIZONTAL)); | |
| p.add(descriptionLabel, GBC.eol().insets(0,10,0,0)); | |
| p.add(descriptionField, GBC.eol().fill(GBC.HORIZONTAL)); | |
| p.add(visibilityLabel, GBC.std().insets(0,10,0,0)); | |
| p.add(visiUrl, GBC.eol().insets(5,10,0,0)); | |
| p.add(visibilityCombo, GBC.eol()); | |
| return p; | |
| } | |
| private void initTitleAndDescriptionFromGpxData(GpxData gpxData) { | |
| String description = "", title = " ", tags = ""; | |
| // non-breaking space in title fixes #10275 | |
| if (gpxData != null) { | |
| IGpxTrack firstTrack = gpxData.tracks.isEmpty() ? null : gpxData.tracks.iterator().next(); | |
| Object meta_desc = gpxData.attr.get(GpxConstants.META_DESC); | |
| if (meta_desc != null) { | |
| description = meta_desc.toString(); | |
| } else if (firstTrack != null && gpxData.tracks.size() == 1 && firstTrack.get("desc") != null) { | |
| description = firstTrack.getString("desc"); | |
| } else if (gpxData.storageFile != null) { | |
| description = gpxData.storageFile.getName().replaceAll("[&?/\\\\]"," ").replaceAll("(\\.[^.]*)$",""); | |
| } | |
| if (gpxData.storageFile != null) { | |
| title = tr("Selected track: {0}", gpxData.storageFile.getName()); | |
| } | |
| Object meta_tags = gpxData.attr.get(GpxConstants.META_KEYWORDS); | |
| if (meta_tags != null) { | |
| tags = meta_tags.toString(); | |
| } | |
| } | |
| else { | |
| description = new SimpleDateFormat("yyMMddHHmmss").format(new Date()); | |
| title = tr("No GPX layer selected. Cannot upload a trace."); | |
| } | |
| outputDisplay.setText(title); | |
| descriptionField.setText(description); | |
| tagsField.setText(tags); | |
| } | |
| /** | |
| * This is the actual workhorse that manages the upload. | |
| * @param description Description of the GPX track being uploaded | |
| * @param tags Tags associated with the GPX track being uploaded | |
| * @param visi Shall the GPX track be public | |
| * @param gpxData The GPX Data to upload | |
| * @param progressMonitor Progress monitor | |
| * @throws IOException if any I/O error occurs | |
| */ | |
| private void upload(String description, String tags, String visi, GpxData gpxData, ProgressMonitor progressMonitor) throws IOException { | |
| progressMonitor.beginTask(tr("Uploading trace ...")); | |
| try { | |
| if (checkForErrors(description, gpxData)) { | |
| return; | |
| } | |
| // Clean description/tags from disallowed chars | |
| description = description.replaceAll("[&?/\\\\]", " "); | |
| tags = tags.replaceAll("[&?/\\\\.;]", " "); | |
| // Generate data for upload | |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
| writeGpxFile(baos, "file", gpxData); | |
| writeField(baos, "description", description); | |
| writeField(baos, "tags", (tags != null && tags.length() > 0) ? tags : ""); | |
| writeField(baos, "visibility", visi); | |
| writeString(baos, "--" + BOUNDARY + "--" + LINE_END); | |
| HttpClient conn = setupConnection(baos.size()); | |
| // FIXME previous method allowed to see real % progress (each 10 Kb of data) | |
| //flushToServer(bais, conn.getOutputStream(), progressMonitor); | |
| Response response = conn.setRequestBody(baos.toByteArray()).connect(progressMonitor.createSubTaskMonitor(1, false)); | |
| if (canceled) { | |
| response.disconnect(); | |
| GuiHelper.runInEDT(new Runnable() { | |
| @Override public void run() { | |
| outputDisplay.setText(tr("Upload canceled")); | |
| buttons.get(0).setEnabled(true); | |
| } | |
| }); | |
| canceled = false; | |
| } | |
| else { | |
| final boolean success = finishUpConnection(response); | |
| GuiHelper.runInEDT(new Runnable() { | |
| @Override public void run() { | |
| buttons.get(0).setEnabled(!success); | |
| if (success) { | |
| buttons.get(1).setText(tr("Close")); | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| catch (Exception e) { | |
| GuiHelper.runInEDT(new Runnable() { | |
| @Override public void run() { | |
| outputDisplay.setText(tr("Error while uploading")); | |
| } | |
| }); | |
| e.printStackTrace(); | |
| } | |
| finally { | |
| progressMonitor.finishTask(); | |
| } | |
| } | |
| /** | |
| * This function sets up the upload URL and logs in using the username and password given | |
| * in the preferences. | |
| * @param contentLength The length of the content to be sent to the server | |
| * @return HttpURLConnection The set up conenction | |
| * @throws MalformedURLException in case of invalid URL | |
| * @throws OsmTransferException if auth header cannot be added | |
| */ | |
| private HttpClient setupConnection(int contentLength) throws MalformedURLException, OsmTransferException { | |
| // Upload URL | |
| URL url = new URL(OsmApi.getOsmApi().getBaseUrl() + "gpx/create"); | |
| // Set up connection and log in | |
| HttpClient c = HttpClient.create(url, "POST").setConnectTimeout(15000); | |
| // unfortunately, addAuth() is protected, so we need to subclass OsmConnection | |
| // XXX make addAuth public. | |
| UploadOsmConnection.getInstance().addAuthHack(c); | |
| c.setHeader("Content-Type", "multipart/form-data; boundary=" + BOUNDARY); | |
| c.setHeader("Connection", "close"); // counterpart of keep-alive | |
| c.setHeader("Expect", ""); | |
| return c; | |
| } | |
| /** | |
| * This function checks if the given connection finished up fine, closes it and returns the result. | |
| * It also posts the result (or errors) to OutputDisplay. | |
| * @param c The HTTP connection to check/finish up | |
| * @return {@code true} for success | |
| */ | |
| private boolean finishUpConnection(Response c) { | |
| String returnMsg = c.getResponseMessage(); | |
| final boolean success = returnMsg.equals("OK"); | |
| if (c.getResponseCode() != 200) { | |
| if (c.getHeaderField("Error") != null) | |
| returnMsg += "\n" + c.getHeaderField("Error"); | |
| } | |
| final String returnMsgEDT = returnMsg; | |
| GuiHelper.runInEDT(new Runnable() { | |
| @Override public void run() { | |
| outputDisplay.setText(success | |
| ? tr("GPX upload was successful") | |
| : tr("Upload failed. Server returned the following message: ") + returnMsgEDT); | |
| } | |
| }); | |
| c.disconnect(); | |
| return success; | |
| } | |
| /** | |
| * Uploads a given InputStream to a given OutputStream and sets progress | |
| * @param InputSteam | |
| * @param OutputStream | |
| */ | |
| /* private void flushToServer(InputStream in, OutputStream out, ProgressMonitor progressMonitor) throws Exception { | |
| // Upload in 10 kB chunks | |
| byte[] buffer = new byte[10000]; | |
| int nread; | |
| int cur = 0; | |
| synchronized (in) { | |
| while ((nread = in.read(buffer, 0, buffer.length)) >= 0) { | |
| out.write(buffer, 0, nread); | |
| cur += nread; | |
| out.flush(); | |
| progressMonitor.worked(nread); | |
| progressMonitor.subTask(getProgressText(cur, progressMonitor)); | |
| if(canceled) | |
| break; | |
| } | |
| } | |
| if(!canceled) | |
| out.flush(); | |
| progressMonitor.subTask("Waiting for server reply..."); | |
| buffer = null; | |
| } | |
| */ | |
| /** | |
| * Generates the output string displayed in the PleaseWaitDialog. | |
| * @param cur Bytes already uploaded | |
| * @param progressMonitor Progress monitor | |
| * @return Message | |
| */ | |
| private String getProgressText(int cur, ProgressMonitor progressMonitor) { | |
| int max = progressMonitor.getTicksCount(); | |
| int percent = (cur * 100 / max); | |
| // FIXME method kept because of translated string | |
| return tr("Uploading GPX track: {0}% ({1} of {2})", | |
| percent, formatBytes(cur), formatBytes(max)); | |
| } | |
| /** | |
| * Nicely calculates given bytes into MB, kB and B (with units) | |
| * @param bytes Bytes | |
| * @return String | |
| */ | |
| private String formatBytes(int bytes) { | |
| return (bytes > 1000 * 1000 | |
| // Rounds to 2 decimal places | |
| ? new DecimalFormat("0.00") | |
| .format((double)(bytes/(1000*10))/100) + " MB" | |
| : (bytes > 1000 | |
| ? (bytes/1000) + " kB" | |
| : bytes + " B")); | |
| } | |
| /** | |
| * Checks for common errors and displays them in OutputDisplay if it finds any. | |
| * Returns whether errors have been found or not. | |
| * @param description GPX track description | |
| * @param gpxData the GPX data to upload | |
| * @return boolean true if errors have been found | |
| */ | |
| private boolean checkForErrors(String description, GpxData gpxData) { | |
| String errors=""; | |
| if(description == null || description.length() == 0) | |
| errors += tr("No description provided. Please provide some description."); | |
| if(gpxData == null) | |
| errors += tr("No GPX layer selected. Cannot upload a trace."); | |
| final String errorsEDT = errors; | |
| GuiHelper.runInEDT(new Runnable() { | |
| @Override public void run() { | |
| outputDisplay.setText(errorsEDT); | |
| } | |
| }); | |
| return errors.length() > 0; | |
| } | |
| /** | |
| * This creates the uploadTask that does the actual work and hands it to the main.worker to be executed. | |
| */ | |
| private void setupUpload() { | |
| final GpxData gpxData = UploadOsmConnection.getInstance().autoSelectTrace(); | |
| if (gpxData == null) { | |
| return; | |
| } | |
| // Disable Upload button so users can't just upload that track again | |
| buttons.get(0).setEnabled(false); | |
| // save history | |
| Config.getPref().put("directupload.visibility.last-used", visibility.desc2visi(visibilityCombo.getSelectedItem().toString()).name()); | |
| descriptionField.addCurrentItemToHistory(); | |
| descriptionField.getModel().prefs().save("directupload.description.history"); | |
| tagsField.addCurrentItemToHistory(); | |
| tagsField.getModel().prefs().save("directupload.tags.history"); | |
| PleaseWaitRunnable uploadTask = new PleaseWaitRunnable(tr("Uploading GPX Track")){ | |
| @Override protected void realRun() throws IOException { | |
| upload(descriptionField.getText(), | |
| tagsField.getText(), | |
| visibility.desc2visi(visibilityCombo.getSelectedItem()).toString(), | |
| gpxData, | |
| progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false) | |
| ); | |
| } | |
| @Override protected void finish() {} | |
| @Override protected void cancel() { | |
| canceled = true; | |
| } | |
| }; | |
| MainApplication.worker.execute(uploadTask); | |
| } | |
| /** | |
| * Writes textfields (like in webbrowser) to the given ByteArrayOutputStream | |
| * @param baos output stream | |
| * @param name The name of the "textbox" | |
| * @param value The value to write | |
| * @throws IOException if any I/O error occurs | |
| */ | |
| private void writeField(ByteArrayOutputStream baos, String name, String value) throws IOException { | |
| writeBoundary(baos); | |
| writeString(baos, "Content-Disposition: form-data; name=\"" + name + "\""); | |
| writeLineEnd(baos); | |
| writeLineEnd(baos); | |
| baos.write(value.getBytes("UTF-8")); | |
| writeLineEnd(baos); | |
| } | |
| /** | |
| * Writes gpxData (= file field in webbrowser) to the given ByteArrayOutputStream | |
| * @param baos output stream | |
| * @param name The name of the "upload field" | |
| * @param gpxData The GPX data to upload | |
| * @throws IOException if any I/O error occurs | |
| */ | |
| private void writeGpxFile(ByteArrayOutputStream baos, String name, GpxData gpxData) throws IOException { | |
| String filename; | |
| writeBoundary(baos); | |
| writeString(baos, "Content-Disposition: form-data; name=\"" + name + "\"; "); | |
| if (gpxData.storageFile != null) | |
| filename = gpxData.storageFile.getName(); | |
| else | |
| filename = "not saved"; | |
| writeString(baos, "filename=\"" + filename + "\""); | |
| writeLineEnd(baos); | |
| writeString(baos, "Content-Type: application/octet-stream"); | |
| writeLineEnd(baos); | |
| writeLineEnd(baos); | |
| new GpxWriter(baos).write(gpxData); | |
| writeLineEnd(baos); | |
| } | |
| /** | |
| * Writes a String to the given ByteArrayOutputStream | |
| * @param baos output stream | |
| * @param s string | |
| */ | |
| private void writeString(ByteArrayOutputStream baos, String s) { | |
| try { | |
| baos.write(s.getBytes(StandardCharsets.UTF_8)); | |
| } catch(Exception e) { | |
| Logging.error(e); | |
| } | |
| } | |
| /** | |
| * Writes a newline to the given ByteArrayOutputStream | |
| * @param baos output stream | |
| */ | |
| private void writeLineEnd(ByteArrayOutputStream baos) { | |
| writeString(baos, LINE_END); | |
| } | |
| /** | |
| * Writes a boundary line to the given ByteArrayOutputStream | |
| * @param baos output stream | |
| */ | |
| private void writeBoundary(ByteArrayOutputStream baos) { | |
| writeString(baos, "--" + BOUNDARY); | |
| writeLineEnd(baos); | |
| } | |
| /** | |
| * Overrides the default actions. Will not close the window when upload trace is clicked | |
| */ | |
| @Override protected void buttonAction(int buttonIndex, ActionEvent evt) { | |
| String a = evt.getActionCommand(); | |
| if(uploadTraceText.equals(a)) | |
| setupUpload(); | |
| else | |
| setVisible(false); | |
| } | |
| } |