Ticket #19199: 19199.threading.5.patch
| File 19199.threading.5.patch, 17.7 KB (added by , 5 years ago) |
|---|
-
src/org/openstreetmap/josm/actions/SimplifyWayAction.java
diff --git a/src/org/openstreetmap/josm/actions/SimplifyWayAction.java b/src/org/openstreetmap/josm/actions/SimplifyWayAction.java index de0c78790..e13f2b471 100644
a b import static org.openstreetmap.josm.tools.I18n.trn; 8 8 import java.awt.GridBagLayout; 9 9 import java.awt.event.ActionEvent; 10 10 import java.awt.event.KeyEvent; 11 import java.awt.event.WindowAdapter; 12 import java.awt.event.WindowEvent; 11 13 import java.util.ArrayList; 12 14 import java.util.Arrays; 13 15 import java.util.Collection; … … import java.util.LinkedList; 17 19 import java.util.List; 18 20 import java.util.Objects; 19 21 import java.util.Set; 22 import java.util.concurrent.ForkJoinTask; 23 import java.util.concurrent.atomic.AtomicBoolean; 24 import java.util.concurrent.atomic.AtomicReference; 20 25 import java.util.stream.Collectors; 26 import java.util.stream.Stream; 21 27 22 28 import javax.swing.BorderFactory; 23 29 import javax.swing.JCheckBox; … … import org.openstreetmap.josm.gui.HelpAwareOptionPane; 46 52 import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 47 53 import org.openstreetmap.josm.gui.MainApplication; 48 54 import org.openstreetmap.josm.gui.Notification; 55 import org.openstreetmap.josm.gui.util.GuiHelper; 49 56 import org.openstreetmap.josm.spi.preferences.Config; 50 57 import org.openstreetmap.josm.spi.preferences.IPreferences; 51 58 import org.openstreetmap.josm.tools.GBC; 52 59 import org.openstreetmap.josm.tools.ImageProvider; 60 import org.openstreetmap.josm.tools.Logging; 53 61 import org.openstreetmap.josm.tools.Shortcut; 54 62 import org.openstreetmap.josm.tools.StreamUtils; 55 63 … … import org.openstreetmap.josm.tools.StreamUtils; 58 66 * @since 2575 59 67 */ 60 68 public class SimplifyWayAction extends JosmAction { 69 private static final int MAX_WAYS_NO_ASK = 10; 61 70 62 71 /** 63 72 * Constructs a new {@code SimplifyWayAction}. … … public class SimplifyWayAction extends JosmAction { 120 129 } 121 130 122 131 /** 123 * Asks the user for max-err value used to simplify ways, if not remembered before 132 * Asks the user for max-err value used to simplify ways, if not remembered before. This is not asynchronous. 124 133 * @param ways the ways that are being simplified (to show estimated number of nodes to be removed) 125 134 * @param text the text being shown 126 135 * @param auto whether it's called automatically (conversion) or by the user 127 136 * @return the max-err value or -1 if canceled 137 * @see #askSimplifyWays(Collection, String, boolean, SimplifyWayActionListener...) 128 138 * @since 16566 129 139 */ 130 140 public static double askSimplifyWays(List<Way> ways, String text, boolean auto) { 141 AtomicReference<Double> returnVar = new AtomicReference<>((double) -1); 142 AtomicBoolean hasRun = new AtomicBoolean(); 143 askSimplifyWays(ways, text, auto, (result, command) -> { 144 synchronized (returnVar) { 145 returnVar.set(result); 146 returnVar.notifyAll(); 147 hasRun.set(true); 148 } 149 }); 150 if (!SwingUtilities.isEventDispatchThread()) { 151 synchronized (returnVar) { 152 while (!hasRun.get()) { 153 try { 154 // Wait 2 seconds 155 returnVar.wait(2000); 156 } catch (InterruptedException e) { 157 Logging.error(e); 158 Thread.currentThread().interrupt(); 159 } 160 } 161 } 162 } 163 return returnVar.get(); 164 } 165 166 /** 167 * Asks the user for max-err value used to simplify ways, if not remembered before 168 * @param waysCollection the ways that are being simplified (to show estimated number of nodes to be removed) 169 * @param text the text being shown 170 * @param auto whether it's called automatically (conversion) or by the user 171 * @param listeners Listeners to call when the max error is calculated 172 * @since xxx 173 */ 174 public static void askSimplifyWays(Collection<Way> waysCollection, String text, boolean auto, SimplifyWayActionListener... listeners) { 175 final Collection<SimplifyWayActionListener> realListeners = Stream.of(listeners).filter(Objects::nonNull).collect(Collectors.toList()); 176 final List<Way> ways; 177 if (waysCollection instanceof List) { 178 ways = (List<Way>) waysCollection; 179 } else { 180 ways = new ArrayList<>(waysCollection); 181 } 131 182 IPreferences s = Config.getPref(); 132 183 String key = "simplify-way." + (auto ? "auto." : ""); 133 184 String keyRemember = key + "remember"; … … public class SimplifyWayAction extends JosmAction { 135 186 136 187 String r = s.get(keyRemember, "ask"); 137 188 if (auto && "no".equals(r)) { 138 return -1; 189 realListeners.forEach(listener -> listener.maxErrorListener(-1, null)); 190 return; 139 191 } else if ("yes".equals(r)) { 140 return s.getDouble(keyError, 3.0); 192 realListeners.forEach(listener -> listener.maxErrorListener(s.getDouble(keyError, 3.0), null)); 193 return; 141 194 } 142 195 143 196 JPanel p = new JPanel(new GridBagLayout()); … … public class SimplifyWayAction extends JosmAction { 178 231 } else { 179 232 ed.setButtonIcons("ok", "cancel"); 180 233 } 181 182 int ret = ed.showDialog().getValue(); 183 double val = (double) n.getValue(); 184 if (l.lastCommand != null && l.lastCommand.equals(UndoRedoHandler.getInstance().getLastCommand())) { 185 UndoRedoHandler.getInstance().undo(); 186 l.lastCommand = null; 187 } 188 if (ret == 1) { 189 s.putDouble(keyError, val); 190 if (c.isSelected()) { 191 s.put(keyRemember, "yes"); 192 } 193 return val; 194 } else { 195 if (auto && c.isSelected()) { //do not remember cancel for manual simplify, otherwise nothing would happen 196 s.put(keyRemember, "no"); 234 ed.addWindowListener(new WindowAdapter() { 235 @Override 236 public void windowClosed(WindowEvent e) { 237 final double result; 238 final int ret = ed.getValue(); 239 double val = (double) n.getValue(); 240 final Command command = l.lastCommand; 241 if (l.lastCommand != null && l.lastCommand.equals(UndoRedoHandler.getInstance().getLastCommand())) { 242 UndoRedoHandler.getInstance().undo(); 243 l.lastCommand = null; 244 } 245 if (ret == 1) { 246 s.putDouble(keyError, val); 247 if (c.isSelected()) { 248 s.put(keyRemember, "yes"); 249 } 250 result = val; 251 } else { 252 if (auto && c.isSelected()) { //do not remember cancel for manual simplify, otherwise nothing would happen 253 s.put(keyRemember, "no"); 254 } 255 result = -1; 256 } 257 realListeners.forEach(listener -> listener.maxErrorListener(result, command)); 197 258 } 198 return -1;199 }259 }); 260 GuiHelper.runInEDT(ed::showDialog); 200 261 } 201 262 202 263 @Override 203 264 public void actionPerformed(ActionEvent e) { 204 265 DataSet ds = getLayerManager().getEditDataSet(); 205 ds.update(() -> { 206 List<Way> ways = ds.getSelectedWays().stream() 207 .filter(p -> !p.isIncomplete()) 208 .collect(Collectors.toList()); 209 if (ways.isEmpty()) { 210 alertSelectAtLeastOneWay(); 211 return; 212 } else if (!confirmWayWithNodesOutsideBoundingBox(ways) || (ways.size() > 10 && !confirmSimplifyManyWays(ways.size()))) { 213 return; 214 } 215 216 String lengthstr = SystemOfMeasurement.getSystemOfMeasurement().getDistText( 217 ways.stream().mapToDouble(Way::getLength).sum()); 218 219 double err = askSimplifyWays(ways, trn( 220 "You are about to simplify {0} way with a total length of {1}.", 221 "You are about to simplify {0} ways with a total length of {1}.", 222 ways.size(), ways.size(), lengthstr), false); 266 List<Way> ways = ds.getSelectedWays().stream() 267 .filter(p -> !p.isIncomplete()) 268 .collect(Collectors.toList()); 269 if (ways.isEmpty()) { 270 alertSelectAtLeastOneWay(); 271 return; 272 } else if (!confirmWayWithNodesOutsideBoundingBox(ways) || (ways.size() > MAX_WAYS_NO_ASK && !confirmSimplifyManyWays(ways.size()))) { 273 return; 274 } 223 275 224 if (err > 0) { 225 simplifyWays(ways, err); 276 String lengthstr = SystemOfMeasurement.getSystemOfMeasurement().getDistText( 277 ways.stream().mapToDouble(Way::getLength).sum()); 278 279 askSimplifyWays(ways, trn( 280 "You are about to simplify {0} way with a total length of {1}.", 281 "You are about to simplify {0} ways with a total length of {1}.", 282 ways.size(), ways.size(), lengthstr), false, (maxErr, simplifyCommand) -> { 283 if (maxErr > 0) { 284 if (simplifyCommand == null) { 285 simplifyWays(ways, maxErr); 286 } else { 287 GuiHelper.runInEDT(() -> UndoRedoHandler.getInstance().add(simplifyCommand)); 288 } 289 } 226 290 } 227 }); 291 ); 292 228 293 } 229 294 230 295 /** … … public class SimplifyWayAction extends JosmAction { 308 373 * @since 16566 (private) 309 374 */ 310 375 private static SequenceCommand buildSimplifyWaysCommand(List<Way> ways, double threshold) { 311 Collection<Command> allCommands = ways.stream() 376 // Use `parallelStream` since this can significantly decrease the amount of time taken for large numbers of ways 377 Collection<Command> allCommands = ways.parallelStream() 312 378 .map(way -> createSimplifyCommand(way, threshold)) 313 379 .filter(Objects::nonNull) 314 380 .collect(StreamUtils.toUnmodifiableList()); … … public class SimplifyWayAction extends JosmAction { 499 565 private final JLabel nodesToRemove; 500 566 private final SpinnerNumberModel errorModel; 501 567 private final List<Way> ways; 568 private ForkJoinTask<?> futureNodesToRemove; 502 569 503 570 SimplifyChangeListener(JLabel nodesToRemove, SpinnerNumberModel errorModel, List<Way> ways) { 504 571 this.nodesToRemove = nodesToRemove; … … public class SimplifyWayAction extends JosmAction { 508 575 509 576 @Override 510 577 public void stateChanged(ChangeEvent e) { 511 if (Objects.equals(UndoRedoHandler.getInstance().getLastCommand(), lastCommand)) { 512 UndoRedoHandler.getInstance().undo(); 578 MainApplication.worker.execute(() -> { 579 synchronized (errorModel) { 580 double threshold = errorModel.getNumber().doubleValue(); 581 if (futureNodesToRemove != null) { 582 futureNodesToRemove.cancel(false); 583 } 584 futureNodesToRemove = MainApplication.getForkJoinPool().submit(new ForkJoinTask<Command>() { 585 private Command result; 586 587 @Override 588 public Command getRawResult() { 589 return result; 590 } 591 592 @Override 593 protected void setRawResult(Command value) { 594 this.result = value; 595 } 596 597 @Override 598 protected boolean exec() { 599 synchronized (SimplifyChangeListener.this) { 600 if (!this.isCancelled()) { 601 GuiHelper.runInEDT(() -> nodesToRemove.setText(tr("calculating..."))); 602 result = updateNodesToRemove(ways, threshold); 603 return true; 604 } 605 } 606 return false; 607 } 608 }); 609 } 610 }); 611 } 612 613 private Command updateNodesToRemove(List<Way> ways, double threshold) { 614 // This must come first in order for everything else to be accurate 615 if (lastCommand != null && lastCommand.equals(UndoRedoHandler.getInstance().getLastCommand())) { 616 // We need to wait to avoid cases where we cannot undo due threading issues. 617 GuiHelper.runInEDTAndWait(() -> UndoRedoHandler.getInstance().undo()); 513 618 } 514 double threshold = errorModel.getNumber().doubleValue(); 515 int removeNodes = simplifyWaysCountNodesRemoved(ways, threshold); 516 nodesToRemove.setText(trn( 517 "(about {0} node to remove)", 518 "(about {0} nodes to remove)", removeNodes, removeNodes)); 519 lastCommand = SimplifyWayAction.buildSimplifyWaysCommand(ways, threshold); 520 if (lastCommand != null) { 521 UndoRedoHandler.getInstance().add(lastCommand); 619 final Command command = buildSimplifyWaysCommand(ways, threshold); 620 // This is the same code from simplifyWaysCountNodesRemoved, but is duplicated for performance reasons 621 // (this avoids running the code to calculate the command twice). 622 int removeNodes = command == null ? 0 : (int) command.getParticipatingPrimitives().stream() 623 .filter(Node.class::isInstance) 624 .count(); 625 int totalNodes = ways.parallelStream().mapToInt(Way::getNodesCount).sum(); 626 int percent = (int) Math.round(100 * removeNodes / (double) totalNodes); 627 GuiHelper.runInEDTAndWait(() -> 628 nodesToRemove.setText(trn( 629 "(about {0} node to remove ({1}%))", 630 "(about {0} nodes to remove ({1}%))", removeNodes, removeNodes, percent)) 631 ); 632 lastCommand = command; 633 if (lastCommand != null && ways.size() < MAX_WAYS_NO_ASK) { 634 GuiHelper.runInEDTAndWait(() -> UndoRedoHandler.getInstance().add(lastCommand)); 522 635 } 636 return lastCommand; 523 637 } 524 638 } 639 640 641 /** 642 * A listener that is called when the {@link #askSimplifyWays} method is called. 643 * @since xxx 644 */ 645 @FunctionalInterface 646 public interface SimplifyWayActionListener { 647 /** 648 * Called when the max error is set 649 * @param maxError The max error 650 * @param simplify The command that was used to generate stats for the user with the maxError. May be null. 651 */ 652 void maxErrorListener(double maxError, Command simplify); 653 } 525 654 } -
src/org/openstreetmap/josm/gui/MainApplication.java
diff --git a/src/org/openstreetmap/josm/gui/MainApplication.java b/src/org/openstreetmap/josm/gui/MainApplication.java index 744d8878c..a3ab8ff65 100644
a b import java.util.Set; 44 44 import java.util.TreeSet; 45 45 import java.util.concurrent.ExecutorService; 46 46 import java.util.concurrent.Executors; 47 import java.util.concurrent.ForkJoinPool; 47 48 import java.util.concurrent.Future; 48 49 import java.util.logging.Level; 49 50 import java.util.stream.Collectors; … … public class MainApplication { 212 213 */ 213 214 public static final ExecutorService worker = new ProgressMonitorExecutor("main-worker-%d", Thread.NORM_PRIORITY); 214 215 216 /** 217 * This class literally exists to ensure that consumers cannot shut the pool down. 218 * @since xxx 219 */ 220 private static class ForkJoinPoolAlways extends ForkJoinPool { 221 ForkJoinPoolAlways() { 222 // Leave a processor for the UI. 223 super(Math.max(1, Runtime.getRuntime().availableProcessors() - 1), ForkJoinPool.defaultForkJoinWorkerThreadFactory, 224 new BugReportExceptionHandler(), false); 225 } 226 227 @Override 228 public void shutdown() { 229 // Do nothing. This is functionally the same as for the commonPool. 230 } 231 232 @Override 233 public List<Runnable> shutdownNow() { 234 // Do nothing. This is functionally the same as for the commonPool. 235 return Collections.emptyList(); 236 } 237 } 238 239 // If there is a security manager, ForkJoinPool.commonPool uses an "InnocuousForkJoinWorkerThreadFactory", which can kill stuff calling 240 // repaint. Otherwise, the common pool is better from (most) perspectives. 241 private static final ForkJoinPool forkJoinPool = System.getSecurityManager() != null ? new ForkJoinPoolAlways() : ForkJoinPool.commonPool(); 242 243 /** 244 * Get a generic {@link ForkJoinPool}. 245 * @return A ForkJoinPool to use. Calling {@link ForkJoinPool#shutdown()} or {@link ForkJoinPool#shutdownNow()} has no effect. 246 * @since xxx 247 */ 248 public static final ForkJoinPool getForkJoinPool() { 249 return forkJoinPool; 250 } 251 215 252 /** 216 253 * Provides access to the layers displayed in the main view. 217 254 */ … … public class MainApplication { 283 320 * Listener that sets the enabled state of undo/redo menu entries. 284 321 */ 285 322 final CommandQueueListener redoUndoListener = (queueSize, redoSize) -> { 286 menu.undo.setEnabled(queueSize > 0);287 menu.redo.setEnabled(redoSize > 0);323 GuiHelper.runInEDT(() -> menu.undo.setEnabled(queueSize > 0)); 324 GuiHelper.runInEDT(() -> menu.redo.setEnabled(redoSize > 0)); 288 325 }; 289 326 290 327 /**
