001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.actions; 003 004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005 import static org.openstreetmap.josm.tools.I18n.tr; 006 007 import java.awt.event.ActionEvent; 008 import java.awt.event.KeyEvent; 009 import java.util.LinkedList; 010 011 import javax.swing.JOptionPane; 012 import javax.swing.SwingUtilities; 013 014 import org.openstreetmap.josm.Main; 015 import org.openstreetmap.josm.actions.upload.ApiPreconditionCheckerHook; 016 import org.openstreetmap.josm.actions.upload.RelationUploadOrderHook; 017 import org.openstreetmap.josm.actions.upload.UploadHook; 018 import org.openstreetmap.josm.actions.upload.ValidateUploadHook; 019 import org.openstreetmap.josm.data.APIDataSet; 020 import org.openstreetmap.josm.data.conflict.ConflictCollection; 021 import org.openstreetmap.josm.gui.HelpAwareOptionPane; 022 import org.openstreetmap.josm.gui.help.HelpUtil; 023 import org.openstreetmap.josm.gui.io.UploadDialog; 024 import org.openstreetmap.josm.gui.io.UploadPrimitivesTask; 025 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 026 import org.openstreetmap.josm.gui.util.GuiHelper; 027 import org.openstreetmap.josm.tools.ImageProvider; 028 import org.openstreetmap.josm.tools.Shortcut; 029 030 /** 031 * Action that opens a connection to the osm server and uploads all changes. 032 * 033 * An dialog is displayed asking the user to specify a rectangle to grab. 034 * The url and account settings from the preferences are used. 035 * 036 * If the upload fails this action offers various options to resolve conflicts. 037 * 038 * @author imi 039 */ 040 public class UploadAction extends JosmAction{ 041 /** 042 * The list of upload hooks. These hooks will be called one after the other 043 * when the user wants to upload data. Plugins can insert their own hooks here 044 * if they want to be able to veto an upload. 045 * 046 * Be default, the standard upload dialog is the only element in the list. 047 * Plugins should normally insert their code before that, so that the upload 048 * dialog is the last thing shown before upload really starts; on occasion 049 * however, a plugin might also want to insert something after that. 050 */ 051 private static final LinkedList<UploadHook> uploadHooks = new LinkedList<UploadHook>(); 052 static { 053 uploadHooks.add(new ValidateUploadHook()); 054 /** 055 * Checks server capabilities before upload. 056 */ 057 uploadHooks.add(new ApiPreconditionCheckerHook()); 058 059 /** 060 * Adjusts the upload order of new relations 061 */ 062 uploadHooks.add(new RelationUploadOrderHook()); 063 } 064 065 /** 066 * Registers an upload hook. Adds the hook at the first position of the upload hooks. 067 * 068 * @param hook the upload hook. Ignored if null. 069 */ 070 public static void registerUploadHook(UploadHook hook) { 071 if(hook == null) return; 072 if (!uploadHooks.contains(hook)) { 073 uploadHooks.add(0,hook); 074 } 075 } 076 077 /** 078 * Unregisters an upload hook. Removes the hook from the list of upload hooks. 079 * 080 * @param hook the upload hook. Ignored if null. 081 */ 082 public static void unregisterUploadHook(UploadHook hook) { 083 if(hook == null) return; 084 if (uploadHooks.contains(hook)) { 085 uploadHooks.remove(hook); 086 } 087 } 088 089 public UploadAction() { 090 super(tr("Upload data"), "upload", tr("Upload all changes in the active data layer to the OSM server"), 091 Shortcut.registerShortcut("file:upload", tr("File: {0}", tr("Upload data")), KeyEvent.VK_UP, Shortcut.CTRL_SHIFT), true); 092 putValue("help", ht("/Action/Upload")); 093 } 094 095 /** 096 * Refreshes the enabled state 097 * 098 */ 099 @Override 100 protected void updateEnabledState() { 101 setEnabled(getEditLayer() != null); 102 } 103 104 public boolean checkPreUploadConditions(OsmDataLayer layer) { 105 return checkPreUploadConditions(layer, new APIDataSet(layer.data)); 106 } 107 108 protected static void alertUnresolvedConflicts(OsmDataLayer layer) { 109 HelpAwareOptionPane.showOptionDialog( 110 Main.parent, 111 tr("<html>The data to be uploaded participates in unresolved conflicts of layer ''{0}''.<br>" 112 + "You have to resolve them first.</html>", layer.getName() 113 ), 114 tr("Warning"), 115 JOptionPane.WARNING_MESSAGE, 116 HelpUtil.ht("/Action/Upload#PrimitivesParticipateInConflicts") 117 ); 118 } 119 120 /** 121 * returns true if the user wants to cancel, false if they 122 * want to continue 123 */ 124 public static final boolean warnUploadDiscouraged(OsmDataLayer layer) { 125 return GuiHelper.warnUser(tr("Upload discouraged"), 126 "<html>" + 127 tr("You are about to upload data from the layer ''{0}''.<br /><br />"+ 128 "Sending data from this layer is <b>strongly discouraged</b>. If you continue,<br />"+ 129 "it may require you subsequently have to revert your changes, or force other contributors to.<br /><br />"+ 130 "Are you sure you want to continue?", layer.getName())+ 131 "</html>", 132 ImageProvider.get("upload"), tr("Ignore this hint and upload anyway")); 133 } 134 135 /** 136 * Check whether the preconditions are met to upload data in <code>apiData</code>. 137 * Makes sure upload is allowed, primitives in <code>apiData</code> don't participate in conflicts and 138 * runs the installed {@link UploadHook}s. 139 * 140 * @param layer the source layer of the data to be uploaded 141 * @param apiData the data to be uploaded 142 * @return true, if the preconditions are met; false, otherwise 143 */ 144 public boolean checkPreUploadConditions(OsmDataLayer layer, APIDataSet apiData) { 145 if (layer.isUploadDiscouraged()) { 146 if (warnUploadDiscouraged(layer)) { 147 return false; 148 } 149 } 150 ConflictCollection conflicts = layer.getConflicts(); 151 if (apiData.participatesInConflict(conflicts)) { 152 alertUnresolvedConflicts(layer); 153 return false; 154 } 155 // Call all upload hooks in sequence. 156 // FIXME: this should become an asynchronous task 157 // 158 for (UploadHook hook : uploadHooks) { 159 if (!hook.checkUpload(apiData)) 160 return false; 161 } 162 163 return true; 164 } 165 166 /** 167 * Uploads data to the OSM API. 168 * 169 * @param layer the source layer for the data to upload 170 * @param apiData the primitives to be added, updated, or deleted 171 */ 172 public void uploadData(final OsmDataLayer layer, APIDataSet apiData) { 173 if (apiData.isEmpty()) { 174 JOptionPane.showMessageDialog( 175 Main.parent, 176 tr("No changes to upload."), 177 tr("Warning"), 178 JOptionPane.INFORMATION_MESSAGE 179 ); 180 return; 181 } 182 if (!checkPreUploadConditions(layer, apiData)) 183 return; 184 185 final UploadDialog dialog = UploadDialog.getUploadDialog(); 186 // If we simply set the changeset comment here, it would be 187 // overridden by subsequent events in EDT that are caused by 188 // dialog creation. The current solution is to queue this operation 189 // after these events. 190 // TODO: find better way to initialize the comment field 191 SwingUtilities.invokeLater(new Runnable() { 192 public void run() { 193 dialog.setDefaultChangesetTags(layer.data.getChangeSetTags()); 194 } 195 }); 196 dialog.setUploadedPrimitives(apiData); 197 dialog.setVisible(true); 198 if (dialog.isCanceled()) 199 return; 200 dialog.rememberUserInput(); 201 202 Main.worker.execute( 203 new UploadPrimitivesTask( 204 UploadDialog.getUploadDialog().getUploadStrategySpecification(), 205 layer, 206 apiData, 207 UploadDialog.getUploadDialog().getChangeset() 208 ) 209 ); 210 } 211 212 public void actionPerformed(ActionEvent e) { 213 if (!isEnabled()) 214 return; 215 if (Main.map == null) { 216 JOptionPane.showMessageDialog( 217 Main.parent, 218 tr("Nothing to upload. Get some data first."), 219 tr("Warning"), 220 JOptionPane.WARNING_MESSAGE 221 ); 222 return; 223 } 224 APIDataSet apiData = new APIDataSet(Main.main.getCurrentDataSet()); 225 uploadData(Main.map.mapView.getEditLayer(), apiData); 226 } 227 }