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