Ticket #19199: 19199.threading.2.patch

File 19199.threading.2.patch, 20.5 KB (added by taylor.smock, 5 years ago)

Add methods to allow for async ExtendedDialogs (so we can update the map), don't update mapview when there are more than 10 ways selected

  • .project

    diff --git a/.project b/.project
    index c82979ddf..844e2ad74 100644
    a b  
    11<?xml version="1.0" encoding="UTF-8"?>
    22<projectDescription>
    3         <name>JOSM</name>
     3        <name>JOSM_clean</name>
    44        <comment></comment>
    55        <projects>
    66        </projects>
  • 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..66cb6bffb 100644
    a b import java.util.LinkedList;  
    1717import java.util.List;
    1818import java.util.Objects;
    1919import java.util.Set;
     20import java.util.concurrent.ForkJoinTask;
     21import java.util.concurrent.atomic.AtomicBoolean;
     22import java.util.concurrent.atomic.AtomicReference;
    2023import java.util.stream.Collectors;
     24import java.util.stream.Stream;
    2125
    2226import javax.swing.BorderFactory;
    2327import javax.swing.JCheckBox;
    import org.openstreetmap.josm.gui.HelpAwareOptionPane;  
    4650import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
    4751import org.openstreetmap.josm.gui.MainApplication;
    4852import org.openstreetmap.josm.gui.Notification;
     53import org.openstreetmap.josm.gui.util.GuiHelper;
    4954import org.openstreetmap.josm.spi.preferences.Config;
    5055import org.openstreetmap.josm.spi.preferences.IPreferences;
    5156import org.openstreetmap.josm.tools.GBC;
    5257import org.openstreetmap.josm.tools.ImageProvider;
     58import org.openstreetmap.josm.tools.Logging;
    5359import org.openstreetmap.josm.tools.Shortcut;
    5460import org.openstreetmap.josm.tools.StreamUtils;
    5561
    import org.openstreetmap.josm.tools.StreamUtils;  
    5864 * @since 2575
    5965 */
    6066public class SimplifyWayAction extends JosmAction {
     67    private static final int MAX_WAYS_NO_ASK = 10;
    6168
    6269    /**
    6370     * Constructs a new {@code SimplifyWayAction}.
    public class SimplifyWayAction extends JosmAction {  
    120127    }
    121128
    122129    /**
    123      * Asks the user for max-err value used to simplify ways, if not remembered before
     130     * Asks the user for max-err value used to simplify ways, if not remembered before. This is not asynchronous.
    124131     * @param ways the ways that are being simplified (to show estimated number of nodes to be removed)
    125132     * @param text the text being shown
    126133     * @param auto whether it's called automatically (conversion) or by the user
    127134     * @return the max-err value or -1 if canceled
     135     * @see #askSimplifyWays(Collection, String, boolean, SimplifyWayActionListener...)
    128136     * @since 16566
    129137     */
    130138    public static double askSimplifyWays(List<Way> ways, String text, boolean auto) {
     139        AtomicReference<Double> returnVar = new AtomicReference<>((double) -1);
     140        AtomicBoolean hasRun = new AtomicBoolean();
     141        askSimplifyWays(ways, text, auto, result -> {
     142            synchronized (returnVar) {
     143                returnVar.set(result);
     144                returnVar.notifyAll();
     145                hasRun.set(true);
     146            }
     147        });
     148        if (!SwingUtilities.isEventDispatchThread()) {
     149            synchronized (returnVar) {
     150                while (!hasRun.get()) {
     151                    try {
     152                        // Wait 2 seconds
     153                        returnVar.wait(2000);
     154                    } catch (InterruptedException e) {
     155                        Logging.error(e);
     156                        Thread.currentThread().interrupt();
     157                    }
     158                }
     159            }
     160        }
     161        return returnVar.get();
     162    }
     163
     164    /**
     165     * Asks the user for max-err value used to simplify ways, if not remembered before
     166     * @param waysCollection the ways that are being simplified (to show estimated number of nodes to be removed)
     167     * @param text the text being shown
     168     * @param auto whether it's called automatically (conversion) or by the user
     169     * @param listeners Listeners to call when the max error is calculated
     170     * @since xxx
     171     */
     172    public static void askSimplifyWays(Collection<Way> waysCollection, String text, boolean auto, SimplifyWayActionListener... listeners) {
     173        final Collection<SimplifyWayActionListener> realListeners = Stream.of(listeners).filter(Objects::nonNull).collect(Collectors.toList());
     174        final List<Way> ways;
     175        if (waysCollection instanceof List) {
     176            ways = (List<Way>) waysCollection;
     177        } else {
     178            ways = new ArrayList<>(waysCollection);
     179        }
    131180        IPreferences s = Config.getPref();
    132181        String key = "simplify-way." + (auto ? "auto." : "");
    133182        String keyRemember = key + "remember";
    public class SimplifyWayAction extends JosmAction {  
    135184
    136185        String r = s.get(keyRemember, "ask");
    137186        if (auto && "no".equals(r)) {
    138             return -1;
     187            realListeners.forEach(listener -> listener.maxErrorListener(-1));
     188            return;
    139189        } else if ("yes".equals(r)) {
    140             return s.getDouble(keyError, 3.0);
     190            realListeners.forEach(listener -> listener.maxErrorListener(s.getDouble(keyError, 3.0)));
     191            return;
    141192        }
    142193
    143194        JPanel p = new JPanel(new GridBagLayout());
    public class SimplifyWayAction extends JosmAction {  
    178229        } else {
    179230            ed.setButtonIcons("ok", "cancel");
    180231        }
    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");
     232        ed.addResultListener(ret -> {
     233            final double result;
     234            double val = (double) n.getValue();
     235            if (l.lastCommand != null && l.lastCommand.equals(UndoRedoHandler.getInstance().getLastCommand())) {
     236                UndoRedoHandler.getInstance().undo();
     237                l.lastCommand = null;
    192238            }
    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");
     239            if (ret == 1) {
     240                s.putDouble(keyError, val);
     241                if (c.isSelected()) {
     242                    s.put(keyRemember, "yes");
     243                }
     244                result = val;
     245            } else {
     246                if (auto && c.isSelected()) { //do not remember cancel for manual simplify, otherwise nothing would happen
     247                    s.put(keyRemember, "no");
     248                }
     249                result = -1;
    197250            }
    198             return -1;
    199         }
     251            realListeners.forEach(listener -> listener.maxErrorListener(result));
     252        });
     253        GuiHelper.runInEDT(ed::showDialog);
    200254    }
    201255
    202256    @Override
    203257    public void actionPerformed(ActionEvent e) {
    204258        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             }
     259        List<Way> ways = ds.getSelectedWays().stream()
     260                .filter(p -> !p.isIncomplete())
     261                .collect(Collectors.toList());
     262        if (ways.isEmpty()) {
     263            alertSelectAtLeastOneWay();
     264            return;
     265        } else if (!confirmWayWithNodesOutsideBoundingBox(ways) || (ways.size() > MAX_WAYS_NO_ASK && !confirmSimplifyManyWays(ways.size()))) {
     266            return;
     267        }
    215268
    216             String lengthstr = SystemOfMeasurement.getSystemOfMeasurement().getDistText(
    217                     ways.stream().mapToDouble(Way::getLength).sum());
     269        String lengthstr = SystemOfMeasurement.getSystemOfMeasurement().getDistText(
     270                ways.stream().mapToDouble(Way::getLength).sum());
    218271
    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);
     272        double err = askSimplifyWays(ways, trn(
     273                "You are about to simplify {0} way with a total length of {1}.",
     274                "You are about to simplify {0} ways with a total length of {1}.",
     275                ways.size(), ways.size(), lengthstr), false);
    223276
    224             if (err > 0) {
    225                 simplifyWays(ways, err);
    226             }
    227         });
     277        if (err > 0) {
     278            simplifyWays(ways, err);
     279        }
    228280    }
    229281
    230282    /**
    public class SimplifyWayAction extends JosmAction {  
    308360     * @since 16566 (private)
    309361     */
    310362    private static SequenceCommand buildSimplifyWaysCommand(List<Way> ways, double threshold) {
    311         Collection<Command> allCommands = ways.stream()
     363        // Use `parallelStream` since this can significantly decrease the amount of time taken for large numbers of ways
     364        Collection<Command> allCommands = ways.parallelStream()
    312365                .map(way -> createSimplifyCommand(way, threshold))
    313366                .filter(Objects::nonNull)
    314367                .collect(StreamUtils.toUnmodifiableList());
    public class SimplifyWayAction extends JosmAction {  
    499552        private final JLabel nodesToRemove;
    500553        private final SpinnerNumberModel errorModel;
    501554        private final List<Way> ways;
     555        private ForkJoinTask<?> futureNodesToRemove;
    502556
    503557        SimplifyChangeListener(JLabel nodesToRemove, SpinnerNumberModel errorModel, List<Way> ways) {
    504558            this.nodesToRemove = nodesToRemove;
    public class SimplifyWayAction extends JosmAction {  
    508562
    509563        @Override
    510564        public void stateChanged(ChangeEvent e) {
    511             if (Objects.equals(UndoRedoHandler.getInstance().getLastCommand(), lastCommand)) {
    512                 UndoRedoHandler.getInstance().undo();
     565            MainApplication.worker.execute(() -> {
     566                synchronized (errorModel) {
     567                    double threshold = errorModel.getNumber().doubleValue();
     568                    if (futureNodesToRemove != null) {
     569                        futureNodesToRemove.cancel(false);
     570                    }
     571                    futureNodesToRemove = MainApplication.getForkJoinPool().submit(new ForkJoinTask<Command>() {
     572                        private Command result;
     573
     574                        @Override
     575                        public Command getRawResult() {
     576                            return result;
     577                        }
     578
     579                        @Override
     580                        protected void setRawResult(Command value) {
     581                            this.result = value;
     582                        }
     583
     584                        @Override
     585                        protected boolean exec() {
     586                            synchronized (SimplifyChangeListener.this) {
     587                                if (!this.isCancelled()) {
     588                                    result = updateNodesToRemove(ways, threshold);
     589                                    return true;
     590                                }
     591                            }
     592                            return false;
     593                        }
     594                    });
     595                }
     596            });
     597        }
     598
     599        private Command updateNodesToRemove(List<Way> ways, double threshold) {
     600            // This must come first in order for everything else to be accurate
     601            if (lastCommand != null && Objects.equals(lastCommand, UndoRedoHandler.getInstance().getLastCommand())) {
     602                // We need to wait to avoid cases where we cannot undo due threading issues.
     603                GuiHelper.runInEDTAndWait(() -> UndoRedoHandler.getInstance().undo());
    513604            }
    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);
     605            final Command command = buildSimplifyWaysCommand(ways, threshold);
     606            // This is the same code from simplifyWaysCountNodesRemoved, but is duplicated for performance reasons
     607            // (this avoids running the code to calculate the command twice).
     608            int removeNodes = command == null ? 0 : (int) command.getParticipatingPrimitives().stream()
     609                    .filter(Node.class::isInstance)
     610                    .count();
     611            int totalNodes = ways.parallelStream().mapToInt(Way::getNodesCount).sum();
     612            int percent = (int) Math.round(100 * removeNodes / (double) totalNodes);
     613            GuiHelper.runInEDTAndWait(() ->
     614                    nodesToRemove.setText(trn(
     615                            "(about {0} node to remove ({1}%))",
     616                            "(about {0} nodes to remove ({1}%))", removeNodes, removeNodes, percent))
     617            );
     618            lastCommand = command;
     619            if (lastCommand != null && ways.size() < MAX_WAYS_NO_ASK) {
     620                GuiHelper.runInEDTAndWait(() -> UndoRedoHandler.getInstance().add(lastCommand));
    522621            }
     622            return lastCommand;
    523623        }
    524624    }
     625
     626
     627    /**
     628     * A listener that is called when the {@link #askSimplifyWays} method is called.
     629     * @since xxx
     630     */
     631    @FunctionalInterface
     632    public interface SimplifyWayActionListener {
     633        /**
     634         * Called when the max error is set
     635         * @param maxError The max error
     636         */
     637        void maxErrorListener(double maxError);
     638    }
    525639}
  • src/org/openstreetmap/josm/gui/ExtendedDialog.java

    diff --git a/src/org/openstreetmap/josm/gui/ExtendedDialog.java b/src/org/openstreetmap/josm/gui/ExtendedDialog.java
    index a11295f58..fcc616a4d 100644
    a b import org.openstreetmap.josm.tools.GBC;  
    4242import org.openstreetmap.josm.tools.ImageProvider;
    4343import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
    4444import org.openstreetmap.josm.tools.InputMapUtils;
     45import org.openstreetmap.josm.tools.ListenerList;
    4546import org.openstreetmap.josm.tools.Logging;
    4647import org.openstreetmap.josm.tools.Utils;
    4748
    public class ExtendedDialog extends JDialog implements IExtendedDialog {  
    9596    private transient Icon icon;
    9697    private final boolean modal;
    9798    private boolean focusOnDefaultButton;
     99    private final ListenerList<ExtendedDialogResultListener> resultListeners = ListenerList.create();
    98100
    99101    /** true, if the dialog should include a help button */
    100102    private boolean showHelpButton;
    public class ExtendedDialog extends JDialog implements IExtendedDialog {  
    241243        // Check if the user has set the dialog to not be shown again
    242244        if (toggleCheckState()) {
    243245            result = toggleValue;
     246            this.finishResult();
    244247            return this;
    245248        }
    246249
    public class ExtendedDialog extends JDialog implements IExtendedDialog {  
    388391    protected void buttonAction(int buttonIndex, ActionEvent evt) {
    389392        result = buttonIndex+1;
    390393        setVisible(false);
     394        this.finishResult();
    391395    }
    392396
    393397    /**
    public class ExtendedDialog extends JDialog implements IExtendedDialog {  
    419423                            getClass().getName(), actionEvent, new Exception().getStackTrace()[1]);
    420424                }
    421425                setVisible(false);
     426                ExtendedDialog.this.finishResult();
    422427            }
    423428        };
    424429
    public class ExtendedDialog extends JDialog implements IExtendedDialog {  
    561566            HelpBrowser.setUrlForHelpTopic(helpTopic);
    562567        }
    563568    }
     569
     570    /**
     571     * Add a listener for the results. This makes this dialog non-modal, and stops blocking the EDT.
     572     * @param listener The listener to add
     573     * @since xxx
     574     */
     575    public void addResultListener(ExtendedDialogResultListener listener) {
     576        if (this.isModal()) {
     577            this.setModal(false);
     578        }
     579        this.resultListeners.addListener(listener);
     580    }
     581
     582    /**
     583     * Remove a result listener. If there are no more result listeners, this dialog will become modal, and will block the EDT.
     584     * @param listener The listener to remove
     585     * @since xxx
     586     */
     587    public void removeResultListener(ExtendedDialogResultListener listener) {
     588        this.resultListeners.removeListener(listener);
     589        if (!this.resultListeners.hasListeners()) {
     590            this.setModal(true);
     591        }
     592    }
     593
     594    /**
     595     * Call when the result has been calculated. There is no point in keeping the listeners, so remove them all.
     596     */
     597    private void finishResult() {
     598        this.resultListeners.fireEvent(listener -> listener.parseResult(result));
     599        final List<ExtendedDialogResultListener> listeners = new ArrayList<>();
     600        this.resultListeners.fireEvent(listeners::add);
     601        for (ExtendedDialogResultListener listener : listeners) {
     602            this.removeResultListener(listener);
     603        }
     604    }
     605
     606    /**
     607     * A listener for results
     608     * @since xxx
     609     */
     610    public interface ExtendedDialogResultListener {
     611        /**
     612         * Do something with a result
     613         * @param result The result when the dialog is finished
     614         */
     615        void parseResult(int result);
     616    }
    564617}
  • 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 3609f845c..bba626e49 100644
    a b import java.util.Set;  
    4444import java.util.TreeSet;
    4545import java.util.concurrent.ExecutorService;
    4646import java.util.concurrent.Executors;
     47import java.util.concurrent.ForkJoinPool;
    4748import java.util.concurrent.Future;
    4849import java.util.logging.Level;
    4950import java.util.stream.Collectors;
    public class MainApplication {  
    212213     */
    213214    public static final ExecutorService worker = new ProgressMonitorExecutor("main-worker-%d", Thread.NORM_PRIORITY);
    214215
     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
    215252    /**
    216253     * Provides access to the layers displayed in the main view.
    217254     */
    public class MainApplication {  
    283320     * Listener that sets the enabled state of undo/redo menu entries.
    284321     */
    285322    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));
    288325        };
    289326
    290327    /**