[6380] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[626] | 2 | package org.openstreetmap.josm.actions;
|
---|
| 3 |
|
---|
[3384] | 4 | import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
|
---|
[626] | 5 | import static org.openstreetmap.josm.tools.I18n.tr;
|
---|
[2008] | 6 | import static org.openstreetmap.josm.tools.I18n.trn;
|
---|
[626] | 7 |
|
---|
| 8 | import java.awt.event.ActionEvent;
|
---|
| 9 | import java.awt.event.KeyEvent;
|
---|
[2008] | 10 | import java.util.ArrayList;
|
---|
[626] | 11 | import java.util.Collection;
|
---|
| 12 | import java.util.HashMap;
|
---|
[2008] | 13 | import java.util.List;
|
---|
[626] | 14 | import java.util.Map;
|
---|
[6258] | 15 | import java.util.Map.Entry;
|
---|
[626] | 16 |
|
---|
| 17 | import org.openstreetmap.josm.Main;
|
---|
| 18 | import org.openstreetmap.josm.command.ChangePropertyCommand;
|
---|
| 19 | import org.openstreetmap.josm.command.Command;
|
---|
| 20 | import org.openstreetmap.josm.command.SequenceCommand;
|
---|
| 21 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
---|
[2008] | 22 | import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
|
---|
[2305] | 23 | import org.openstreetmap.josm.data.osm.PrimitiveData;
|
---|
[3640] | 24 | import org.openstreetmap.josm.data.osm.Tag;
|
---|
[2008] | 25 | import org.openstreetmap.josm.data.osm.TagCollection;
|
---|
| 26 | import org.openstreetmap.josm.gui.conflict.tags.PasteTagsConflictResolverDialog;
|
---|
[1084] | 27 | import org.openstreetmap.josm.tools.Shortcut;
|
---|
[5738] | 28 | import org.openstreetmap.josm.tools.TextTagParser;
|
---|
| 29 | import org.openstreetmap.josm.tools.Utils;
|
---|
[626] | 30 |
|
---|
[5275] | 31 | /**
|
---|
| 32 | * Action, to paste all tags from one primitive to another.
|
---|
| 33 | *
|
---|
| 34 | * It will take the primitive from the copy-paste buffer an apply all its tags
|
---|
| 35 | * to the selected primitive(s).
|
---|
| 36 | *
|
---|
| 37 | * @author David Earl
|
---|
| 38 | */
|
---|
[5738] | 39 | public final class PasteTagsAction extends JosmAction {
|
---|
[626] | 40 |
|
---|
[5809] | 41 | private static final String help = ht("/Action/PasteTags");
|
---|
[6069] | 42 |
|
---|
[6258] | 43 | /**
|
---|
| 44 | * Constructs a new {@code PasteTagsAction}.
|
---|
| 45 | */
|
---|
[3385] | 46 | public PasteTagsAction() {
|
---|
[1169] | 47 | super(tr("Paste Tags"), "pastetags",
|
---|
[1814] | 48 | tr("Apply tags of contents of paste buffer to all selected items."),
|
---|
[4943] | 49 | Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")),
|
---|
[4982] | 50 | KeyEvent.VK_V, Shortcut.CTRL_SHIFT), true);
|
---|
[5809] | 51 | putValue("help", help);
|
---|
[1169] | 52 | }
|
---|
[626] | 53 |
|
---|
[2305] | 54 | public static class TagPaster {
|
---|
[2070] | 55 |
|
---|
[2305] | 56 | private final Collection<PrimitiveData> source;
|
---|
| 57 | private final Collection<OsmPrimitive> target;
|
---|
[3640] | 58 | private final List<Tag> commands = new ArrayList<Tag>();
|
---|
[1514] | 59 |
|
---|
[2305] | 60 | public TagPaster(Collection<PrimitiveData> source, Collection<OsmPrimitive> target) {
|
---|
| 61 | this.source = source;
|
---|
| 62 | this.target = target;
|
---|
| 63 | }
|
---|
[2008] | 64 |
|
---|
[2305] | 65 | /**
|
---|
| 66 | * Replies true if the source for tag pasting is heterogeneous, i.e. if it doesn't consist of
|
---|
[5266] | 67 | * {@link OsmPrimitive}s of exactly one type
|
---|
[2305] | 68 | */
|
---|
| 69 | protected boolean isHeteogeneousSource() {
|
---|
| 70 | int count = 0;
|
---|
| 71 | count = !getSourcePrimitivesByType(OsmPrimitiveType.NODE).isEmpty() ? count + 1 : count;
|
---|
| 72 | count = !getSourcePrimitivesByType(OsmPrimitiveType.WAY).isEmpty() ? count + 1 : count;
|
---|
| 73 | count = !getSourcePrimitivesByType(OsmPrimitiveType.RELATION).isEmpty() ? count + 1 : count;
|
---|
| 74 | return count > 1;
|
---|
| 75 | }
|
---|
[2008] | 76 |
|
---|
[2305] | 77 | /**
|
---|
| 78 | * Replies all primitives of type <code>type</code> in the current selection.
|
---|
| 79 | *
|
---|
| 80 | * @param <T>
|
---|
| 81 | * @param type the type
|
---|
| 82 | * @return all primitives of type <code>type</code> in the current selection.
|
---|
| 83 | */
|
---|
| 84 | protected <T extends PrimitiveData> Collection<? extends PrimitiveData> getSourcePrimitivesByType(OsmPrimitiveType type) {
|
---|
| 85 | return PrimitiveData.getFilteredList(source, type);
|
---|
| 86 | }
|
---|
[2008] | 87 |
|
---|
[2305] | 88 | /**
|
---|
| 89 | * Replies the collection of tags for all primitives of type <code>type</code> in the current
|
---|
| 90 | * selection
|
---|
| 91 | *
|
---|
| 92 | * @param <T>
|
---|
| 93 | * @param type the type
|
---|
| 94 | * @return the collection of tags for all primitives of type <code>type</code> in the current
|
---|
| 95 | * selection
|
---|
| 96 | */
|
---|
| 97 | protected <T extends OsmPrimitive> TagCollection getSourceTagsByType(OsmPrimitiveType type) {
|
---|
| 98 | return TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type));
|
---|
[2008] | 99 | }
|
---|
[2305] | 100 |
|
---|
| 101 | /**
|
---|
| 102 | * Replies true if there is at least one tag in the current selection for primitives of
|
---|
| 103 | * type <code>type</code>
|
---|
| 104 | *
|
---|
| 105 | * @param <T>
|
---|
| 106 | * @param type the type
|
---|
| 107 | * @return true if there is at least one tag in the current selection for primitives of
|
---|
| 108 | * type <code>type</code>
|
---|
| 109 | */
|
---|
| 110 | protected <T extends OsmPrimitive> boolean hasSourceTagsByType(OsmPrimitiveType type) {
|
---|
| 111 | return ! getSourceTagsByType(type).isEmpty();
|
---|
[2008] | 112 | }
|
---|
| 113 |
|
---|
[3640] | 114 | protected void buildChangeCommand(Collection<? extends OsmPrimitive> selection, TagCollection tc) {
|
---|
[2305] | 115 | for (String key : tc.getKeys()) {
|
---|
[3640] | 116 | commands.add(new Tag(key, tc.getValues(key).iterator().next()));
|
---|
[1169] | 117 | }
|
---|
| 118 | }
|
---|
[750] | 119 |
|
---|
[2305] | 120 | protected Map<OsmPrimitiveType, Integer> getSourceStatistics() {
|
---|
| 121 | HashMap<OsmPrimitiveType, Integer> ret = new HashMap<OsmPrimitiveType, Integer>();
|
---|
[4387] | 122 | for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
|
---|
[2305] | 123 | if (!getSourceTagsByType(type).isEmpty()) {
|
---|
| 124 | ret.put(type, getSourcePrimitivesByType(type).size());
|
---|
| 125 | }
|
---|
[2008] | 126 | }
|
---|
[2305] | 127 | return ret;
|
---|
[1814] | 128 | }
|
---|
[1514] | 129 |
|
---|
[2305] | 130 | protected Map<OsmPrimitiveType, Integer> getTargetStatistics() {
|
---|
| 131 | HashMap<OsmPrimitiveType, Integer> ret = new HashMap<OsmPrimitiveType, Integer>();
|
---|
[4387] | 132 | for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
|
---|
[2305] | 133 | int count = OsmPrimitive.getFilteredList(target, type.getOsmClass()).size();
|
---|
| 134 | if (count > 0) {
|
---|
| 135 | ret.put(type, count);
|
---|
| 136 | }
|
---|
[2008] | 137 | }
|
---|
[2305] | 138 | return ret;
|
---|
[2008] | 139 | }
|
---|
[1514] | 140 |
|
---|
[2305] | 141 | /**
|
---|
[5275] | 142 | * Pastes the tags from a homogeneous source (the {@link Main#pasteBuffer}s selection consisting
|
---|
| 143 | * of one type of {@link OsmPrimitive}s only).
|
---|
[2305] | 144 | *
|
---|
| 145 | * Tags from a homogeneous source can be pasted to a heterogeneous target. All target primitives,
|
---|
| 146 | * regardless of their type, receive the same tags.
|
---|
| 147 | */
|
---|
| 148 | protected void pasteFromHomogeneousSource() {
|
---|
| 149 | TagCollection tc = null;
|
---|
[4387] | 150 | for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
|
---|
[2305] | 151 | TagCollection tc1 = getSourceTagsByType(type);
|
---|
| 152 | if (!tc1.isEmpty()) {
|
---|
| 153 | tc = tc1;
|
---|
| 154 | }
|
---|
| 155 | }
|
---|
| 156 | if (tc == null)
|
---|
| 157 | // no tags found to paste. Abort.
|
---|
[2008] | 158 | return;
|
---|
[2305] | 159 |
|
---|
| 160 | if (!tc.isApplicableToPrimitive()) {
|
---|
| 161 | PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent);
|
---|
| 162 | dialog.populate(tc, getSourceStatistics(), getTargetStatistics());
|
---|
| 163 | dialog.setVisible(true);
|
---|
| 164 | if (dialog.isCanceled())
|
---|
| 165 | return;
|
---|
[3640] | 166 | buildChangeCommand(target, dialog.getResolution());
|
---|
[2305] | 167 | } else {
|
---|
| 168 | // no conflicts in the source tags to resolve. Just apply the tags
|
---|
| 169 | // to the target primitives
|
---|
| 170 | //
|
---|
[3640] | 171 | buildChangeCommand(target, tc);
|
---|
[2305] | 172 | }
|
---|
[1514] | 173 | }
|
---|
[750] | 174 |
|
---|
[2305] | 175 | /**
|
---|
[6069] | 176 | * Replies true if there is at least one primitive of type <code>type</code>
|
---|
[5275] | 177 | * is in the target collection
|
---|
[2305] | 178 | *
|
---|
| 179 | * @param <T>
|
---|
| 180 | * @param type the type to look for
|
---|
| 181 | * @return true if there is at least one primitive of type <code>type</code> in the collection
|
---|
| 182 | * <code>selection</code>
|
---|
| 183 | */
|
---|
| 184 | protected <T extends OsmPrimitive> boolean hasTargetPrimitives(Class<T> type) {
|
---|
| 185 | return !OsmPrimitive.getFilteredList(target, type).isEmpty();
|
---|
| 186 | }
|
---|
[2008] | 187 |
|
---|
[2305] | 188 | /**
|
---|
| 189 | * Replies true if this a heterogeneous source can be pasted without conflict to targets
|
---|
| 190 | *
|
---|
| 191 | * @param targets the collection of target primitives
|
---|
| 192 | * @return true if this a heterogeneous source can be pasted without conflicts to targets
|
---|
| 193 | */
|
---|
| 194 | protected boolean canPasteFromHeterogeneousSourceWithoutConflict(Collection<OsmPrimitive> targets) {
|
---|
[4387] | 195 | for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
|
---|
[2305] | 196 | if (hasTargetPrimitives(type.getOsmClass())) {
|
---|
| 197 | TagCollection tc = TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type));
|
---|
| 198 | if (!tc.isEmpty() && ! tc.isApplicableToPrimitive())
|
---|
| 199 | return false;
|
---|
| 200 | }
|
---|
| 201 | }
|
---|
| 202 | return true;
|
---|
[1169] | 203 | }
|
---|
[2305] | 204 |
|
---|
| 205 | /**
|
---|
| 206 | * Pastes the tags in the current selection of the paste buffer to a set of target
|
---|
| 207 | * primitives.
|
---|
| 208 | */
|
---|
| 209 | protected void pasteFromHeterogeneousSource() {
|
---|
| 210 | if (canPasteFromHeterogeneousSourceWithoutConflict(target)) {
|
---|
[4387] | 211 | for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
|
---|
[2305] | 212 | if (hasSourceTagsByType(type) && hasTargetPrimitives(type.getOsmClass())) {
|
---|
[3640] | 213 | buildChangeCommand(target, getSourceTagsByType(type));
|
---|
[2305] | 214 | }
|
---|
| 215 | }
|
---|
| 216 | } else {
|
---|
| 217 | PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent);
|
---|
| 218 | dialog.populate(
|
---|
| 219 | getSourceTagsByType(OsmPrimitiveType.NODE),
|
---|
| 220 | getSourceTagsByType(OsmPrimitiveType.WAY),
|
---|
| 221 | getSourceTagsByType(OsmPrimitiveType.RELATION),
|
---|
| 222 | getSourceStatistics(),
|
---|
| 223 | getTargetStatistics()
|
---|
| 224 | );
|
---|
| 225 | dialog.setVisible(true);
|
---|
| 226 | if (dialog.isCanceled())
|
---|
| 227 | return;
|
---|
[4387] | 228 | for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
|
---|
[2305] | 229 | if (hasSourceTagsByType(type) && hasTargetPrimitives(type.getOsmClass())) {
|
---|
[3640] | 230 | buildChangeCommand(OsmPrimitive.getFilteredList(target, type.getOsmClass()), dialog.getResolution(type));
|
---|
[2305] | 231 | }
|
---|
| 232 | }
|
---|
| 233 | }
|
---|
[2008] | 234 | }
|
---|
[750] | 235 |
|
---|
[3640] | 236 | public List<Tag> execute() {
|
---|
[2305] | 237 | commands.clear();
|
---|
| 238 | if (isHeteogeneousSource()) {
|
---|
| 239 | pasteFromHeterogeneousSource();
|
---|
| 240 | } else {
|
---|
| 241 | pasteFromHomogeneousSource();
|
---|
[2008] | 242 | }
|
---|
[2305] | 243 | return commands;
|
---|
[2008] | 244 | }
|
---|
[2305] | 245 |
|
---|
[2008] | 246 | }
|
---|
| 247 |
|
---|
[5890] | 248 | @Override
|
---|
[2008] | 249 | public void actionPerformed(ActionEvent e) {
|
---|
[3640] | 250 | Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
|
---|
| 251 |
|
---|
| 252 | if (selection.isEmpty())
|
---|
[1847] | 253 | return;
|
---|
[6069] | 254 |
|
---|
[5738] | 255 | String buf = Utils.getClipboardContent();
|
---|
[6321] | 256 | if (buf == null || buf.isEmpty() || buf.matches(CopyAction.CLIPBOARD_REGEXP)) {
|
---|
[5809] | 257 | pasteTagsFromJOSMBuffer(selection);
|
---|
[6069] | 258 | } else {
|
---|
[5809] | 259 | // Paste tags from arbitrary text
|
---|
| 260 | pasteTagsFromText(selection, buf);
|
---|
| 261 | }
|
---|
| 262 | }
|
---|
[3640] | 263 |
|
---|
[5890] | 264 | /** Paste tags from arbitrary text, not using JOSM buffer
|
---|
| 265 | * @return true if action was successful
|
---|
[5809] | 266 | */
|
---|
| 267 | public static boolean pasteTagsFromText(Collection<OsmPrimitive> selection, String text) {
|
---|
| 268 | Map<String, String> tags = TextTagParser.readTagsFromText(text);
|
---|
| 269 | if (tags==null || tags.isEmpty()) {
|
---|
[5915] | 270 | TextTagParser.showBadBufferMessage(help);
|
---|
[5809] | 271 | return false;
|
---|
[5890] | 272 | }
|
---|
[5809] | 273 | if (!TextTagParser.validateTags(tags)) return false;
|
---|
[6106] | 274 |
|
---|
| 275 | List<Command> commands = new ArrayList<Command>(tags.size());
|
---|
[6258] | 276 | for (Entry<String, String> entry: tags.entrySet()) {
|
---|
| 277 | String v = entry.getValue();
|
---|
| 278 | commands.add(new ChangePropertyCommand(selection, entry.getKey(), "".equals(v)?null:v));
|
---|
[1847] | 279 | }
|
---|
[5809] | 280 | commitCommands(selection, commands);
|
---|
| 281 | return !commands.isEmpty();
|
---|
| 282 | }
|
---|
[6069] | 283 |
|
---|
[5809] | 284 | /** Paste tags from JOSM buffer
|
---|
[5890] | 285 | * @param selection objects that will have the tags
|
---|
[6069] | 286 | * @return false if JOSM buffer was empty
|
---|
[5809] | 287 | */
|
---|
| 288 | public static boolean pasteTagsFromJOSMBuffer(Collection<OsmPrimitive> selection) {
|
---|
| 289 | List<PrimitiveData> directlyAdded = Main.pasteBuffer.getDirectlyAdded();
|
---|
| 290 | if (directlyAdded==null || directlyAdded.isEmpty()) return false;
|
---|
| 291 |
|
---|
| 292 | PasteTagsAction.TagPaster tagPaster = new PasteTagsAction.TagPaster(directlyAdded, selection);
|
---|
| 293 | List<Command> commands = new ArrayList<Command>();
|
---|
| 294 | for (Tag tag : tagPaster.execute()) {
|
---|
| 295 | commands.add(new ChangePropertyCommand(selection, tag.getKey(), "".equals(tag.getValue()) ? null : tag.getValue()));
|
---|
[5738] | 296 | }
|
---|
[5809] | 297 | commitCommands(selection, commands);
|
---|
| 298 | return true;
|
---|
| 299 | }
|
---|
| 300 |
|
---|
| 301 | /**
|
---|
| 302 | * Create and execute SequenceCommand with descriptive title
|
---|
[6069] | 303 | * @param commands
|
---|
[5809] | 304 | */
|
---|
| 305 | private static void commitCommands(Collection<OsmPrimitive> selection, List<Command> commands) {
|
---|
[3640] | 306 | if (!commands.isEmpty()) {
|
---|
| 307 | String title1 = trn("Pasting {0} tag", "Pasting {0} tags", commands.size(), commands.size());
|
---|
[3995] | 308 | String title2 = trn("to {0} object", "to {0} objects", selection.size(), selection.size());
|
---|
[3640] | 309 | Main.main.undoRedo.add(
|
---|
| 310 | new SequenceCommand(
|
---|
| 311 | title1 + " " + title2,
|
---|
| 312 | commands
|
---|
| 313 | ));
|
---|
| 314 | }
|
---|
[6069] | 315 | }
|
---|
| 316 |
|
---|
[1820] | 317 | @Override
|
---|
| 318 | protected void updateEnabledState() {
|
---|
[5738] | 319 | if (getCurrentDataSet() == null) {
|
---|
[1820] | 320 | setEnabled(false);
|
---|
| 321 | return;
|
---|
| 322 | }
|
---|
[5738] | 323 | // buffer listening slows down the program and is not very good for arbitrary text in buffer
|
---|
| 324 | setEnabled(!getCurrentDataSet().getSelected().isEmpty());
|
---|
[1169] | 325 | }
|
---|
[2256] | 326 |
|
---|
| 327 | @Override
|
---|
| 328 | protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
|
---|
[5738] | 329 | setEnabled(selection!= null && !selection.isEmpty());
|
---|
[2256] | 330 | }
|
---|
[626] | 331 | }
|
---|