Ticket #19111: 19111.patch

File 19111.patch, 24.9 KB (added by GerdP, 5 years ago)

WIP

  • src/org/openstreetmap/josm/actions/UnGlueAction.java

     
    55import static org.openstreetmap.josm.tools.I18n.tr;
    66import static org.openstreetmap.josm.tools.I18n.trn;
    77
     8import java.awt.Point;
    89import java.awt.event.ActionEvent;
    910import java.awt.event.KeyEvent;
    1011import java.util.ArrayList;
    1112import java.util.Collection;
    1213import java.util.Collections;
     14import java.util.HashMap;
    1315import java.util.HashSet;
    14 import java.util.LinkedList;
    1516import java.util.List;
     17import java.util.Map;
    1618import java.util.Set;
    1719import java.util.stream.Collectors;
    1820
    1921import javax.swing.JOptionPane;
    20 import javax.swing.JPanel;
    2122
    2223import org.openstreetmap.josm.command.AddCommand;
    2324import org.openstreetmap.josm.command.ChangeCommand;
     
    4142import org.openstreetmap.josm.tools.Logging;
    4243import org.openstreetmap.josm.tools.Shortcut;
    4344import org.openstreetmap.josm.tools.UserCancelException;
    44 import org.openstreetmap.josm.tools.Utils;
    4545
    4646/**
    47  * Duplicate nodes that are used by multiple ways.
     47 * Duplicate nodes that are used by multiple ways or tagged nodes used by a single way
     48 * or nodes which referenced more than once by a single way.
    4849 *
    49  * Resulting nodes are identical, up to their position.
    50  *
    5150 * This is the opposite of the MergeNodesAction.
    5251 *
    53  * If a single node is selected, it will copy that node and remove all tags from the old one
    5452 */
    5553public class UnGlueAction extends JosmAction {
    5654
     
    7573    @Override
    7674    public void actionPerformed(ActionEvent e) {
    7775        try {
    78             unglue(e);
     76            unglue();
    7977        } catch (UserCancelException ignore) {
    8078            Logging.trace(ignore);
    8179        } finally {
     
    8381        }
    8482    }
    8583
    86     protected void unglue(ActionEvent e) throws UserCancelException {
     84    protected void unglue() throws UserCancelException {
    8785
    8886        Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected();
    8987
    9088        String errMsg = null;
    9189        int errorTime = Notification.TIME_DEFAULT;
     90
    9291        if (checkSelectionOneNodeAtMostOneWay(selection)) {
    9392            checkAndConfirmOutlyingUnglue();
    94             int count = 0;
    95             for (Way w : selectedNode.getParentWays()) {
    96                 if (!w.isUsable() || w.getNodesCount() < 1) {
    97                     continue;
     93            List<Way> parentWays = selectedNode.getParentWays().stream().filter(Way::isUsable).collect(Collectors.toList());
     94
     95            if (parentWays.size() < 2) {
     96                if (!parentWays.isEmpty()) {
     97                    // single way
     98                    Way way = selectedWay == null ? parentWays.get(0) : selectedWay;
     99                    boolean closedOrSelfCrossing = way.getNodes().stream().filter(n -> n == selectedNode).count() > 1;
     100
     101                    final PropertiesMembershipChoiceDialog dialog = PropertiesMembershipChoiceDialog.showIfNecessary(
     102                            Collections.singleton(selectedNode), !selectedNode.isTagged());
     103                    if (dialog != null) {
     104                        unglueOneNodeAtMostOneWay(way, dialog);
     105                        return;
     106                    } else if (closedOrSelfCrossing) {
     107                        unglueClosedOrSelfCrossingWay(way, dialog);
     108                        return;
     109                    }
    98110                }
    99                 count++;
    100             }
    101             if (count < 2) {
    102                 boolean selfCrossing = false;
    103                 if (count == 1) {
    104                     // First try unglue self-crossing way
    105                     selfCrossing = unglueSelfCrossingWay();
    106                 }
    107                 // If there aren't enough ways, maybe the user wanted to unglue the nodes
    108                 // (= copy tags to a new node)
    109                 if (!selfCrossing)
    110                     if (checkForUnglueNode(selection)) {
    111                         unglueOneNodeAtMostOneWay(e);
    112                     } else {
    113                         errorTime = Notification.TIME_SHORT;
    114                         errMsg = tr("This node is not glued to anything else.");
    115                     }
     111                errorTime = Notification.TIME_SHORT;
     112                errMsg = tr("This node is not glued to anything else.");
    116113            } else {
    117114                // and then do the work.
    118115                unglueWays();
     
    119116            }
    120117        } else if (checkSelectionOneWayAnyNodes(selection)) {
    121118            checkAndConfirmOutlyingUnglue();
    122             Set<Node> tmpNodes = new HashSet<>();
    123             for (Node n : selectedNodes) {
    124                 int count = 0;
    125                 for (Way w : n.getParentWays()) {
    126                     if (!w.isUsable()) {
    127                         continue;
    128                     }
    129                     count++;
    130                 }
    131                 if (count >= 2) {
    132                     tmpNodes.add(n);
    133                 }
    134             }
    135             if (tmpNodes.isEmpty()) {
     119            selectedNodes.removeIf(n -> n.getParentWays().stream().filter(Way::isUsable).count() < 2);
     120            if (selectedNodes.isEmpty()) {
    136121                if (selection.size() > 1) {
    137122                    errMsg = tr("None of these nodes are glued to anything else.");
    138123                } else {
    139124                    errMsg = tr("None of this way''s nodes are glued to anything else.");
    140125                }
     126            } else if (selectedNodes.size() == 1) {
     127                selectedNode = selectedNodes.iterator().next();
     128                unglueWays();
    141129            } else {
    142130                // and then do the work.
    143                 selectedNodes = tmpNodes;
    144131                unglueOneWayAnyNodes();
    145132            }
    146133        } else {
     
    175162        selectedNodes = null;
    176163    }
    177164
    178     static void update(PropertiesMembershipChoiceDialog dialog, Node existingNode, List<Node> newNodes, Collection<Command> cmds) {
     165    static void update(PropertiesMembershipChoiceDialog dialog, Node existingNode, List<Node> newNodes, List<Command> cmds) {
    179166        updateMemberships(dialog.getMemberships().orElse(null), existingNode, newNodes, cmds);
    180167        updateProperties(dialog.getTags().orElse(null), existingNode, newNodes, cmds);
    181168    }
    182169
    183     private static void updateProperties(ExistingBothNew tags, Node existingNode, Iterable<Node> newNodes, Collection<Command> cmds) {
     170    private static void updateProperties(ExistingBothNew tags, Node existingNode, Iterable<Node> newNodes, List<Command> cmds) {
    184171        if (ExistingBothNew.NEW == tags) {
    185172            final Node newSelectedNode = new Node(existingNode);
    186173            newSelectedNode.removeAll();
     
    194181
    195182    /**
    196183     * Assumes there is one tagged Node stored in selectedNode that it will try to unglue.
    197      * (i.e. copy node and remove all tags from the old one. Relations will not be removed)
    198      * @param e event that triggered the action
     184     * (i.e. copy node and remove all tags from the old one.)
     185     * @param way way to modify
     186     * @param dialog
    199187     */
    200     private void unglueOneNodeAtMostOneWay(ActionEvent e) {
    201         final PropertiesMembershipChoiceDialog dialog;
    202         try {
    203             dialog = PropertiesMembershipChoiceDialog.showIfNecessary(Collections.singleton(selectedNode), true);
    204         } catch (UserCancelException ex) {
    205             Logging.trace(ex);
    206             return;
     188    private void unglueOneNodeAtMostOneWay(Way way, PropertiesMembershipChoiceDialog dialog) {
     189        List<Command> cmds = new ArrayList<>();
     190        List<Node> newNodes = new ArrayList<>();
     191        Way modWay = modifyWay(selectedNode, way, cmds, newNodes);
     192        cmds.add(new ChangeNodesCommand(way, modWay.getNodes()));
     193        if (dialog != null) {
     194            update(dialog, selectedNode, newNodes, cmds);
    207195        }
    208196
    209         final Node unglued = new Node(selectedNode, true);
    210         boolean moveSelectedNode = false;
    211 
    212         List<Command> cmds = new LinkedList<>();
    213         cmds.add(new AddCommand(selectedNode.getDataSet(), unglued));
    214         if (dialog != null && ExistingBothNew.NEW == dialog.getTags().orElse(null)) {
    215             // unglued node gets the ID and history, thus replace way node with a fresh one
    216             final Way way = selectedNode.getParentWays().get(0);
    217             final List<Node> newWayNodes = way.getNodes();
    218             newWayNodes.replaceAll(n -> selectedNode.equals(n) ? unglued : n);
    219             cmds.add(new ChangeNodesCommand(way, newWayNodes));
    220             updateMemberships(dialog.getMemberships().map(ExistingBothNew::opposite).orElse(null),
    221                     selectedNode, Collections.singletonList(unglued), cmds);
    222             updateProperties(dialog.getTags().map(ExistingBothNew::opposite).orElse(null),
    223                     selectedNode, Collections.singletonList(unglued), cmds);
    224             moveSelectedNode = true;
    225         } else if (dialog != null) {
    226             update(dialog, selectedNode, Collections.singletonList(unglued), cmds);
    227         }
    228 
    229         // If this wasn't called from menu, place it where the cursor is/was
     197        // Place the selected node where the cursor is/was
    230198        MapView mv = MainApplication.getMap().mapView;
    231         if (e.getSource() instanceof JPanel) {
    232             final LatLon latLon = mv.getLatLon(mv.lastMEvent.getX(), mv.lastMEvent.getY());
    233             if (moveSelectedNode) {
    234                 cmds.add(new MoveCommand(selectedNode, latLon));
    235             } else {
    236                 unglued.setCoor(latLon);
    237             }
     199        Point currMousePos = mv.getMousePosition();
     200        if (currMousePos != null) {
     201            final LatLon latLon = mv.getLatLon(currMousePos.getX(), currMousePos.getY());
     202            cmds.add(new MoveCommand(selectedNode, latLon));
     203        } else {
     204            cmds.add(new MoveCommand(selectedNode, 0, 5));
    238205        }
    239 
    240206        UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Unglued Node"), cmds));
    241         getLayerManager().getEditDataSet().setSelected(moveSelectedNode ? selectedNode : unglued);
    242         mv.repaint();
     207        getLayerManager().getEditDataSet().setSelected(selectedNode);
    243208    }
    244209
    245210    /**
    246      * Checks if selection is suitable for ungluing. This is the case when there's a single,
    247      * tagged node selected that's part of at least one way (ungluing an unconnected node does
    248      * not make sense. Due to the call order in actionPerformed, this is only called when the
    249      * node is only part of one or less ways.
    250      *
    251      * @param selection The selection to check against
    252      * @return {@code true} if selection is suitable
    253      */
    254     private boolean checkForUnglueNode(Collection<? extends OsmPrimitive> selection) {
    255         if (selection.size() != 1)
    256             return false;
    257         OsmPrimitive n = (OsmPrimitive) selection.toArray()[0];
    258         if (!(n instanceof Node))
    259             return false;
    260         if (((Node) n).getParentWays().isEmpty())
    261             return false;
    262 
    263         selectedNode = (Node) n;
    264         return selectedNode.isTagged();
    265     }
    266 
    267     /**
    268211     * Checks if the selection consists of something we can work with.
    269212     * Checks only if the number and type of items selected looks good.
    270213     *
    271      * If this method returns "true", selectedNode and selectedWay will be set.
     214     * If this method returns "true", selectedNode will be set, selectedWay might be set
    272215     *
    273216     * Returns true if either one node is selected or one node and one
    274217     * way are selected and the node is part of the way.
     
    361304     */
    362305    private static Way modifyWay(Node originalNode, Way w, List<Command> cmds, List<Node> newNodes) {
    363306        // clone the node for the way
    364         Node newNode = new Node(originalNode, true /* clear OSM ID */);
     307        Node newNode = cloneNode(originalNode, cmds);
    365308        newNodes.add(newNode);
    366         cmds.add(new AddCommand(originalNode.getDataSet(), newNode));
    367309
    368         List<Node> nn = new ArrayList<>();
    369         for (Node pushNode : w.getNodes()) {
    370             if (originalNode == pushNode) {
    371                 pushNode = newNode;
    372             }
    373             nn.add(pushNode);
    374         }
     310        List<Node> nn = new ArrayList<>(w.getNodes());
     311        nn.replaceAll(n -> n == originalNode ? newNode : n);
    375312        Way newWay = new Way(w);
    376313        newWay.setNodes(nn);
    377314
     
    378315        return newWay;
    379316    }
    380317
     318    private static Node cloneNode(Node originalNode, List<Command> cmds) {
     319        Node newNode = new Node(originalNode, true /* clear OSM ID */);
     320        cmds.add(new AddCommand(originalNode.getDataSet(), newNode));
     321        return newNode;
     322    }
     323
    381324    /**
    382325     * put all newNodes into the same relation(s) that originalNode is in
    383326     * @param memberships where the memberships should be places
     
    385328     * @param cmds List of commands that will contain the new "change relation" commands
    386329     * @param newNodes List of nodes that contain the new node
    387330     */
    388     private static void updateMemberships(ExistingBothNew memberships, Node originalNode, List<Node> newNodes, Collection<Command> cmds) {
     331    private static void updateMemberships(ExistingBothNew memberships, Node originalNode, List<Node> newNodes, List<Command> cmds) {
    389332        if (memberships == null || ExistingBothNew.OLD == memberships) {
    390333            return;
    391334        }
     
    421364     * dupe a single node into as many nodes as there are ways using it, OR
    422365     *
    423366     * dupe a single node once, and put the copy on the selected way
     367     * @throws UserCancelException if user cancels choice
    424368     */
    425     private void unglueWays() {
    426         final PropertiesMembershipChoiceDialog dialog;
    427         try {
    428             dialog = PropertiesMembershipChoiceDialog.showIfNecessary(Collections.singleton(selectedNode), false);
    429         } catch (UserCancelException e) {
    430             Logging.trace(e);
    431             return;
    432         }
    433 
    434         List<Command> cmds = new LinkedList<>();
    435         List<Node> newNodes = new LinkedList<>();
     369    private void unglueWays() throws UserCancelException {
     370        final PropertiesMembershipChoiceDialog dialog = PropertiesMembershipChoiceDialog
     371                .showIfNecessary(Collections.singleton(selectedNode), false);
     372        List<Command> cmds = new ArrayList<>();
     373        List<Node> newNodes = new ArrayList<>();
     374        List<Way> parentWays;
    436375        if (selectedWay == null) {
    437             LinkedList<Way> parentWays = selectedNode.referrers(Way.class).filter(Way::isUsable)
    438                     .collect(Collectors.toCollection(LinkedList::new));
     376            parentWays = selectedNode.referrers(Way.class).filter(Way::isUsable).collect(Collectors.toList());
    439377            // see #5452 and #18670
    440378            parentWays.sort((o1, o2) -> {
    441379                int d = Boolean.compare(!o1.isNew() && !o1.isModified(), !o2.isNew() && !o2.isModified());
     
    448386                return d;
    449387            });
    450388            // first way should not be changed, preferring older ways and those with fewer parents
    451             parentWays.removeFirst();
    452 
    453             Set<Way> warnParents = new HashSet<>();
    454             for (Way w : parentWays) {
    455                 if (w.isFirstLastNode(selectedNode))
    456                     warnParents.add(w);
    457                 cmds.add(new ChangeCommand(w, modifyWay(selectedNode, w, cmds, newNodes)));
    458             }
    459             notifyWayPartOfRelation(warnParents);
     389            parentWays.remove(0);
    460390        } else {
    461             Way modWay = modifyWay(selectedNode, selectedWay, cmds, newNodes);
    462             addCheckedChangeNodesCmd(cmds, selectedWay, modWay.getNodes());
     391            parentWays = Collections.singletonList(selectedWay);
    463392        }
     393        Set<Way> warnParents = new HashSet<>();
     394        for (Way w : parentWays) {
     395            if (w.isFirstLastNode(selectedNode))
     396                warnParents.add(w);
     397            cmds.add(new ChangeCommand(w, modifyWay(selectedNode, w, cmds, newNodes)));
     398        }
    464399
    465400        if (dialog != null) {
    466401            update(dialog, selectedNode, newNodes, cmds);
    467402        }
     403        notifyWayPartOfRelation(warnParents);
    468404
    469405        execCommands(cmds, newNodes);
    470406    }
     
    483419
    484420    /**
    485421     * Duplicates a node used several times by the same way. See #9896.
     422     * A closed way will be "opened" when the closing node is unglued.
     423     * @param way way to modify
     424     * @param dialog user dialog, might be null
    486425     * @return true if action is OK false if there is nothing to do
    487426     */
    488     private boolean unglueSelfCrossingWay() {
     427    private boolean unglueClosedOrSelfCrossingWay(Way way, PropertiesMembershipChoiceDialog dialog) {
    489428        // According to previous check, only one valid way through that node
    490         Way way = null;
    491         for (Way w: selectedNode.getParentWays()) {
    492             if (w.isUsable() && w.getNodesCount() >= 1) {
    493                 way = w;
    494             }
    495         }
    496         if (way == null) {
    497             return false;
    498         }
    499         List<Command> cmds = new LinkedList<>();
     429        List<Command> cmds = new ArrayList<>();
    500430        List<Node> oldNodes = way.getNodes();
    501431        List<Node> newNodes = new ArrayList<>(oldNodes.size());
    502432        List<Node> addNodes = new ArrayList<>();
    503         boolean seen = false;
     433        int count = 0;
    504434        for (Node n: oldNodes) {
    505             if (n == selectedNode) {
    506                 if (seen) {
    507                     Node newNode = new Node(n, true /* clear OSM ID */);
    508                     cmds.add(new AddCommand(selectedNode.getDataSet(), newNode));
    509                     newNodes.add(newNode);
    510                     addNodes.add(newNode);
    511                 } else {
    512                     newNodes.add(n);
    513                     seen = true;
    514                 }
    515             } else {
    516                 newNodes.add(n);
     435            if (n == selectedNode && count++ > 0) {
     436                n = cloneNode(selectedNode, cmds);
     437                addNodes.add(n);
    517438            }
     439            newNodes.add(n);
    518440        }
    519441        if (addNodes.isEmpty()) {
    520442            // selectedNode doesn't need unglue
    521443            return false;
    522444        }
     445        if (dialog != null) {
     446            update(dialog, selectedNode, addNodes, cmds);
     447        }
    523448        addCheckedChangeNodesCmd(cmds, way, newNodes);
    524         try {
    525             final PropertiesMembershipChoiceDialog dialog = PropertiesMembershipChoiceDialog.showIfNecessary(
    526                     Collections.singleton(selectedNode), false);
    527             if (dialog != null) {
    528                 update(dialog, selectedNode, addNodes, cmds);
    529             }
    530             execCommands(cmds, addNodes);
    531             return true;
    532         } catch (UserCancelException ignore) {
    533             Logging.trace(ignore);
    534         }
    535         return false;
     449        execCommands(cmds, addNodes);
     450        return true;
    536451    }
    537452
    538453    /**
    539454     * dupe all nodes that are selected, and put the copies on the selected way
     455     * @throws UserCancelException
    540456     *
    541457     */
    542     private void unglueOneWayAnyNodes() {
    543         Way tmpWay = selectedWay;
     458    private void unglueOneWayAnyNodes() throws UserCancelException {
     459        final PropertiesMembershipChoiceDialog dialog =
     460            PropertiesMembershipChoiceDialog.showIfNecessary(selectedNodes, false);
    544461
    545         final PropertiesMembershipChoiceDialog dialog;
    546         try {
    547             dialog = PropertiesMembershipChoiceDialog.showIfNecessary(selectedNodes, false);
    548         } catch (UserCancelException e) {
    549             Logging.trace(e);
    550             return;
    551         }
     462        Map<Node, Node> replaced = new HashMap<>();
     463        List<Command> cmds = new ArrayList<>();
    552464
    553         List<Command> cmds = new LinkedList<>();
    554         List<Node> allNewNodes = new LinkedList<>();
    555         for (Node n : selectedNodes) {
    556             List<Node> newNodes = new LinkedList<>();
    557             tmpWay = modifyWay(n, tmpWay, cmds, newNodes);
    558             if (dialog != null) {
    559                 update(dialog, n, newNodes, cmds);
    560             }
    561             allNewNodes.addAll(newNodes);
     465        selectedNodes.forEach(n -> replaced.put(n, cloneNode(n, cmds)));
     466        List<Node> modNodes = selectedWay.getNodes().stream().map(n -> replaced.getOrDefault(n, n))
     467                .collect(Collectors.toList());
     468
     469        // only one changeCommand for a way, else garbage will happen
     470        addCheckedChangeNodesCmd(cmds, selectedWay, modNodes);
     471        if (dialog != null) {
     472            replaced.forEach((k,v) -> update(dialog, k, Collections.singletonList(v), cmds));
    562473        }
    563         // only one changeCommand for a way, else garbage will happen
    564         addCheckedChangeNodesCmd(cmds, selectedWay, tmpWay.getNodes());
    565474
    566475        UndoRedoHandler.getInstance().add(new SequenceCommand(
    567476                trn("Dupe {0} node into {1} nodes", "Dupe {0} nodes into {1} nodes",
    568                         selectedNodes.size(), selectedNodes.size(), selectedNodes.size()+allNewNodes.size()), cmds));
    569         getLayerManager().getEditDataSet().setSelected(allNewNodes);
     477                        selectedNodes.size(), selectedNodes.size(), 2 * selectedNodes.size()), cmds));
     478        getLayerManager().getEditDataSet().setSelected(replaced.values());
    570479    }
    571480
    572     private void addCheckedChangeNodesCmd(List<Command> cmds, Way w, List<Node> nodes) {
    573         boolean relationCheck = w.firstNode() != nodes.get(0) || w.lastNode() != nodes.get(nodes.size() - 1);
     481    private boolean addCheckedChangeNodesCmd(List<Command> cmds, Way w, List<Node> nodes) {
     482        boolean relationCheck = !calcAffectedRelations(Collections.singleton(w)).isEmpty();
    574483        cmds.add(new ChangeNodesCommand(w, nodes));
    575484        if (relationCheck) {
    576485            notifyWayPartOfRelation(Collections.singleton(w));
    577486        }
     487        return relationCheck;
    578488    }
    579489
    580490    @Override
     
    611521    }
    612522
    613523    protected void notifyWayPartOfRelation(final Collection<Way> ways) {
    614         final Set<Node> affectedNodes = (selectedNodes != null) ? selectedNodes : Collections.singleton(selectedNode);
    615         final Set<String> affectedRelations = new HashSet<>();
    616         for (Relation r: OsmPrimitive.getParentRelations(ways)) {
    617             if (!r.isUsable())
    618                 continue;
    619             // see #18670: suppress notification when well known restriction types are not affected
    620             if (r.hasTag("type", "restriction", "connectivity", "destination_sign") && !r.hasIncompleteMembers()) {
    621                 int count = 0;
    622                 for (RelationMember rm : r.getMembers()) {
    623                     if (rm.isNode() && affectedNodes.contains(rm.getNode()))
    624                         count++;
    625                     if (rm.isWay() && ways.contains(rm.getWay())) {
    626                         count++;
    627                         if ("via".equals(rm.getRole())) {
    628                             count++;
    629                         }
    630                     }
    631                 }
    632                 if (count < 2)
    633                     continue;
    634             }
    635             affectedRelations.add(r.getDisplayName(DefaultNameFormatter.getInstance()));
    636         }
     524        Set<Relation> affectedRelations = calcAffectedRelations(ways);
    637525        if (affectedRelations.isEmpty()) {
    638526            return;
    639527        }
    640 
    641528        final int size = affectedRelations.size();
    642529        final String msg1 = trn("Unglueing possibly affected {0} relation: {1}", "Unglueing possibly affected {0} relations: {1}",
    643                 size, size, Utils.joinAsHtmlUnorderedList(affectedRelations));
     530                size, size, DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(affectedRelations, 20));
    644531        final String msg2 = trn("Ensure that the relation has not been broken!", "Ensure that the relations have not been broken!",
    645532                size);
    646         new Notification("<html>" + msg1 + msg2).setIcon(JOptionPane.WARNING_MESSAGE).show();
     533        new Notification(msg1 + msg2).setIcon(JOptionPane.WARNING_MESSAGE).show();
    647534    }
     535
     536    protected Set<Relation> calcAffectedRelations(final Collection<Way> ways) {
     537        final Set<Node> affectedNodes = (selectedNodes != null) ? selectedNodes : Collections.singleton(selectedNode);
     538        return OsmPrimitive.getParentRelations(ways)
     539                .stream().filter(r -> isRelationAffected(r, affectedNodes, ways))
     540                .collect(Collectors.toSet());
     541    }
     542
     543    private static boolean isRelationAffected(Relation r, Set<Node> affectedNodes, Collection<Way> ways) {
     544        if (!r.isUsable())
     545            return false;
     546        // see #18670: suppress notification when well known restriction types are not affected
     547        if (!r.hasTag("type", "restriction", "connectivity", "destination_sign") || r.hasIncompleteMembers())
     548            return true;
     549        int count = 0;
     550        for (RelationMember rm : r.getMembers()) {
     551            if (rm.isNode() && affectedNodes.contains(rm.getNode()))
     552                count++;
     553            if (rm.isWay() && ways.contains(rm.getWay())) {
     554                count++;
     555                if ("via".equals(rm.getRole())) {
     556                    count++;
     557                }
     558            }
     559        }
     560        return count >= 2;
     561    }
    648562}