Index: src/org/openstreetmap/josm/actions/AlignInLineAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/AlignInLineAction.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/AlignInLineAction.java	(working copy)
@@ -25,6 +25,7 @@
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.gui.Notification;
 import org.openstreetmap.josm.tools.Logging;
@@ -172,7 +173,7 @@
             return;

         try {
-            Command cmd = buildCommand(getLayerManager().getEditDataSet());
+            Command<OsmPrimitive, Node, Way, Relation> cmd = buildCommand(getLayerManager().getEditDataSet());
             if (cmd != null) {
                 UndoRedoHandler.getInstance().add(cmd);
             }
@@ -191,7 +192,7 @@
      * @throws InvalidSelection if a polygon is selected, or if a node is used by 3 or more ways
      * @since 13108
      */
-    public Command buildCommand(DataSet ds) throws InvalidSelection {
+    public Command<OsmPrimitive, Node, Way, Relation> buildCommand(DataSet ds) throws InvalidSelection {
         List<Node> selectedNodes = new ArrayList<>(ds.getSelectedNodes());
         List<Way> selectedWays = new ArrayList<>(ds.getSelectedWays());
         selectedWays.removeIf(w -> w.isIncomplete() || w.isEmpty());
@@ -230,10 +231,10 @@
      * @return Command that perform action.
      * @throws InvalidSelection If the nodes have same coordinates.
      */
-    private static Command alignOnlyNodes(List<Node> nodes) throws InvalidSelection {
+    private static Command<OsmPrimitive, Node, Way, Relation> alignOnlyNodes(List<Node> nodes) throws InvalidSelection {
         // Choose nodes used as anchor points for projection.
         Node[] anchors = nodePairFurthestApart(nodes);
-        Collection<Command> cmds = new ArrayList<>(nodes.size());
+        Collection<Command<OsmPrimitive, Node, Way, Relation>> cmds = new ArrayList<>(nodes.size());
         Line line = new Line(anchors[0], anchors[1]);
         for (Node node: nodes) {
             if (node != anchors[0] && node != anchors[1])
@@ -248,7 +249,7 @@
      * @return Command that perform action
      * @throws InvalidSelection if a polygon is selected, or if a node is used by 3 or more ways
      */
-    private static Command alignMultiWay(Collection<Way> ways) throws InvalidSelection {
+    private static Command<OsmPrimitive, Node, Way, Relation> alignMultiWay(Collection<Way> ways) throws InvalidSelection {
         // Collect all nodes and compute line equation
         Set<Node> nodes = new HashSet<>();
         Map<Way, Line> lines = new HashMap<>();
@@ -263,7 +264,7 @@
         if (nodes.isEmpty()) {
             throw new InvalidSelection(tr("Intersection of three or more ways can not be solved. Abort."));
         }
-        Collection<Command> cmds = new ArrayList<>(nodes.size());
+        Collection<Command<OsmPrimitive, Node, Way, Relation>> cmds = new ArrayList<>(nodes.size());
         List<Way> referers = new ArrayList<>(ways.size());
         for (Node n: nodes) {
             referers.clear();
@@ -346,7 +347,7 @@
      * @return Command that perform action
      * @throws InvalidSelection if more than 2 lines
      */
-    private static Command alignSingleNode(Node node, List<Line> lines) throws InvalidSelection {
+    private static Command<OsmPrimitive, Node, Way, Relation> alignSingleNode(Node node, List<Line> lines) throws InvalidSelection {
         if (lines.size() == 1)
             return lines.get(0).projectionCommand(node);
         else if (lines.size() == 2)
@@ -404,9 +405,9 @@
          * @param n Node to be projected
          * @return The command that do the projection of this node
          */
-        public Command projectionCommand(Node n) {
+        public Command<OsmPrimitive, Node, Way, Relation> projectionCommand(Node n) {
             double s = (xM - n.getEastNorth().getX()) * a + (yM - n.getEastNorth().getY()) * b;
-            return new MoveCommand(n, a*s, b*s);
+            return new MoveCommand<>(n, a*s, b*s);
         }

         /**
@@ -416,7 +417,7 @@
          * @return The command that move the node
          * @throws InvalidSelection if two parallels ways found
          */
-        public Command intersectionCommand(Node n, Line other) throws InvalidSelection {
+        public Command<OsmPrimitive, Node, Way, Relation> intersectionCommand(Node n, Line other) throws InvalidSelection {
             double d = this.a * other.b - other.a * this.b;
             if (Math.abs(d) < 10e-6)
                 // parallels lines
@@ -423,7 +424,7 @@
                 throw new InvalidSelection(tr("Two parallels ways found. Abort."));
             double x = (this.b * other.c - other.b * this.c) / d;
             double y = (other.a * this.c - this.a * other.c) / d;
-            return new MoveCommand(n, x - n.getEastNorth().getX(), y - n.getEastNorth().getY());
+            return new MoveCommand<>(n, x - n.getEastNorth().getX(), y - n.getEastNorth().getY());
         }
     }

Index: src/org/openstreetmap/josm/actions/CreateCircleAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/CreateCircleAction.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/CreateCircleAction.java	(working copy)
@@ -25,6 +25,7 @@
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.projection.ProjectionRegistry;
 import org.openstreetmap.josm.gui.Notification;
@@ -199,7 +200,7 @@
                 numberOfNodesInCircle >= nodes.size() ? (numberOfNodesInCircle - nodes.size()) : 0);

         // now we can start doing things to OSM data
-        Collection<Command> cmds = new LinkedList<>();
+        Collection<Command<OsmPrimitive, Node, Way, Relation>> cmds = new LinkedList<>();

         // build a way for the circle
         List<Node> nodesToAdd = new ArrayList<>();
@@ -219,7 +220,7 @@
                 }
                 Node n = new Node(ll);
                 nodesToAdd.add(n);
-                cmds.add(new AddCommand(ds, n));
+                cmds.add(new AddCommand<>(ds, n));
             }
         }
         nodesToAdd.add(nodesToAdd.get(0)); // close the circle
@@ -231,11 +232,11 @@
         if (existingWay == null) {
             Way newWay = new Way();
             newWay.setNodes(nodesToAdd);
-            cmds.add(new AddCommand(ds, newWay));
+            cmds.add(new AddCommand<>(ds, newWay));
         } else {
             Way newWay = new Way(existingWay);
             newWay.setNodes(nodesToAdd);
-            cmds.add(new ChangeCommand(ds, existingWay, newWay));
+            cmds.add(new ChangeCommand<>(ds, existingWay, newWay));
         }

         UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Create Circle"), cmds));
Index: src/org/openstreetmap/josm/actions/CreateMultipolygonAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/CreateMultipolygonAction.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/CreateMultipolygonAction.java	(working copy)
@@ -32,6 +32,7 @@
 import org.openstreetmap.josm.data.UndoRedoHandler;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.OsmUtils;
 import org.openstreetmap.josm.data.osm.Relation;
@@ -109,7 +110,7 @@
             if (commandAndRelation == null) {
                 return;
             }
-            final Command command = commandAndRelation.a;
+            final Command<OsmPrimitive, Node, Way, Relation> command = commandAndRelation.a;
             final Relation relation = commandAndRelation.b;

             // to avoid EDT violations
@@ -329,14 +330,14 @@
         final Relation existingRelation = rr.a;
         final Relation relation = rr.b;

-        final List<Command> list = removeTagsFromWaysIfNeeded(relation);
+        final List<Command<OsmPrimitive, Node, Way, Relation>> list = removeTagsFromWaysIfNeeded(relation);
         final String commandName;
         if (existingRelation == null) {
-            list.add(new AddCommand(selectedWays.iterator().next().getDataSet(), relation));
+            list.add(new AddCommand<>(selectedWays.iterator().next().getDataSet(), relation));
             commandName = getName(false);
         } else {
             if (!unchanged) {
-                list.add(new ChangeCommand(existingRelation, relation));
+                list.add(new ChangeCommand<>(existingRelation, relation));
             }
             if (list.isEmpty()) {
                 if (unchanged) {
@@ -388,7 +389,7 @@
      * @param relation the multipolygon style relation to process
      * @return a list of commands to execute
      */
-    public static List<Command> removeTagsFromWaysIfNeeded(Relation relation) {
+    public static List<Command<OsmPrimitive, Node, Way, Relation>> removeTagsFromWaysIfNeeded(Relation relation) {
         Map<String, String> values = new HashMap<>(relation.getKeys());

         List<Way> innerWays = new ArrayList<>();
@@ -442,7 +443,7 @@

         values.put("area", OsmUtils.TRUE_VALUE);

-        List<Command> commands = new ArrayList<>();
+        List<Command<OsmPrimitive, Node, Way, Relation>> commands = new ArrayList<>();
         boolean moveTags = Config.getPref().getBoolean("multipoly.movetags", true);

         for (Entry<String, String> entry : values.entrySet()) {
@@ -490,7 +491,7 @@
                 if (ds == null) {
                     ds = MainApplication.getLayerManager().getEditDataSet();
                 }
-                commands.add(new ChangeCommand(ds, relation, r2));
+                commands.add(new ChangeCommand<>(ds, relation, r2));
             }
         }

Index: src/org/openstreetmap/josm/actions/DeleteAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/DeleteAction.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/DeleteAction.java	(working copy)
@@ -15,8 +15,9 @@

 import org.openstreetmap.josm.command.DeleteCommand.DeletionCallback;
 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.RelationToChildReference;
 import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
 import org.openstreetmap.josm.gui.MainApplication;
@@ -37,13 +38,13 @@
      */
     public static final DeletionCallback defaultDeletionCallback = new DeletionCallback() {
         @Override
-        public boolean checkAndConfirmOutlyingDelete(Collection<? extends OsmPrimitive> primitives,
-                Collection<? extends OsmPrimitive> ignore) {
+        public boolean checkAndConfirmOutlyingDelete(Collection<? extends IPrimitive> primitives,
+                Collection<? extends IPrimitive> ignore) {
             return DeleteAction.checkAndConfirmOutlyingDelete(primitives, ignore);
         }

         @Override
-        public boolean confirmRelationDeletion(Collection<Relation> relations) {
+        public boolean confirmRelationDeletion(Collection<? extends IRelation<?>> relations) {
             return DeleteAction.confirmRelationDeletion(relations);
         }

@@ -91,8 +92,8 @@
      * @return true, if operating on outlying primitives is OK; false, otherwise
      * @since 12749 (moved from DeleteCommand)
      */
-    public static boolean checkAndConfirmOutlyingDelete(Collection<? extends OsmPrimitive> primitives,
-            Collection<? extends OsmPrimitive> ignore) {
+    public static boolean checkAndConfirmOutlyingDelete(Collection<? extends IPrimitive> primitives,
+            Collection<? extends IPrimitive> ignore) {
         return checkAndConfirmOutlyingOperation("delete",
                 tr("Delete confirmation"),
                 tr("You are about to delete nodes outside of the area you have downloaded."
@@ -113,7 +114,7 @@
      * @return {@code true} if user confirms the deletion
      * @since 12760
      */
-    public static boolean confirmRelationDeletion(Collection<Relation> relations) {
+    public static boolean confirmRelationDeletion(Collection<? extends IRelation<?>> relations) {
         JPanel msg = new JPanel(new GridBagLayout());
         msg.add(new JMultilineLabel("<html>" + trn(
                 "You are about to delete {0} relation: {1}"
Index: src/org/openstreetmap/josm/actions/JoinAreasAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/JoinAreasAction.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/JoinAreasAction.java	(working copy)
@@ -57,8 +57,8 @@
  */
 public class JoinAreasAction extends JosmAction {
     // This will be used to commit commands and unite them into one large command sequence at the end
-    private final transient LinkedList<Command> executedCmds = new LinkedList<>();
-    private final transient LinkedList<Command> cmds = new LinkedList<>();
+    private final transient LinkedList<Command<OsmPrimitive, Node, Way, Relation>> executedCmds = new LinkedList<>();
+    private final transient LinkedList<Command<OsmPrimitive, Node, Way, Relation>> cmds = new LinkedList<>();
     private DataSet ds;
     private final transient List<Relation> addedRelations = new LinkedList<>();
     private final boolean addUndoRedo;
@@ -590,7 +590,7 @@
         cmds.clear();
         if (!executedCmds.isEmpty()) {
             // revert all executed commands
-            ds = executedCmds.getFirst().getAffectedDataSet();
+            ds = (DataSet) executedCmds.getFirst().getAffectedDataSet();
             ds.update(() -> {
                 while (!executedCmds.isEmpty()) {
                     executedCmds.removeLast().undoCommand();
@@ -1740,7 +1740,7 @@
     }

     private static class JoinAreaCommand extends SequenceCommand {
-        JoinAreaCommand(Collection<Command> sequenz) {
+        JoinAreaCommand(Collection<Command<OsmPrimitive, Node, Way, Relation>> sequenz) {
             super(tr("Joined overlapping areas"), sequenz, true);
             setSequenceComplete(true);
         }
@@ -1747,12 +1747,12 @@

         @Override
         public void undoCommand() {
-            getAffectedDataSet().update(super::undoCommand);
+            ((DataSet) getAffectedDataSet()).update(super::undoCommand);
         }

         @Override
         public boolean executeCommand() {
-            return getAffectedDataSet().update(super::executeCommand);
+            return ((DataSet) getAffectedDataSet()).update(super::executeCommand);
         }
     }
 }
Index: src/org/openstreetmap/josm/actions/JoinNodeWayAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/JoinNodeWayAction.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/JoinNodeWayAction.java	(working copy)
@@ -29,8 +29,10 @@
 import org.openstreetmap.josm.data.UndoRedoHandler;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.IPrimitive;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.WaySegment;
 import org.openstreetmap.josm.data.projection.ProjectionRegistry;
@@ -92,7 +94,7 @@
             return;
         DataSet ds = getLayerManager().getEditDataSet();
         Collection<Node> selectedNodes = ds.getSelectedNodes();
-        Collection<Command> cmds = new LinkedList<>();
+        Collection<Command<OsmPrimitive, Node, Way, Relation>> cmds = new LinkedList<>();
         Map<Way, MultiMap<Integer, Node>> data = new LinkedHashMap<>();

         // If the user has selected some ways, only join the node to these.
@@ -101,11 +103,11 @@
         // Planning phase: decide where we'll insert the nodes and put it all in "data"
         MapView mapView = MainApplication.getMap().mapView;
         for (Node node : selectedNodes) {
-            List<WaySegment> wss = mapView.getNearestWaySegments(mapView.getPoint(node), OsmPrimitive::isSelectable);
+            List<WaySegment<Node, Way>> wss = mapView.getNearestWaySegments(mapView.getPoint(node), IPrimitive::isSelectable);
             // we cannot trust the order of elements in wss because it was calculated based on rounded position value of node
-            TreeMap<Double, List<WaySegment>> nearestMap = new TreeMap<>();
+            TreeMap<Double, List<WaySegment<Node, Way>>> nearestMap = new TreeMap<>();
             EastNorth en = node.getEastNorth();
-            for (WaySegment ws : wss) {
+            for (WaySegment<Node, Way> ws : wss) {
                 // Maybe cleaner to pass a "isSelected" predicate to getNearestWaySegments, but this is less invasive.
                 if (restrictToSelectedWays && !ws.way.isSelected()) {
                     continue;
@@ -119,19 +121,19 @@
                         ws.getSecondNode().getEastNorth(), en));
                 // resolution in numbers with large exponent not needed here..
                 distSq = Double.longBitsToDouble(Double.doubleToLongBits(distSq) >> 32 << 32);
-                List<WaySegment> wslist = nearestMap.computeIfAbsent(distSq, k -> new LinkedList<>());
+                List<WaySegment<Node, Way>> wslist = nearestMap.computeIfAbsent(distSq, k -> new LinkedList<>());
                 wslist.add(ws);
             }
             Set<Way> seenWays = new HashSet<>();
             Double usedDist = null;
             while (!nearestMap.isEmpty()) {
-                Entry<Double, List<WaySegment>> entry = nearestMap.pollFirstEntry();
+                Entry<Double, List<WaySegment<Node, Way>>> entry = nearestMap.pollFirstEntry();
                 if (usedDist != null) {
                     double delta = entry.getKey() - usedDist;
                     if (delta > 1e-4)
                         break;
                 }
-                for (WaySegment ws : entry.getValue()) {
+                for (WaySegment<Node, Way> ws : entry.getValue()) {
                     // only use the closest WaySegment of each way and ignore those that already contain the node
                     if (!ws.getFirstNode().equals(node) && !ws.getSecondNode().equals(node)
                             && !seenWays.contains(ws.way)) {
@@ -179,7 +181,7 @@
                             }
                             continue;
                         }
-                        MoveCommand c = new MoveCommand(node,
+                        MoveCommand<OsmPrimitive, Node, Way, Relation> c = new MoveCommand<>(node,
                                 ProjectionRegistry.getProjection().eastNorth2latlon(newPosition));
                         cmds.add(c);
                         movedNodes.put(node, newPosition);
@@ -193,11 +195,11 @@
             }
             Way wnew = new Way(w);
             wnew.setNodes(wayNodes);
-            cmds.add(new ChangeCommand(ds, w, wnew));
+            cmds.add(new ChangeCommand<>(ds, w, wnew));
         }

         if (cmds.isEmpty()) return;
-        UndoRedoHandler.getInstance().add(new SequenceCommand(getValue(NAME).toString(), cmds));
+        UndoRedoHandler.getInstance().add(new SequenceCommand<>(getValue(NAME).toString(), cmds));
     }

     /**
Index: src/org/openstreetmap/josm/actions/JosmAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/JosmAction.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/JosmAction.java	(working copy)
@@ -18,6 +18,7 @@
 import org.openstreetmap.josm.command.Command;
 import org.openstreetmap.josm.data.osm.DataSelectionListener;
 import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.IPrimitive;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.OsmUtils;
 import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
@@ -507,8 +508,8 @@
      */
     public static boolean checkAndConfirmOutlyingOperation(String operation,
             String dialogTitle, String outsideDialogMessage, String incompleteDialogMessage,
-            Collection<? extends OsmPrimitive> primitives,
-            Collection<? extends OsmPrimitive> ignore) {
+            Collection<? extends IPrimitive> primitives,
+            Collection<? extends IPrimitive> ignore) {
         int checkRes = Command.checkOutlyingOrIncompleteOperation(primitives, ignore);
         if ((checkRes & Command.IS_OUTSIDE) != 0) {
             JPanel msg = new JPanel(new GridBagLayout());
Index: src/org/openstreetmap/josm/actions/MergeNodesAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/MergeNodesAction.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/MergeNodesAction.java	(working copy)
@@ -29,6 +29,8 @@
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.OsmUtils;
@@ -47,6 +49,7 @@
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Shortcut;
 import org.openstreetmap.josm.tools.UserCancelException;
+import org.openstreetmap.josm.tools.Utils;

 /**
  * Merges a collection of nodes into one node.
@@ -81,8 +84,8 @@

         if (selectedNodes.size() == 1) {
             MapView mapView = MainApplication.getMap().mapView;
-            List<Node> nearestNodes = mapView.getNearestNodes(
-                    mapView.getPoint(selectedNodes.get(0)), selectedNodes, OsmPrimitive::isUsable);
+            List<INode> nearestNodes = mapView.getNearestNodes(
+                    mapView.getPoint(selectedNodes.get(0)), new ArrayList<>(selectedNodes), IPrimitive::isUsable);
             if (nearestNodes.isEmpty()) {
                 new Notification(
                         tr("Please select at least two nodes to merge or one node that is close to another node."))
@@ -90,7 +93,7 @@
                         .show();
                 return;
             }
-            selectedNodes.addAll(nearestNodes);
+            nearestNodes.forEach(n -> selectedNodes.add((Node) n));
         }

         Node targetNode = selectTargetNode(selectedNodes);
@@ -169,11 +172,11 @@
      * @param candidates the collection of candidate nodes
      * @return the selected target node
      */
-    public static Node selectTargetNode(Collection<Node> candidates) {
-        Node oldestNode = null;
-        Node targetNode = null;
-        Node lastNode = null;
-        for (Node n : candidates) {
+    public static <N extends INode> N selectTargetNode(Collection<N> candidates) {
+        N oldestNode = null;
+        N targetNode = null;
+        N lastNode = null;
+        for (N n : candidates) {
             if (!n.isNew()) {
                 // Among existing nodes, try to keep the oldest used one
                 if (!n.getReferrers().isEmpty()) {
@@ -292,13 +295,13 @@
      * @throws IllegalArgumentException if {@code layer} is null
      * @since 12689
      */
-    public static Command mergeNodes(Collection<Node> nodes, Node targetLocationNode) {
+    public static <N extends INode> Command<?, N, ?, ?> mergeNodes(Collection<N> nodes, N targetLocationNode) {
         if (nodes == null) {
             return null;
         }
-        Set<Node> allNodes = new HashSet<>(nodes);
+        Set<N> allNodes = new HashSet<>(nodes);
         allNodes.add(targetLocationNode);
-        Node targetNode = selectTargetNode(allNodes);
+        N targetNode = selectTargetNode(allNodes);
         if (targetNode == null) {
             return null;
         }
@@ -314,7 +317,7 @@
      * @return The command necessary to run in order to perform action, or {@code null} if there is nothing to do
      * @throws IllegalArgumentException if layer is null
      */
-    public static Command mergeNodes(Collection<Node> nodes, Node targetNode, Node targetLocationNode) {
+    public static <N extends INode> Command<?, N, ?, ?> mergeNodes(Collection<N> nodes, N targetNode, N targetLocationNode) {
         CheckParameterUtil.ensureParameterNotNull(targetNode, "targetNode");
         if (nodes == null) {
             return null;
@@ -325,16 +328,16 @@

             // the nodes we will have to delete
             //
-            Collection<Node> nodesToDelete = new HashSet<>(nodes);
+            Collection<N> nodesToDelete = new HashSet<>(nodes);
             nodesToDelete.remove(targetNode);

             // fix the ways referring to at least one of the merged nodes
             //
-            List<Command> wayFixCommands = fixParentWays(nodesToDelete, targetNode);
+            List<Command<?, N, ?, ?>> wayFixCommands = fixParentWays(nodesToDelete, targetNode);
             if (wayFixCommands == null) {
                 return null;
             }
-            List<Command> cmds = new LinkedList<>(wayFixCommands);
+            List<Command<?, N, ?, ?>> cmds = new LinkedList<>(wayFixCommands);

             // build the commands
             //
@@ -341,14 +344,14 @@
             if (!targetNode.equals(targetLocationNode)) {
                 LatLon targetLocationCoor = targetLocationNode.getCoor();
                 if (!Objects.equals(targetNode.getCoor(), targetLocationCoor)) {
-                    Node newTargetNode = new Node(targetNode);
+                    N newTargetNode = Utils.clone(targetNode);
                     newTargetNode.setCoor(targetLocationCoor);
-                    cmds.add(new ChangeCommand(targetNode, newTargetNode));
+                    cmds.add(new ChangeCommand<>(targetNode, newTargetNode));
                 }
             }
             cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(nodeTags, nodes, Collections.singleton(targetNode)));
             if (!nodesToDelete.isEmpty()) {
-                cmds.add(new DeleteCommand(nodesToDelete));
+                cmds.add(new DeleteCommand<>(nodesToDelete));
             }
             if (cmds.size() == 1)
                 return cmds.get(0);
Index: src/org/openstreetmap/josm/actions/mapmode/DeleteAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/mapmode/DeleteAction.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/mapmode/DeleteAction.java	(working copy)
@@ -18,8 +18,8 @@
 import org.openstreetmap.josm.command.DeleteCommand;
 import org.openstreetmap.josm.data.UndoRedoHandler;
 import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.WaySegment;
 import org.openstreetmap.josm.gui.MainApplication;
@@ -90,7 +90,7 @@

     private static class DeleteParameters {
         private DeleteMode mode;
-        private Node nearestNode;
+        private INode nearestNode;
         private WaySegment nearestSegment;
     }

@@ -195,7 +195,7 @@
      * handles everything related to highlighting primitives and way
      * segments for the given pointer position (via MouseEvent) and modifiers.
      * @param e current mouse event
-     * @param modifiers extended mouse modifiers, not necessarly taken from the given mouse event
+     * @param modifiers extended mouse modifiers, not necessarily taken from the given mouse event
      */
     private void addHighlighting(MouseEvent e, int modifiers) {
         if (!drawTargetHighlight)
@@ -212,11 +212,11 @@
             // silent operation and SplitWayAction will show dialogs. A lot.
             Command delCmd = buildDeleteCommands(e, modifiers, true);
             // all other cases delete OsmPrimitives directly, so we can safely do the following
-            repaintIfRequired(delCmd == null ? Collections.emptySet() : new HashSet<>(delCmd.getParticipatingPrimitives()), null);
+            repaintIfRequired(delCmd == null ? Collections.emptySet() : new HashSet<IPrimitive>(delCmd.getParticipatingPrimitives()), null);
         }
     }

-    private void repaintIfRequired(Set<OsmPrimitive> newHighlights, WaySegment newHighlightedWaySegment) {
+    private void repaintIfRequired(Set<IPrimitive> newHighlights, WaySegment newHighlightedWaySegment) {
         boolean needsRepaint = false;
         OsmDataLayer editLayer = getLayerManager().getEditLayer();

@@ -364,9 +364,9 @@
         DeleteParameters result = new DeleteParameters();

         MapView mapView = MainApplication.getMap().mapView;
-        result.nearestNode = mapView.getNearestNode(e.getPoint(), OsmPrimitive::isSelectable);
+        result.nearestNode = mapView.getNearestNode(e.getPoint(), IPrimitive::isSelectable);
         if (result.nearestNode == null) {
-            result.nearestSegment = mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable);
+            result.nearestSegment = mapView.getNearestWaySegment(e.getPoint(), IPrimitive::isSelectable);
             if (result.nearestSegment != null) {
                 if (shift) {
                     result.mode = DeleteMode.segment;
Index: src/org/openstreetmap/josm/actions/mapmode/ExtrudeAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/mapmode/ExtrudeAction.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/mapmode/ExtrudeAction.java	(working copy)
@@ -39,6 +39,10 @@
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
 import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Way;
@@ -78,8 +82,8 @@
     private boolean nodeDragWithoutCtrl;

     private long mouseDownTime;
-    private transient WaySegment selectedSegment;
-    private transient Node selectedNode;
+    private transient WaySegment<INode, IWay<INode>> selectedSegment;
+    private transient INode selectedNode;
     private Color mainColor;
     private transient Stroke mainStroke;

@@ -106,7 +110,7 @@
     /**
      * Collection of nodes that is moved
      */
-    private transient List<Node> movingNodeList;
+    private transient List<INode> movingNodeList;

     /**
      * The direction that is currently active.
@@ -141,12 +145,12 @@
     /**
      * the command that performed last move.
      */
-    private transient MoveCommand moveCommand;
+    private transient MoveCommand<?, ?, ?, ?> moveCommand;
     /**
      *  The command used for dual alignment movement.
      *  Needs to be separate, due to two nodes moving in different directions.
      */
-    private transient MoveCommand moveCommand2;
+    private transient MoveCommand<?, ?, ?, ?> moveCommand2;

     /** The cursor for the 'create_new' mode. */
     private final Cursor cursorCreateNew;
@@ -392,8 +396,8 @@
         requestFocusInMapView();
         updateKeyModifiers(e);

-        selectedNode = map.mapView.getNearestNode(e.getPoint(), OsmPrimitive::isSelectable);
-        selectedSegment = map.mapView.getNearestWaySegment(e.getPoint(), OsmPrimitive::isSelectable);
+        selectedNode = map.mapView.getNearestNode(e.getPoint(), IPrimitive::isSelectable);
+        selectedSegment = map.mapView.getNearestWaySegment(e.getPoint(), IPrimitive::isSelectable);

         // If nothing gets caught, stay in select mode
         if (selectedSegment == null && selectedNode == null) return;
@@ -488,9 +492,11 @@
                     // move nodes to new position
                     if (moveCommand == null || moveCommand2 == null) {
                         // make a new move commands
-                        moveCommand = new MoveCommand(movingNodeList.get(0), movement1.getX(), movement1.getY());
-                        moveCommand2 = new MoveCommand(movingNodeList.get(1), movement2.getX(), movement2.getY());
-                        Command c = new SequenceCommand(tr("Extrude Way"), moveCommand, moveCommand2);
+                        MoveCommand<IPrimitive, INode, IWay<INode>, IRelation<?>> tCommand = new MoveCommand<>(movingNodeList.get(0), movement1.getX(), movement1.getY());
+                        MoveCommand<IPrimitive, INode, IWay<INode>, IRelation<?>> tCommand2 = new MoveCommand<>(movingNodeList.get(1), movement2.getX(), movement2.getY());
+                        moveCommand = tCommand;
+                        moveCommand2 = tCommand2;
+                        Command<?, ?, ?, ?> c = new SequenceCommand<>(tr("Extrude Way"), tCommand, tCommand2);
                         UndoRedoHandler.getInstance().add(c);
                     } else {
                         // reuse existing move commands
@@ -505,7 +511,7 @@
                     //move nodes to new position
                     if (moveCommand == null) {
                         //make a new move command
-                        moveCommand = new MoveCommand(new ArrayList<OsmPrimitive>(movingNodeList), bestMovement);
+                        moveCommand = new MoveCommand<>(new ArrayList<IPrimitive>(movingNodeList), bestMovement);
                         UndoRedoHandler.getInstance().add(moveCommand);
                     } else {
                         //reuse existing move command
Index: src/org/openstreetmap/josm/actions/mapmode/ImproveWayAccuracyAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/mapmode/ImproveWayAccuracyAction.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/mapmode/ImproveWayAccuracyAction.java	(working copy)
@@ -35,6 +35,7 @@
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.WaySegment;
 import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
@@ -88,7 +89,7 @@

     private transient Way targetWay;
     private transient Node candidateNode;
-    private transient WaySegment candidateSegment;
+    private transient WaySegment<Node, Way> candidateSegment;

     private Point mousePos;
     private boolean dragging;
@@ -437,7 +438,7 @@

             if (ctrl && !alt && candidateSegment != null) {
                 // Add a new node to the highlighted segment.
-                Collection<WaySegment> virtualSegments = new LinkedList<>();
+                Collection<WaySegment<Node, Way>> virtualSegments = new LinkedList<>();

                 // Check if other ways have the same segment.
                 // We have to make sure that we add the new node to all of them.
@@ -445,7 +446,7 @@
                 commonParentWays.retainAll(candidateSegment.getSecondNode().getParentWays());
                 for (Way w : commonParentWays) {
                     for (int i = 0; i < w.getNodesCount() - 1; i++) {
-                        WaySegment testWS = new WaySegment(w, i);
+                        WaySegment<Node, Way> testWS = new WaySegment<>(w, i);
                         if (testWS.isSimilar(candidateSegment)) {
                             virtualSegments.add(testWS);
                         }
@@ -452,17 +453,17 @@
                     }
                 }

-                Collection<Command> virtualCmds = new LinkedList<>();
+                Collection<Command<OsmPrimitive, Node, Way, Relation>> virtualCmds = new LinkedList<>();
                 // Create the new node
                 Node virtualNode = new Node(mv.getEastNorth(mousePos.x, mousePos.y));
-                virtualCmds.add(new AddCommand(ds, virtualNode));
+                virtualCmds.add(new AddCommand<>(ds, virtualNode));

                 // Adding the node to all segments found
-                for (WaySegment virtualSegment : virtualSegments) {
+                for (WaySegment<Node, Way> virtualSegment : virtualSegments) {
                     Way w = virtualSegment.way;
                     Way wnew = new Way(w);
                     wnew.addNode(virtualSegment.lowerIndex + 1, virtualNode);
-                    virtualCmds.add(new ChangeCommand(w, wnew));
+                    virtualCmds.add(new ChangeCommand<>(w, wnew));
                 }

                 // Finishing the sequence command
@@ -485,12 +486,12 @@
                     nodes.remove(candidateNode);
                     newWay.setNodes(nodes);
                     if (nodes.size() < 2) {
-                        final Command deleteCmd = DeleteCommand.delete(Collections.singleton(targetWay), true);
+                        final Command<OsmPrimitive, Node, Way, Relation> deleteCmd = DeleteCommand.delete(Collections.singleton(targetWay), true);
                         if (deleteCmd != null) {
                             UndoRedoHandler.getInstance().add(deleteCmd);
                         }
                     } else {
-                        UndoRedoHandler.getInstance().add(new ChangeCommand(targetWay, newWay));
+                        UndoRedoHandler.getInstance().add(new ChangeCommand<>(targetWay, newWay));
                     }
                 } else if (candidateNode.isTagged()) {
                     JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
@@ -497,7 +498,7 @@
                             tr("Cannot delete node that has tags"),
                             tr("Error"), JOptionPane.ERROR_MESSAGE);
                 } else {
-                    final Command deleteCmd = DeleteCommand.delete(Collections.singleton(candidateNode), true);
+                    final Command<OsmPrimitive, Node, Way, Relation> deleteCmd = DeleteCommand.delete(Collections.singleton(candidateNode), true);
                     if (deleteCmd != null) {
                         UndoRedoHandler.getInstance().add(deleteCmd);
                     }
@@ -509,7 +510,7 @@
                 EastNorth cursorEN = mv.getEastNorth(mousePos.x, mousePos.y);

                 UndoRedoHandler.getInstance().add(
-                        new MoveCommand(candidateNode, cursorEN.east() - nodeEN.east(), cursorEN.north() - nodeEN.north()));
+                        new MoveCommand<>(candidateNode, cursorEN.east() - nodeEN.east(), cursorEN.north() - nodeEN.north()));
             }
         }

Index: src/org/openstreetmap/josm/actions/mapmode/ImproveWayAccuracyHelper.java
===================================================================
--- src/org/openstreetmap/josm/actions/mapmode/ImproveWayAccuracyHelper.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/mapmode/ImproveWayAccuracyHelper.java	(working copy)
@@ -6,8 +6,9 @@
 import java.util.List;

 import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
 import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.WaySegment;
 import org.openstreetmap.josm.gui.MainApplication;
@@ -40,12 +41,12 @@
             return null;
         }

-        Node node = mv.getNearestNode(p, OsmPrimitive::isSelectable);
+        INode node = mv.getNearestNode(p, IPrimitive::isSelectable);
         Way candidate = null;

         if (node != null) {
-            final Collection<OsmPrimitive> candidates = node.getReferrers();
-            for (OsmPrimitive refferer : candidates) {
+            final Collection<IPrimitive> candidates = (Collection<IPrimitive>) node.getReferrers();
+            for (IPrimitive refferer : candidates) {
                 if (refferer instanceof Way) {
                     candidate = (Way) refferer;
                     break;
@@ -56,7 +57,7 @@
             }
         }

-        return MainApplication.getMap().mapView.getNearestWay(p, OsmPrimitive::isSelectable);
+        return MainApplication.getMap().mapView.getNearestWay(p, IPrimitive::isSelectable);
     }

     /**
@@ -125,7 +126,7 @@
      * @param p the cursor position
      * @return nearest way segment to cursor
      */
-    public static WaySegment findCandidateSegment(MapView mv, Way w, Point p) {
+    public static WaySegment<Node, Way> findCandidateSegment(MapView mv, Way w, Point p) {
         if (mv == null || w == null || p == null) {
             return null;
         }
@@ -172,6 +173,6 @@
             }

         }
-        return candidate != -1 ? new WaySegment(w, candidate) : null;
+        return candidate != -1 ? new WaySegment<Node, Way>(w, candidate) : null;
     }
 }
Index: src/org/openstreetmap/josm/actions/mapmode/SelectAction.java
===================================================================
--- src/org/openstreetmap/josm/actions/mapmode/SelectAction.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/mapmode/SelectAction.java	(working copy)
@@ -11,6 +11,7 @@
 import java.awt.event.KeyEvent;
 import java.awt.event.MouseEvent;
 import java.awt.geom.Point2D;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
@@ -32,9 +33,14 @@
 import org.openstreetmap.josm.data.UndoRedoHandler;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.WaySegment;
 import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
@@ -183,7 +189,7 @@
      * to remove the highlight from them again as otherwise the whole data
      * set would have to be checked.
      */
-    private transient Optional<OsmPrimitive> currentHighlight = Optional.empty();
+    private transient Optional<IPrimitive> currentHighlight = Optional.empty();

     /**
      * Create a new SelectAction
@@ -257,13 +263,13 @@
      * @return {@code true} if repaint is required
      */
     private boolean giveUserFeedback(MouseEvent e, int modifiers) {
-        Optional<OsmPrimitive> c = Optional.ofNullable(
+        Optional<IPrimitive> c = Optional.ofNullable(
                 mv.getNearestNodeOrWay(e.getPoint(), mv.isSelectablePredicate, true));

         updateKeyModifiersEx(modifiers);
         determineMapMode(c.isPresent());

-        Optional<OsmPrimitive> newHighlight = Optional.empty();
+        Optional<IPrimitive> newHighlight = Optional.empty();

         virtualManager.clear();
         if (mode == Mode.MOVE && !dragInProgress() && virtualManager.activateVirtualNodeNearPoint(e.getPoint())) {
@@ -300,7 +306,7 @@
      * @param nearbyStuff primitives near the cursor
      * @return the cursor that should be displayed
      */
-    private Cursor getCursor(OsmPrimitive nearbyStuff) {
+    private Cursor getCursor(IPrimitive nearbyStuff) {
         String c = "rect";
         switch(mode) {
         case MOVE:
@@ -308,7 +314,7 @@
                 c = "virtual_node";
                 break;
             }
-            final OsmPrimitive osm = nearbyStuff;
+            final IPrimitive osm = nearbyStuff;

             if (dragInProgress()) {
                 // only consider merge if ctrl is pressed and there are nodes in
@@ -319,13 +325,13 @@
                 }
                 // only show merge to node cursor if nearby node and that node is currently
                 // not being dragged
-                final boolean hasTarget = osm instanceof Node && !osm.isSelected();
+                final boolean hasTarget = osm instanceof INode && !osm.isSelected();
                 c = hasTarget ? "merge_to_node" : "merge";
                 break;
             }

-            c = (osm instanceof Node) ? "node" : c;
-            c = (osm instanceof Way) ? "way" : c;
+            c = (osm instanceof INode) ? "node" : c;
+            c = (osm instanceof IWay) ? "way" : c;
             if (shift) {
                 c += "_add";
             } else if (ctrl) {
@@ -369,7 +375,7 @@
         return true;
     }

-    private boolean repaintIfRequired(Optional<OsmPrimitive> newHighlight) {
+    private boolean repaintIfRequired(Optional<IPrimitive> newHighlight) {
         if (!drawTargetHighlight || currentHighlight.equals(newHighlight))
             return false;
         currentHighlight.ifPresent(osm -> osm.setHighlighted(false));
@@ -413,7 +419,7 @@

         // primitives under cursor are stored in c collection

-        OsmPrimitive nearestPrimitive = mv.getNearestNodeOrWay(e.getPoint(), mv.isSelectablePredicate, true);
+        IPrimitive nearestPrimitive = mv.getNearestNodeOrWay(e.getPoint(), mv.isSelectablePredicate, true);

         determineMapMode(nearestPrimitive != null);

@@ -437,7 +443,7 @@
             if (!cancelDrawMode && nearestPrimitive instanceof Way) {
                 virtualManager.activateVirtualNodeNearPoint(e.getPoint());
             }
-            OsmPrimitive toSelect = cycleManager.cycleSetup(nearestPrimitive, e.getPoint());
+            IPrimitive toSelect = cycleManager.cycleSetup(nearestPrimitive, e.getPoint());
             selectPrims(asColl(toSelect), false, false);
             useLastMoveCommandIfPossible();
             // Schedule a timer to update status line "initialMoveDelay+1" ms in the future
@@ -510,7 +516,7 @@
             // If ctrl is pressed we are in merge mode. Look for a nearby node,
             // highlight it and adjust the cursor accordingly.
             final boolean canMerge = ctrl && !getLayerManager().getEditDataSet().getSelectedNodes().isEmpty();
-            final OsmPrimitive p = canMerge ? findNodeToMergeTo(e.getPoint()) : null;
+            final IPrimitive p = canMerge ? findNodeToMergeTo(e.getPoint()) : null;
             boolean needsRepaint = removeHighlighting();
             if (p != null) {
                 p.setHighlighted(true);
@@ -603,8 +609,8 @@
                     selectPrims(cycleManager.cyclePrims(), true, false);

                     // If the user double-clicked a node, change to draw mode
-                    Collection<OsmPrimitive> c = ds.getSelected();
-                    if (e.getClickCount() >= 2 && c.size() == 1 && c.iterator().next() instanceof Node) {
+                    Collection<IPrimitive> c = new ArrayList<>(ds.getSelected());
+                    if (e.getClickCount() >= 2 && c.size() == 1 && c.iterator().next() instanceof INode) {
                         // We need to do it like this as otherwise drawAction will see a double
                         // click and switch back to SelectMode
                         MainApplication.worker.execute(() -> map.selectDrawTool(true));
@@ -698,12 +704,12 @@
         // Currently we support only transformations which do not affect relations.
         // So don't add them in the first place to make handling easier
         DataSet ds = getLayerManager().getEditDataSet();
-        Collection<OsmPrimitive> selection = ds.getSelectedNodesAndWays();
+        Collection<IPrimitive> selection = new ArrayList<>(ds.getSelectedNodesAndWays());
         if (selection.isEmpty()) { // if nothing was selected to drag, just select nearest node/way to the cursor
             ds.setSelected(mv.getNearestNodeOrWay(mv.getPoint(startEN), mv.isSelectablePredicate, true));
         }

-        Collection<Node> affectedNodes = AllNodesVisitor.getAllNodes(selection);
+        Collection<INode> affectedNodes = new ArrayList<>(AllNodesVisitor.getAllNodes(selection));
         // for these transformations, having only one node makes no sense - quit silently
         if (affectedNodes.size() < 2 && (mode == Mode.ROTATE || mode == Mode.SCALE)) {
             return false;
@@ -721,7 +727,7 @@
                     moveCmd = new MoveCommand(selection, startEN, currentEN);
                     UndoRedoHandler.getInstance().add(moveCmd);
                 }
-                for (Node n : affectedNodes) {
+                for (INode n : affectedNodes) {
                     if (n.isOutSideWorld()) {
                         // Revert move
                         if (moveCmd != null) {
@@ -761,18 +767,19 @@
                     }
                 }

-                Collection<Way> ways = ds.getSelectedWays();
+                Collection<IWay<?>> ways = new ArrayList<>(ds.getSelectedWays());
                 if (doesImpactStatusLine(affectedNodes, ways)) {
                     MainApplication.getMap().statusLine.setDist(ways);
                 }
+                Logging.getLastErrorAndWarnings();
                 return true;
             });
         }
     }

-    private static boolean doesImpactStatusLine(Collection<Node> affectedNodes, Collection<Way> selectedWays) {
-        for (Way w : selectedWays) {
-            for (Node n : w.getNodes()) {
+    private static boolean doesImpactStatusLine(Collection<INode> affectedNodes, Collection<IWay<?>> selectedWays) {
+        for (IWay<?> w : selectedWays) {
+            for (INode n : w.getNodes()) {
                 if (affectedNodes.contains(n)) {
                     return true;
                 }
@@ -791,7 +798,7 @@
             return;
         }
         Command c = getLastCommandInDataset(dataSet);
-        Collection<Node> affectedNodes = AllNodesVisitor.getAllNodes(dataSet.getSelected());
+        Collection<? extends INode> affectedNodes = AllNodesVisitor.getAllNodes(dataSet.getSelected());
         if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand) c).getParticipatingPrimitives())) {
             // old command was created with different base point of movement, we need to recalculate it
             ((MoveCommand) c).changeStartPoint(startEN);
@@ -803,10 +810,10 @@
      * @param ds The data set the command needs to be in.
      * @return last command
      */
-    private static Command getLastCommandInDataset(DataSet ds) {
-        Command lastCommand = UndoRedoHandler.getInstance().getLastCommand();
+    private static <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> Command<O, N, W, R> getLastCommandInDataset(OsmData<O, N, W, R> ds) {
+        Command<O, N, W, R> lastCommand = UndoRedoHandler.getInstance().getLastCommand();
         if (lastCommand instanceof SequenceCommand) {
-            lastCommand = ((SequenceCommand) lastCommand).getLastCommand();
+            lastCommand = (Command<O, N, W, R>) ((SequenceCommand) lastCommand).getLastCommand();
         }
         if (lastCommand != null && ds.equals(lastCommand.getAffectedDataSet())) {
             return lastCommand;
@@ -830,13 +837,13 @@
             ed.toggleEnable("movedHiddenElements");
             showConfirmMoveDialog(ed);
         }
-        Set<Node> nodes = new HashSet<>();
+        Set<INode> nodes = new HashSet<>();
         int max = Config.getPref().getInt("warn.move.maxelements", 20);
-        for (OsmPrimitive osm : getLayerManager().getEditDataSet().getSelected()) {
-            if (osm instanceof Way) {
-                nodes.addAll(((Way) osm).getNodes());
-            } else if (osm instanceof Node) {
-                nodes.add((Node) osm);
+        for (IPrimitive osm : getLayerManager().getEditDataSet().getSelected()) {
+            if (osm instanceof IWay) {
+                nodes.addAll(((IWay<?>) osm).getNodes());
+            } else if (osm instanceof INode) {
+                nodes.add((INode) osm);
             }
             if (nodes.size() > max) {
                 break;
@@ -875,14 +882,14 @@
     }

     private boolean movesHiddenWay() {
-        DataSet ds = getLayerManager().getEditDataSet();
-        final Collection<Node> elementsToTest = new HashSet<>(ds.getSelectedNodes());
-        for (Way osm : ds.getSelectedWays()) {
+        DataSet ds = getLayerManager().getEditDataSet(); // TODO replace
+        final Collection<INode> elementsToTest = new HashSet<>(ds.getSelectedNodes());
+        for (IWay<?> osm : ds.getSelectedWays()) {
             elementsToTest.addAll(osm.getNodes());
         }
         return elementsToTest.stream()
-                .flatMap(n -> n.referrers(Way.class))
-                .anyMatch(Way::isDisabledAndHidden);
+                .flatMap(n -> n.getReferrers().stream().filter(IWay.class::isInstance).map(IWay.class::cast))
+                .anyMatch(IWay::isDisabledAndHidden);
     }

     /**
@@ -897,18 +904,18 @@
         if (selNodes.isEmpty())
             return;

-        Node target = findNodeToMergeTo(p);
+        INode target = findNodeToMergeTo(p);
         if (target == null)
             return;

         if (selNodes.size() == 1) {
             // Move all selected primitive to preserve shape #10748
-            Collection<OsmPrimitive> selection = ds.getSelectedNodesAndWays();
-            Collection<Node> affectedNodes = AllNodesVisitor.getAllNodes(selection);
+            Collection<IPrimitive> selection = new HashSet<>(ds.getSelectedNodesAndWays());
+            Collection<INode> affectedNodes = AllNodesVisitor.getAllNodes(selection);
             Command c = getLastCommandInDataset(ds);
             ds.update(() -> {
                 if (c instanceof MoveCommand && affectedNodes.equals(((MoveCommand) c).getParticipatingPrimitives())) {
-                    Node selectedNode = selNodes.iterator().next();
+                    INode selectedNode = selNodes.iterator().next();
                     EastNorth selectedEN = selectedNode.getEastNorth();
                     EastNorth targetEN = target.getEastNorth();
                     ((MoveCommand) c).moveAgain(targetEN.getX() - selectedEN.getX(),
@@ -917,7 +924,7 @@
             });
         }

-        Collection<Node> nodesToMerge = new LinkedList<>(selNodes);
+        Collection<INode> nodesToMerge = new LinkedList<>(selNodes);
         nodesToMerge.add(target);
         mergeNodes(MainApplication.getLayerManager().getEditLayer(), nodesToMerge, target);
     }
@@ -929,8 +936,8 @@
      * @param nodes the collection of nodes. Ignored if null
      * @param targetLocationNode this node's location will be used for the target node
      */
-    public void mergeNodes(OsmDataLayer layer, Collection<Node> nodes,
-                           Node targetLocationNode) {
+    public void mergeNodes(OsmDataLayer layer, Collection<INode> nodes,
+                           INode targetLocationNode) {
         MergeNodesAction.doMergeNodes(layer, nodes, targetLocationNode);
     }

@@ -940,15 +947,15 @@
      * @param p mouse position
      * @return node to merge to, or null
      */
-    private Node findNodeToMergeTo(Point p) {
-        Collection<Node> target = mv.getNearestNodes(p,
-                getLayerManager().getEditDataSet().getSelectedNodes(),
+    private INode findNodeToMergeTo(Point p) {
+        Collection<INode> target = mv.getNearestNodes(p,
+                new ArrayList<>(getLayerManager().getEditDataSet().getSelectedNodes()),
                 mv.isSelectablePredicate);
         return target.isEmpty() ? null : target.iterator().next();
     }

-    private void selectPrims(Collection<OsmPrimitive> prims, boolean released, boolean area) {
-        DataSet ds = getLayerManager().getActiveDataSet();
+    private void selectPrims(Collection<IPrimitive> prims, boolean released, boolean area) {
+        OsmData<?, ?, ?, ?> ds = getLayerManager().getActiveData();

         // not allowed together: do not change dataset selection, return early
         // Virtual Ways: if non-empty the cursor is above a virtual node. So don't highlight
@@ -1027,11 +1034,11 @@
     private final transient CycleManager cycleManager = new CycleManager();
     private final transient VirtualManager virtualManager = new VirtualManager();

-    private class CycleManager {
+    private class CycleManager<T extends IPrimitive> {

-        private Collection<OsmPrimitive> cycleList = Collections.emptyList();
+        private Collection<T> cycleList = Collections.emptyList();
         private boolean cyclePrims;
-        private OsmPrimitive cycleStart;
+        private IPrimitive cycleStart;
         private boolean waitForMouseUpParameter;
         private boolean multipleMatchesParameter;
         /**
@@ -1048,8 +1055,8 @@
          * @param p point where user clicked
          * @return OsmPrimitive to be selected
          */
-        private OsmPrimitive cycleSetup(OsmPrimitive nearest, Point p) {
-            OsmPrimitive osm = null;
+        private T cycleSetup(T nearest, Point p) {
+            T osm = null;

             if (nearest != null) {
                 osm = nearest;
@@ -1070,8 +1077,8 @@
                         cyclePrims = false;

                         // find first already selected element in cycle list
-                        OsmPrimitive old = osm;
-                        for (OsmPrimitive o : cycleList) {
+                        T old = osm;
+                        for (T o : cycleList) {
                             if (o.isSelected()) {
                                 cyclePrims = true;
                                 osm = o;
@@ -1105,7 +1112,7 @@
          * <code>cycleList</code> field
          * @return the next element of cycle list
          */
-        private Collection<OsmPrimitive> cyclePrims() {
+        private Collection<IPrimitive> cyclePrims() {
             if (cycleList.size() <= 1) {
                 // no real cycling, just return one-element collection with nearest primitive in it
                 return cycleList;
@@ -1113,11 +1120,11 @@
             // updateKeyModifiers() already called before!

             DataSet ds = getLayerManager().getActiveDataSet();
-            OsmPrimitive first = cycleList.iterator().next(), foundInDS = null;
-            OsmPrimitive nxt = first;
+            IPrimitive first = cycleList.iterator().next(), foundInDS = null;
+            IPrimitive nxt = first;

             if (cyclePrims && shift) {
-                for (Iterator<OsmPrimitive> i = cycleList.iterator(); i.hasNext();) {
+                for (Iterator<IPrimitive> i = cycleList.iterator(); i.hasNext();) {
                     nxt = i.next();
                     if (!nxt.isSelected()) {
                         break; // take first primitive in cycleList not in sel
@@ -1125,7 +1132,7 @@
                 }
                 // if primitives 1,2,3 are under cursor, [Alt-press] [Shift-release] gives 1 -> 12 -> 123
             } else {
-                for (Iterator<OsmPrimitive> i = cycleList.iterator(); i.hasNext();) {
+                for (Iterator<IPrimitive> i = cycleList.iterator(); i.hasNext();) {
                     nxt = i.next();
                     if (nxt.isSelected()) {
                         foundInDS = nxt;
@@ -1168,7 +1175,7 @@
     private class VirtualManager {

         private Node virtualNode;
-        private Collection<WaySegment> virtualWays = new LinkedList<>();
+        private Collection<WaySegment<?, ?>> virtualWays = new LinkedList<>();
         private int nodeVirtualSize;
         private int virtualSnapDistSq2;
         private int virtualSpace;
@@ -1194,11 +1201,11 @@
         private boolean activateVirtualNodeNearPoint(Point p) {
             if (nodeVirtualSize > 0) {

-                Collection<WaySegment> selVirtualWays = new LinkedList<>();
-                Pair<Node, Node> vnp = null, wnp = new Pair<>(null, null);
+                Collection<WaySegment<?, ?>> selVirtualWays = new LinkedList<>();
+                Pair<INode, INode> vnp = null, wnp = new Pair<>(null, null);

-                for (WaySegment ws : mv.getNearestWaySegments(p, mv.isSelectablePredicate)) {
-                    Way w = ws.way;
+                for (WaySegment<?, ?> ws : mv.getNearestWaySegments(p, mv.isSelectablePredicate)) {
+                    IWay<?> w = ws.way;

                     wnp.a = w.getNode(ws.lowerIndex);
                     wnp.b = w.getNode(ws.lowerIndex + 1);
@@ -1237,15 +1244,15 @@
             if (startEN == null) // #13724, #14712, #15087
                 return;
             DataSet ds = getLayerManager().getEditDataSet();
-            Collection<Command> virtualCmds = new LinkedList<>();
-            virtualCmds.add(new AddCommand(ds, virtualNode));
-            for (WaySegment virtualWay : virtualWays) {
+            Collection<Command<OsmPrimitive, Node, Way, Relation>> virtualCmds = new LinkedList<>();
+            virtualCmds.add(new AddCommand<>(ds, virtualNode));
+            for (WaySegment<Node, Way> virtualWay : virtualWays) {
                 Way w = virtualWay.way;
                 Way wnew = new Way(w);
                 wnew.addNode(virtualWay.lowerIndex + 1, virtualNode);
-                virtualCmds.add(new ChangeCommand(ds, w, wnew));
+                virtualCmds.add(new ChangeCommand<>(ds, w, wnew));
             }
-            virtualCmds.add(new MoveCommand(ds, virtualNode, startEN, currentEN));
+            virtualCmds.add(new MoveCommand<>(ds, virtualNode, startEN, currentEN));
             String text = trn("Add and move a virtual new node to way",
                     "Add and move a virtual new node to {0} ways", virtualWays.size(),
                     virtualWays.size());
Index: src/org/openstreetmap/josm/actions/relation/AddSelectionToRelations.java
===================================================================
--- src/org/openstreetmap/josm/actions/relation/AddSelectionToRelations.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/relation/AddSelectionToRelations.java	(working copy)
@@ -15,9 +15,12 @@
 import org.openstreetmap.josm.data.UndoRedoHandler;
 import org.openstreetmap.josm.data.osm.DataSelectionListener;
 import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmData;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.OsmUtils;
 import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.Notification;
 import org.openstreetmap.josm.gui.dialogs.relation.GenericRelationEditor;
@@ -40,9 +43,9 @@

     @Override
     public void actionPerformed(ActionEvent e) {
-        Collection<Command> cmds = new LinkedList<>();
+        Collection<Command<OsmPrimitive, Node, Way, Relation>> cmds = new LinkedList<>();
         for (Relation orig : Utils.filteredCollection(relations, Relation.class)) {
-            Command c = GenericRelationEditor.addPrimitivesToRelation(orig, MainApplication.getLayerManager().getActiveDataSet().getSelected());
+            Command<OsmPrimitive, Node, Way, Relation> c = GenericRelationEditor.addPrimitivesToRelation(orig, MainApplication.getLayerManager().getActiveDataSet().getSelected());
             if (c != null) {
                 cmds.add(c);
             }
Index: src/org/openstreetmap/josm/actions/upload/FixDataHook.java
===================================================================
--- src/org/openstreetmap/josm/actions/upload/FixDataHook.java	(revision 16301)
+++ src/org/openstreetmap/josm/actions/upload/FixDataHook.java	(working copy)
@@ -16,8 +16,10 @@
 import org.openstreetmap.josm.command.SequenceCommand;
 import org.openstreetmap.josm.data.APIDataSet;
 import org.openstreetmap.josm.data.UndoRedoHandler;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.Utils;

@@ -187,7 +189,7 @@
     @Override
     public boolean checkUpload(APIDataSet apiDataSet) {
         if (Config.getPref().getBoolean("fix.data.on.upload", true)) {
-            Collection<Command> cmds = new LinkedList<>();
+            Collection<Command<OsmPrimitive, Node, Way, Relation>> cmds = new LinkedList<>();

             for (OsmPrimitive osm : apiDataSet.getPrimitives()) {
                 Map<String, String> keys = new HashMap<>(osm.getKeys());
Index: src/org/openstreetmap/josm/command/AbstractNodesCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/AbstractNodesCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/AbstractNodesCommand.java	(working copy)
@@ -6,21 +6,26 @@
 
 import javax.swing.Icon;
 
-import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
-import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.tools.ImageProvider;
 
 /**
  * Abstracts superclass of {@link ChangeNodesCommand} / {@link RemoveNodesCommand}.
+ * @param <O> the base type of primitives
+ * @param <N> type representing nodes
+ * @param <W> type representing ways
+ * @param <R> type representing relations
  * @param <C> type of nodes collection used for this command
  * @since 15013
  */
-public abstract class AbstractNodesCommand<C extends Collection<Node>> extends Command {
+public abstract class AbstractNodesCommand<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, C extends Collection<N>> extends Command<O, N, W, R> {
 
-    protected final Way way;
+    protected final W way;
     protected final C cmdNodes;
 
     /**
@@ -28,7 +33,7 @@
      * @param way The way to modify
      * @param cmdNodes The collection of nodes for this command
      */
-    protected AbstractNodesCommand(Way way, C cmdNodes) {
+    protected AbstractNodesCommand(W way, C cmdNodes) {
         this(way.getDataSet(), way, cmdNodes);
     }
 
@@ -38,7 +43,7 @@
      * @param way The way to modify
      * @param cmdNodes The collection of nodes for this command
      */
-    protected AbstractNodesCommand(DataSet ds, Way way, C cmdNodes) {
+    protected AbstractNodesCommand(OsmData<O, N, W, R> ds, W way, C cmdNodes) {
         super(ds);
         this.way = Objects.requireNonNull(way, "way");
         this.cmdNodes = Objects.requireNonNull(cmdNodes, "cmdNodes");
@@ -57,9 +62,10 @@
         return true;
     }
 
+    @SuppressWarnings("unchecked")
     @Override
-    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
-        modified.add(way);
+    public void fillModifiedData(Collection<O> modified, Collection<O> deleted, Collection<O> added) {
+        modified.add((O) way);
     }
 
     @Override
@@ -77,7 +83,7 @@
         if (this == obj) return true;
         if (obj == null || getClass() != obj.getClass()) return false;
         if (!super.equals(obj)) return false;
-        AbstractNodesCommand<?> that = (AbstractNodesCommand<?>) obj;
+        AbstractNodesCommand<?, ?, ?, ?, ?> that = (AbstractNodesCommand<?, ?, ?, ?, ?>) obj;
         return Objects.equals(way, that.way) &&
                Objects.equals(cmdNodes, that.cmdNodes);
     }
Index: src/org/openstreetmap/josm/command/AddCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/AddCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/AddCommand.java	(working copy)
@@ -10,9 +10,12 @@

 import javax.swing.Icon;

-import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.tools.ImageProvider;
@@ -23,13 +26,17 @@
  * See {@link ChangeCommand} for comments on relation back references.
  *
  * @author imi
+ * @param <O> the base type of primitives
+ * @param <N> type representing nodes
+ * @param <W> type representing ways
+ * @param <R> type representing relations
  */
-public class AddCommand extends Command {
+public class AddCommand<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> extends Command<O, N, W, R> {

     /**
      * The primitive to add to the dataset.
      */
-    private final OsmPrimitive osm;
+    private final O osm;

     /**
      * Creates the command and specify the element to add in the context of the given data set.
@@ -37,12 +44,12 @@
      * @param osm The primitive to add
      * @since 11240
      */
-    public AddCommand(DataSet data, OsmPrimitive osm) {
+    public AddCommand(OsmData<O, N, W, R> data, O osm) {
         super(data);
         this.osm = Objects.requireNonNull(osm, "osm");
     }

-    protected static final void checkNodeStyles(OsmPrimitive osm) {
+    protected static final <O extends IPrimitive> void checkNodeStyles(O osm) {
         if (osm instanceof Way) {
             // Fix #10557 - node icon not updated after undoing/redoing addition of a way
             ((Way) osm).clearCachedNodeStyles();
@@ -64,7 +71,7 @@
     }

     @Override
-    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
+    public void fillModifiedData(Collection<O> modified, Collection<O> deleted, Collection<O> added) {
         added.add(osm);
     }

@@ -86,7 +93,7 @@
     }

     @Override
-    public Collection<OsmPrimitive> getParticipatingPrimitives() {
+    public Collection<O> getParticipatingPrimitives() {
         return Collections.singleton(osm);
     }

@@ -100,7 +107,7 @@
         if (this == obj) return true;
         if (obj == null || getClass() != obj.getClass()) return false;
         if (!super.equals(obj)) return false;
-        AddCommand that = (AddCommand) obj;
+        AddCommand<?, ?, ?, ?> that = (AddCommand<?, ?, ?, ?>) obj;
         return Objects.equals(osm, that.osm);
     }
 }
Index: src/org/openstreetmap/josm/command/AddPrimitivesCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/AddPrimitivesCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/AddPrimitivesCommand.java	(working copy)
@@ -13,9 +13,13 @@

 import javax.swing.Icon;

-import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.NodeData;
+import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.PrimitiveData;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -23,9 +27,13 @@

 /**
  * Add primitives to a data layer.
+ * @param <O> the base type of primitives
+ * @param <N> type representing nodes
+ * @param <W> type representing ways
+ * @param <R> type representing relations
  * @since 2305
  */
-public class AddPrimitivesCommand extends Command {
+public class AddPrimitivesCommand<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> extends Command<O, N, W, R> {

     private List<PrimitiveData> data;
     private Collection<PrimitiveData> toSelect;
@@ -32,7 +40,7 @@
     private List<PrimitiveData> preExistingData;

     // only filled on undo
-    private List<OsmPrimitive> createdPrimitives;
+    private List<O> createdPrimitives;

     /**
      * Constructs a new {@code AddPrimitivesCommand} to add data to the given data set.
@@ -41,7 +49,7 @@
      * @param ds The target data set. Must not be {@code null}
      * @since 12718
      */
-    public AddPrimitivesCommand(List<PrimitiveData> data, List<PrimitiveData> toSelect, DataSet ds) {
+    public AddPrimitivesCommand(List<PrimitiveData> data, List<PrimitiveData> toSelect, OsmData<O, N, W, R> ds) {
         super(ds);
         init(data, toSelect);
     }
@@ -52,7 +60,7 @@
      * @param ds The target data set. Must not be {@code null}
      * @since 12726
      */
-    public AddPrimitivesCommand(List<PrimitiveData> data, DataSet ds) {
+    public AddPrimitivesCommand(List<PrimitiveData> data, OsmData<O, N, W, R> ds) {
         this(data, data, ds);
     }

@@ -68,13 +76,13 @@

     @Override
     public boolean executeCommand() {
-        DataSet ds = getAffectedDataSet();
+        OsmData<O, N, W, R> ds = getAffectedDataSet();
         if (createdPrimitives == null) { // first time execution
             List<OsmPrimitive> newPrimitives = new ArrayList<>(data.size());
             preExistingData = new ArrayList<>();

             for (PrimitiveData pd : data) {
-                OsmPrimitive primitive = ds.getPrimitiveById(pd);
+                O primitive = ds.getPrimitiveById(pd);
                 boolean created = primitive == null;
                 if (primitive == null) {
                     primitive = pd.getType().newInstance(pd.getUniqueId(), true);
@@ -100,7 +108,7 @@
         } else { // redo
             // When redoing this command, we have to add the same objects, otherwise
             // a subsequent command (e.g. MoveCommand) cannot be redone.
-            for (OsmPrimitive osm : createdPrimitives) {
+            for (O osm : createdPrimitives) {
                 if (preExistingData.stream().anyMatch(pd -> pd.getUniqueId() == osm.getUniqueId())) {
                     Optional<PrimitiveData> o = data.stream().filter(pd -> pd.getUniqueId() == osm.getUniqueId()).findAny();
                     if (o.isPresent()) {
@@ -118,16 +126,16 @@
     }

     @Override public void undoCommand() {
-        DataSet ds = getAffectedDataSet();
+        OsmData<O, N, W, R> ds = getAffectedDataSet();
         if (createdPrimitives == null) {
             createdPrimitives = new ArrayList<>(data.size());
             for (PrimitiveData pd : data) {
-                OsmPrimitive p = ds.getPrimitiveById(pd);
+                O p = ds.getPrimitiveById(pd);
                 createdPrimitives.add(p);
             }
             createdPrimitives = PurgeCommand.topoSort(createdPrimitives);
         }
-        for (OsmPrimitive osm : createdPrimitives) {
+        for (O osm : createdPrimitives) {
             Optional<PrimitiveData> previous = preExistingData.stream().filter(pd -> pd.getUniqueId() == osm.getUniqueId()).findAny();
             if (previous.isPresent()) {
                 osm.load(previous.get());
@@ -149,17 +157,17 @@
     }

     @Override
-    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
-            Collection<OsmPrimitive> added) {
+    public void fillModifiedData(Collection<O> modified, Collection<O> deleted,
+            Collection<O> added) {
         // Does nothing because we don't want to create OsmPrimitives.
     }

     @Override
-    public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+    public Collection<O> getParticipatingPrimitives() {
         if (createdPrimitives != null)
             return createdPrimitives;

-        Collection<OsmPrimitive> prims = new HashSet<>();
+        Collection<O> prims = new HashSet<>();
         for (PrimitiveData d : data) {
             prims.add(Optional.ofNullable(getAffectedDataSet().getPrimitiveById(d)).orElseThrow(
                     () -> new JosmRuntimeException("No primitive found for " + d)));
@@ -177,7 +185,7 @@
         if (this == obj) return true;
         if (obj == null || getClass() != obj.getClass()) return false;
         if (!super.equals(obj)) return false;
-        AddPrimitivesCommand that = (AddPrimitivesCommand) obj;
+        AddPrimitivesCommand<?, ?, ?, ?> that = (AddPrimitivesCommand<?, ?, ?, ?>) obj;
         return Objects.equals(data, that.data) &&
                Objects.equals(toSelect, that.toSelect) &&
                Objects.equals(preExistingData, that.preExistingData) &&
Index: src/org/openstreetmap/josm/command/ChangeCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/ChangeCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/ChangeCommand.java	(working copy)
@@ -9,9 +9,12 @@

 import javax.swing.Icon;

-import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -19,13 +22,17 @@

 /**
  * Command that basically replaces one OSM primitive by another of the same type.
+ * @param <O> the base type of primitives
+ * @param <N> type representing nodes
+ * @param <W> type representing ways
+ * @param <R> type representing relations
  *
  * @since 93
  */
-public class ChangeCommand extends Command {
+public class ChangeCommand<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> extends Command<O, N, W, R> {

-    private final OsmPrimitive osm;
-    private final OsmPrimitive newOsm;
+    private final O osm;
+    private final O newOsm;

     /**
      * Constructs a new {@code ChangeCommand} in the context of {@code osm} data set.
@@ -33,7 +40,7 @@
      * @param newOsm The new primitive
      * @throws IllegalArgumentException if sanity checks fail
      */
-    public ChangeCommand(OsmPrimitive osm, OsmPrimitive newOsm) {
+    public ChangeCommand(O osm, O newOsm) {
         this(osm.getDataSet(), osm, newOsm);
     }

@@ -45,7 +52,7 @@
      * @throws IllegalArgumentException if sanity checks fail
      * @since 11240
      */
-    public ChangeCommand(DataSet data, OsmPrimitive osm, OsmPrimitive newOsm) {
+    public ChangeCommand(OsmData<O, N, W, R> data, O osm, O newOsm) {
         super(data);
         this.osm = osm;
         this.newOsm = newOsm;
@@ -70,7 +77,7 @@
     }

     @Override
-    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
+    public void fillModifiedData(Collection<O> modified, Collection<O> deleted, Collection<O> added) {
         modified.add(osm);
     }

@@ -96,7 +103,7 @@
      * @return the original OSM primitive to modify
      * @since 14283
      */
-    public final OsmPrimitive getOsmPrimitive() {
+    public final O getOsmPrimitive() {
         return osm;
     }

@@ -105,7 +112,7 @@
      * @return the new OSM primitive
      * @since 14283
      */
-    public final OsmPrimitive getNewOsmPrimitive() {
+    public final O getNewOsmPrimitive() {
         return newOsm;
     }

@@ -119,7 +126,7 @@
         if (this == obj) return true;
         if (obj == null || getClass() != obj.getClass()) return false;
         if (!super.equals(obj)) return false;
-        ChangeCommand that = (ChangeCommand) obj;
+        ChangeCommand<?, ?, ?, ?> that = (ChangeCommand<?, ?, ?, ?>) obj;
         return Objects.equals(osm, that.osm) &&
                 Objects.equals(newOsm, that.newOsm);
     }
Index: src/org/openstreetmap/josm/command/ChangeNodesCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/ChangeNodesCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/ChangeNodesCommand.java	(working copy)
@@ -5,10 +5,12 @@

 import java.util.List;

-import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
-import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.OsmData;

 /**
  * Command that changes the nodes list of a way.
@@ -17,8 +19,12 @@
  * tool of the validator, when processing large data sets.)
  *
  * @author Imi
+ * @param <O> the base type of primitives
+ * @param <N> type representing nodes
+ * @param <W> type representing ways
+ * @param <R> type representing relations
  */
-public class ChangeNodesCommand extends AbstractNodesCommand<List<Node>> {
+public class ChangeNodesCommand<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> extends AbstractNodesCommand<O, N, W, R, List<N>> {

     /**
      * Constructs a new {@code ChangeNodesCommand}.
@@ -25,7 +31,7 @@
      * @param way The way to modify
      * @param newNodes The new list of nodes for the given way
      */
-    public ChangeNodesCommand(Way way, List<Node> newNodes) {
+    public ChangeNodesCommand(W way, List<N> newNodes) {
         super(way.getDataSet(), way, newNodes);
     }

@@ -36,7 +42,7 @@
      * @param newNodes The new list of nodes for the given way
      * @since 12726
      */
-    public ChangeNodesCommand(DataSet ds, Way way, List<Node> newNodes) {
+    public ChangeNodesCommand(OsmData<O, N, W, R> ds, W way, List<N> newNodes) {
         super(ds, way, newNodes);
     }

Index: src/org/openstreetmap/josm/command/ChangePropertyCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/ChangePropertyCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/ChangePropertyCommand.java	(working copy)
@@ -18,8 +18,11 @@

 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.tools.I18n;
 import org.openstreetmap.josm.tools.ImageProvider;

@@ -30,7 +33,7 @@
  * @author imi
  * @since 24
  */
-public class ChangePropertyCommand extends Command {
+public class ChangePropertyCommand extends Command<OsmPrimitive, Node, Way, Relation> {

     static final class OsmPseudoCommand implements PseudoCommand {
         private final OsmPrimitive osm;
Index: src/org/openstreetmap/josm/command/ChangePropertyKeyCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/ChangePropertyKeyCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/ChangePropertyKeyCommand.java	(working copy)
@@ -15,7 +15,10 @@
 import javax.swing.Icon;

 import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.validation.util.NameVisitor;
 import org.openstreetmap.josm.tools.ImageProvider;

@@ -23,7 +26,7 @@
  * Command that replaces the key of one or several objects
  * @since 3669
  */
-public class ChangePropertyKeyCommand extends Command {
+public class ChangePropertyKeyCommand extends Command<OsmPrimitive, Node, Way, Relation> {
     static final class SinglePrimitivePseudoCommand implements PseudoCommand {
         private final String name;
         private final OsmPrimitive osm;
Index: src/org/openstreetmap/josm/command/ChangeRelationMemberRoleCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/ChangeRelationMemberRoleCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/ChangeRelationMemberRoleCommand.java	(working copy)
@@ -10,10 +10,12 @@

 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.tools.ImageProvider;

 /**
@@ -21,7 +23,7 @@
  *
  * @author Teemu Koskinen &lt;teemu.koskinen@mbnet.fi&gt;
  */
-public class ChangeRelationMemberRoleCommand extends Command {
+public class ChangeRelationMemberRoleCommand extends Command<OsmPrimitive, Node, Way, Relation> {

     // The relation to be changed
     private final Relation relation;
Index: src/org/openstreetmap/josm/command/Command.java
===================================================================
--- src/org/openstreetmap/josm/command/Command.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/Command.java	(working copy)
@@ -12,12 +12,15 @@
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
 import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.PrimitiveData;
-import org.openstreetmap.josm.data.osm.Relation;
-import org.openstreetmap.josm.data.osm.Way;
-import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
+import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
 import org.openstreetmap.josm.tools.CheckParameterUtil;

 /**
@@ -27,10 +30,14 @@
  * The command remembers the {@link DataSet} it is operating on.
  *
  * @author imi
+ * @param <O> the base type of primitives
+ * @param <N> type representing nodes
+ * @param <W> type representing ways
+ * @param <R> type representing relations
  * @since 21 (creation)
  * @since 10599 (signature)
  */
-public abstract class Command implements PseudoCommand {
+public abstract class Command<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> implements PseudoCommand {

     /** IS_OK : operation is okay */
     public static final int IS_OK = 0;
@@ -39,21 +46,21 @@
     /** IS_INCOMPLETE: operation on incomplete target */
     public static final int IS_INCOMPLETE = 2;

-    private static final class CloneVisitor implements OsmPrimitiveVisitor {
-        final Map<OsmPrimitive, PrimitiveData> orig = new LinkedHashMap<>();
+    private static final class CloneVisitor<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> implements PrimitiveVisitor<N, W, R> {
+        final Map<O, PrimitiveData> orig = new LinkedHashMap<>();

         @Override
-        public void visit(Node n) {
+        public void visit(N n) {
             orig.put(n, n.save());
         }

         @Override
-        public void visit(Way w) {
+        public void visit(W w) {
             orig.put(w, w.save());
         }

         @Override
-        public void visit(Relation e) {
+        public void visit(R e) {
             orig.put(e, e.save());
         }
     }
@@ -71,7 +78,7 @@
          * Constructs a new {@code OldNodeState} for the given node.
          * @param node The node whose state has to be remembered
          */
-        public OldNodeState(Node node) {
+        public OldNodeState(INode node) {
             latLon = node.getCoor();
             eastNorth = node.getEastNorth();
             modified = node.isModified();
@@ -122,10 +129,10 @@
     }

     /** the map of OsmPrimitives in the original state to OsmPrimitives in cloned state */
-    private Map<OsmPrimitive, PrimitiveData> cloneMap = new HashMap<>();
+    private Map<O, PrimitiveData> cloneMap = new HashMap<>();

     /** the dataset which this command is applied to */
-    private final DataSet data;
+    private final OsmData<O, N, W, R> data;

     /**
      * Creates a new command in the context of a specific data set, without data layer
@@ -134,7 +141,7 @@
      * @throws IllegalArgumentException if data is null
      * @since 11240
      */
-    public Command(DataSet data) {
+    public Command(OsmData<O, N, W, R> data) {
         CheckParameterUtil.ensureParameterNotNull(data, "data");
         this.data = data;
     }
@@ -147,10 +154,10 @@
      * @return true
      */
     public boolean executeCommand() {
-        CloneVisitor visitor = new CloneVisitor();
-        Collection<OsmPrimitive> all = new ArrayList<>();
+        CloneVisitor<O, N, W, R> visitor = new CloneVisitor<>();
+        Collection<O> all = new ArrayList<>();
         fillModifiedData(all, all, all);
-        for (OsmPrimitive osm : all) {
+        for (O osm : all) {
             osm.accept(visitor);
         }
         cloneMap = visitor.orig;
@@ -165,8 +172,8 @@
      * This implementation undoes all objects stored by a former call to executeCommand.
      */
     public void undoCommand() {
-        for (Entry<OsmPrimitive, PrimitiveData> e : cloneMap.entrySet()) {
-            OsmPrimitive primitive = e.getKey();
+        for (Entry<O, PrimitiveData> e : cloneMap.entrySet()) {
+            O primitive = e.getKey();
             if (primitive.getDataSet() != null) {
                 e.getKey().load(e.getValue());
             }
@@ -188,7 +195,7 @@
      * @return The data set. May be <code>null</code> if no layer was set and no edit layer was found.
      * @since 10467
      */
-    public DataSet getAffectedDataSet() {
+    public OsmData<O, N, W, R> getAffectedDataSet() {
         return data;
     }

@@ -200,9 +207,9 @@
      * @param deleted The deleted primitives
      * @param added The added primitives
      */
-    public abstract void fillModifiedData(Collection<OsmPrimitive> modified,
-            Collection<OsmPrimitive> deleted,
-            Collection<OsmPrimitive> added);
+    public abstract void fillModifiedData(Collection<O> modified,
+            Collection<O> deleted,
+            Collection<O> added);

     /**
      * Return the primitives that take part in this command.
@@ -209,7 +216,7 @@
      * The collection is computed during execution.
      */
     @Override
-    public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+    public Collection<? extends O> getParticipatingPrimitives() {
         return cloneMap.keySet();
     }

@@ -221,13 +228,13 @@
      * @return true, if operating on outlying primitives is OK; false, otherwise
      */
     public static int checkOutlyingOrIncompleteOperation(
-            Collection<? extends OsmPrimitive> primitives,
-            Collection<? extends OsmPrimitive> ignore) {
+            Collection<? extends IPrimitive> primitives,
+            Collection<? extends IPrimitive> ignore) {
         int res = 0;
-        for (OsmPrimitive osm : primitives) {
+        for (IPrimitive osm : primitives) {
             if (osm.isIncomplete()) {
                 res |= IS_INCOMPLETE;
-            } else if (osm.isOutsideDownloadArea()
+            } else if (osm instanceof OsmPrimitive && ((OsmPrimitive) osm).isOutsideDownloadArea()
                     && (ignore == null || !ignore.contains(osm))) {
                 res |= IS_OUTSIDE;
             }
@@ -242,7 +249,7 @@
      * @throws AssertionError if no {@link DataSet} is set or if any primitive does not belong to that dataset.
      */
     protected void ensurePrimitivesAreInDataset() {
-        for (OsmPrimitive primitive : this.getParticipatingPrimitives()) {
+        for (IPrimitive primitive : this.getParticipatingPrimitives()) {
             if (primitive.getDataSet() != this.getAffectedDataSet()) {
                 throw new AssertionError("Primitive is of wrong data set for this command: " + primitive);
             }
@@ -258,7 +265,7 @@
     public boolean equals(Object obj) {
         if (this == obj) return true;
         if (obj == null || getClass() != obj.getClass()) return false;
-        Command command = (Command) obj;
+        Command<?, ?, ?, ?> command = (Command<?, ?, ?, ?>) obj;
         return Objects.equals(cloneMap, command.cloneMap) &&
                Objects.equals(data, command.data);
     }
Index: src/org/openstreetmap/josm/command/DeleteCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/DeleteCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/DeleteCommand.java	(working copy)
@@ -22,15 +22,16 @@

 import javax.swing.Icon;

-import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
-import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
 import org.openstreetmap.josm.data.osm.PrimitiveData;
-import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.RelationToChildReference;
-import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.WaySegment;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.openstreetmap.josm.tools.ImageProvider;
@@ -40,13 +41,17 @@
  * A command to delete a number of primitives from the dataset.
  * To be used correctly, this class requires an initial call to {@link #setDeletionCallback(DeletionCallback)} to
  * allow interactive confirmation actions.
+ * @param <O> the base type of primitives
+ * @param <N> type representing nodes
+ * @param <W> type representing ways
+ * @param <R> type representing relations
  * @since 23
  */
-public class DeleteCommand extends Command {
+public class DeleteCommand<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> extends Command<O, N, W, R> {
     private static final class DeleteChildCommand implements PseudoCommand {
-        private final OsmPrimitive osm;
+        private final IPrimitive osm;

-        private DeleteChildCommand(OsmPrimitive osm) {
+        private DeleteChildCommand(IPrimitive osm) {
             this.osm = osm;
         }

@@ -61,7 +66,7 @@
         }

         @Override
-        public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+        public Collection<? extends IPrimitive> getParticipatingPrimitives() {
             return Collections.singleton(osm);
         }

@@ -83,7 +88,7 @@
          * @param ignore {@code null} or a primitive to be ignored
          * @return true, if operating on outlying primitives is OK; false, otherwise
          */
-        boolean checkAndConfirmOutlyingDelete(Collection<? extends OsmPrimitive> primitives, Collection<? extends OsmPrimitive> ignore);
+        boolean checkAndConfirmOutlyingDelete(Collection<? extends IPrimitive> primitives, Collection<? extends IPrimitive> ignore);

         /**
          * Confirm before deleting a relation, as it is a common newbie error.
@@ -91,7 +96,7 @@
          * @return {@code true} if user confirms the deletion
          * @since 12760
          */
-        boolean confirmRelationDeletion(Collection<Relation> relations);
+        boolean confirmRelationDeletion(Collection<? extends IRelation<?>> relations);

         /**
          * Confirm before removing a collection of primitives from their parent relations.
@@ -117,8 +122,8 @@
     /**
      * The primitives that get deleted.
      */
-    private final Collection<? extends OsmPrimitive> toDelete;
-    private final Map<OsmPrimitive, PrimitiveData> clonedPrimitives = new HashMap<>();
+    private final Collection<O> toDelete;
+    private final Map<O, PrimitiveData> clonedPrimitives = new HashMap<>();

     /**
      * Constructor. Deletes a collection of primitives in the current edit layer.
@@ -126,7 +131,7 @@
      * @param data the primitives to delete. Must neither be null nor empty, and belong to a data set
      * @throws IllegalArgumentException if data is null or empty
      */
-    public DeleteCommand(Collection<? extends OsmPrimitive> data) {
+    public DeleteCommand(Collection<? extends O> data) {
         this(data.iterator().next().getDataSet(), data);
     }

@@ -136,7 +141,7 @@
      * @param data  the primitive to delete. Must not be null.
      * @throws IllegalArgumentException if data is null
      */
-    public DeleteCommand(OsmPrimitive data) {
+    public DeleteCommand(O data) {
         this(Collections.singleton(data));
     }

@@ -149,7 +154,7 @@
      * @throws IllegalArgumentException if layer is null
      * @since 12718
      */
-    public DeleteCommand(DataSet dataset, OsmPrimitive data) {
+    public DeleteCommand(OsmData<O, N, W, R> dataset, O data) {
         this(dataset, Collections.singleton(data));
     }

@@ -162,10 +167,10 @@
      * @throws IllegalArgumentException if data is null or empty
      * @since 11240
      */
-    public DeleteCommand(DataSet dataset, Collection<? extends OsmPrimitive> data) {
+    public DeleteCommand(OsmData<O, N, W, R> dataset, Collection<? extends O> data) {
         super(dataset);
         CheckParameterUtil.ensureParameterNotNull(data, "data");
-        this.toDelete = data;
+        this.toDelete = new HashSet<>(data);
         checkConsistency();
     }

@@ -173,7 +178,7 @@
         if (toDelete.isEmpty()) {
             throw new IllegalArgumentException(tr("At least one object to delete required, got empty collection"));
         }
-        for (OsmPrimitive p : toDelete) {
+        for (O p : toDelete) {
             if (p == null) {
                 throw new IllegalArgumentException("Primitive to delete must not be null");
             } else if (p.getDataSet() == null) {
@@ -186,19 +191,19 @@
     public boolean executeCommand() {
         ensurePrimitivesAreInDataset();
         // Make copy and remove all references (to prevent inconsistent dataset (delete referenced) while command is executed)
-        for (OsmPrimitive osm: toDelete) {
+        for (O osm: toDelete) {
             if (osm.isDeleted())
                 throw new IllegalArgumentException(osm + " is already deleted");
             clonedPrimitives.put(osm, osm.save());

-            if (osm instanceof Way) {
-                ((Way) osm).setNodes(null);
-            } else if (osm instanceof Relation) {
-                ((Relation) osm).setMembers(null);
+            if (osm instanceof IWay<?>) {
+                ((IWay<?>) osm).setNodes(null);
+            } else if (osm instanceof IRelation<?>) {
+                ((IRelation<?>) osm).setMembers(null);
             }
         }

-        for (OsmPrimitive osm: toDelete) {
+        for (O osm: toDelete) {
             osm.setDeleted(true);
         }

@@ -209,23 +214,23 @@
     public void undoCommand() {
         ensurePrimitivesAreInDataset();

-        for (OsmPrimitive osm: toDelete) {
+        for (O osm: toDelete) {
             osm.setDeleted(false);
         }

-        for (Entry<OsmPrimitive, PrimitiveData> entry: clonedPrimitives.entrySet()) {
+        for (Entry<O, PrimitiveData> entry: clonedPrimitives.entrySet()) {
             entry.getKey().load(entry.getValue());
         }
     }

     @Override
-    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
+    public void fillModifiedData(Collection<O> modified, Collection<O> deleted, Collection<O> added) {
         // Do nothing
     }

     private EnumSet<OsmPrimitiveType> getTypesToDelete() {
         EnumSet<OsmPrimitiveType> typesToDelete = EnumSet.noneOf(OsmPrimitiveType.class);
-        for (OsmPrimitive osm : toDelete) {
+        for (O osm : toDelete) {
             typesToDelete.add(OsmPrimitiveType.from(osm));
         }
         return typesToDelete;
@@ -234,7 +239,7 @@
     @Override
     public String getDescriptionText() {
         if (toDelete.size() == 1) {
-            OsmPrimitive primitive = toDelete.iterator().next();
+            O primitive = toDelete.iterator().next();
             String msg;
             switch(OsmPrimitiveType.from(primitive)) {
             case NODE: msg = marktr("Delete node {0}"); break;
@@ -278,7 +283,7 @@
             return null;
         else {
             List<PseudoCommand> children = new ArrayList<>(toDelete.size());
-            for (final OsmPrimitive osm : toDelete) {
+            for (final O osm : toDelete) {
                 children.add(new DeleteChildCommand(osm));
             }
             return children;
@@ -286,7 +291,7 @@
         }
     }

-    @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+    @Override public Collection<O> getParticipatingPrimitives() {
         return toDelete;
     }

@@ -303,9 +308,9 @@
      * @throws IllegalArgumentException if layer is null
      * @since 12718
      */
-    public static Command deleteWithReferences(Collection<? extends OsmPrimitive> selection, boolean silent) {
+    public static <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> Command<O, N, W, R> deleteWithReferences(Collection<O> selection, boolean silent) {
         if (selection == null || selection.isEmpty()) return null;
-        Set<OsmPrimitive> parents = OsmPrimitive.getReferrer(selection);
+        Set<O> parents = OsmPrimitive.getReferrer(selection);
         parents.addAll(selection);

         if (parents.isEmpty())
@@ -312,7 +317,7 @@
             return null;
         if (!silent && !callback.checkAndConfirmOutlyingDelete(parents, null))
             return null;
-        return new DeleteCommand(parents.iterator().next().getDataSet(), parents);
+        return new DeleteCommand<>(parents.iterator().next().getDataSet(), parents);
     }

     /**
@@ -327,7 +332,7 @@
      * @throws IllegalArgumentException if layer is null
      * @since 12718
      */
-    public static Command deleteWithReferences(Collection<? extends OsmPrimitive> selection) {
+    public static <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> Command<O, N, W, R> deleteWithReferences(Collection<? extends O> selection) {
         return deleteWithReferences(selection, false);
     }

@@ -344,7 +349,7 @@
      * @return command a command to perform the deletions, or null if there is nothing to delete.
      * @since 12718
      */
-    public static Command delete(Collection<? extends OsmPrimitive> selection) {
+    public static <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> Command<O, N, W, R> delete(Collection<O> selection) {
         return delete(selection, true, false);
     }

@@ -359,17 +364,17 @@
      * @return the collection of nodes referred to by primitives in <code>primitivesToDelete</code> which
      * can be deleted too
      */
-    protected static Collection<Node> computeNodesToDelete(Collection<OsmPrimitive> primitivesToDelete) {
-        Collection<Node> nodesToDelete = new HashSet<>();
-        for (Way way : Utils.filteredCollection(primitivesToDelete, Way.class)) {
-            for (Node n : way.getNodes()) {
+    protected static Collection<INode> computeNodesToDelete(Collection<IPrimitive> primitivesToDelete) {
+        Collection<INode> nodesToDelete = new HashSet<>();
+        for (IWay<?> way : Utils.filteredCollection(primitivesToDelete, IWay.class)) {
+            for (INode n : way.getNodes()) {
                 if (n.isTagged()) {
                     continue;
                 }
-                Collection<OsmPrimitive> referringPrimitives = n.getReferrers();
+                Collection<IPrimitive> referringPrimitives = new HashSet<>(n.getReferrers());
                 referringPrimitives.removeAll(primitivesToDelete);
                 int count = 0;
-                for (OsmPrimitive p : referringPrimitives) {
+                for (IPrimitive p : referringPrimitives) {
                     if (!p.isDeleted()) {
                         count++;
                     }
@@ -396,7 +401,7 @@
      * @return command a command to perform the deletions, or null if there is nothing to delete.
      * @since 12718
      */
-    public static Command delete(Collection<? extends OsmPrimitive> selection, boolean alsoDeleteNodesInWay) {
+    public static <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> Command<O, N, W, R> delete(Collection<? extends O> selection, boolean alsoDeleteNodesInWay) {
         return delete(selection, alsoDeleteNodesInWay, false /* not silent */);
     }

@@ -415,34 +420,34 @@
      * @return command a command to perform the deletions, or null if there is nothing to delete.
      * @since 12718
      */
-    public static Command delete(Collection<? extends OsmPrimitive> selection, boolean alsoDeleteNodesInWay, boolean silent) {
+    public static <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> Command<O, N, W, R> delete(Collection<? extends O> selection, boolean alsoDeleteNodesInWay, boolean silent) {
         if (selection == null || selection.isEmpty())
             return null;

-        Set<OsmPrimitive> primitivesToDelete = new HashSet<>(selection);
+        Set<IPrimitive> primitivesToDelete = new HashSet<>(selection);

-        Collection<Relation> relationsToDelete = Utils.filteredCollection(primitivesToDelete, Relation.class);
+        Collection<R> relationsToDelete = Utils.filteredCollection(primitivesToDelete, R);
         if (!relationsToDelete.isEmpty() && !silent && !callback.confirmRelationDeletion(relationsToDelete))
             return null;

         if (alsoDeleteNodesInWay) {
             // delete untagged nodes only referenced by primitives in primitivesToDelete, too
-            Collection<Node> nodesToDelete = computeNodesToDelete(primitivesToDelete);
+            Collection<INode> nodesToDelete = computeNodesToDelete(primitivesToDelete);
             primitivesToDelete.addAll(nodesToDelete);
         }

         if (!silent && !callback.checkAndConfirmOutlyingDelete(
-                primitivesToDelete, Utils.filteredCollection(primitivesToDelete, Way.class)))
+                primitivesToDelete, Utils.filteredCollection(primitivesToDelete, IWay.class)))
             return null;

-        Collection<Way> waysToBeChanged = primitivesToDelete.stream()
-                .flatMap(p -> p.referrers(Way.class))
+        Collection<IWay<?>> waysToBeChanged = primitivesToDelete.stream()
+                .flatMap(p -> p.getReferrers().stream().filter(IWay.class::isInstance).map(IWay.class::cast))
                 .collect(Collectors.toSet());

         Collection<Command> cmds = new LinkedList<>();
-        Set<Node> nodesToRemove = new HashSet<>(Utils.filteredCollection(primitivesToDelete, Node.class));
-        for (Way w : waysToBeChanged) {
-            Way wnew = new Way(w);
+        Set<INode> nodesToRemove = new HashSet<>(Utils.filteredCollection(primitivesToDelete, INode.class));
+        for (IWay<?> w : waysToBeChanged) {
+            IWay<?> wnew = Utils.clone(w);
             wnew.removeNodes(nodesToRemove);
             if (wnew.getNodesCount() < 2) {
                 primitivesToDelete.add(w);
@@ -463,11 +468,11 @@

         // remove the objects from their parent relations
         //
-        final Set<Relation> relationsToBeChanged = primitivesToDelete.stream()
-                .flatMap(p -> p.referrers(Relation.class))
+        final Set<IRelation<?>> relationsToBeChanged = primitivesToDelete.stream()
+                .flatMap(p -> p.getReferrers().stream().filter(IRelation.class::isInstance).map(IRelation.class::cast))
                 .collect(Collectors.toSet());
-        for (Relation cur : relationsToBeChanged) {
-            Relation rel = new Relation(cur);
+        for (IRelation<?> cur : relationsToBeChanged) {
+            IRelation<?> rel = Utils.clone(cur);
             rel.removeMembersFor(primitivesToDelete);
             cmds.add(new ChangeCommand(cur, rel));
         }
@@ -475,10 +480,10 @@
         // build the delete command
         //
         if (!primitivesToDelete.isEmpty()) {
-            cmds.add(new DeleteCommand(primitivesToDelete.iterator().next().getDataSet(), primitivesToDelete));
+            cmds.add(new DeleteCommand<>(primitivesToDelete.iterator().next().getDataSet(), primitivesToDelete));
         }

-        return new SequenceCommand(tr("Delete"), cmds);
+        return SequenceCommand.createSimplifiedSequenceCommand(tr("Delete"), cmds);
     }

     /**
@@ -487,7 +492,7 @@
      * @return A matching command to safely delete that segment.
      * @since 12718
      */
-    public static Command deleteWaySegment(WaySegment ws) {
+    public static <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> Command<O, N, W, R> deleteWaySegment(WaySegment<N, W> ws) {
         if (ws.way.getNodesCount() < 3)
             return delete(Collections.singleton(ws.way), false);

@@ -494,24 +499,24 @@
         if (ws.way.isClosed()) {
             // If the way is circular (first and last nodes are the same), the way shouldn't be splitted

-            List<Node> n = new ArrayList<>();
+            List<N> n = new ArrayList<>();

             n.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount() - 1));
             n.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));

-            Way wnew = new Way(ws.way);
+            W wnew = Utils.clone(ws.way);
             wnew.setNodes(n);

             return new ChangeCommand(ws.way, wnew);
         }

-        List<Node> n1 = new ArrayList<>();
-        List<Node> n2 = new ArrayList<>();
+        List<N> n1 = new ArrayList<>();
+        List<N> n2 = new ArrayList<>();

         n1.addAll(ws.way.getNodes().subList(0, ws.lowerIndex + 1));
         n2.addAll(ws.way.getNodes().subList(ws.lowerIndex + 1, ws.way.getNodesCount()));

-        Way wnew = new Way(ws.way);
+        W wnew = Utils.clone(ws.way);

         if (n1.size() < 2) {
             wnew.setNodes(n2);
Index: src/org/openstreetmap/josm/command/MoveCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/MoveCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/MoveCommand.java	(working copy)
@@ -10,14 +10,17 @@
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.stream.Collectors;

 import javax.swing.Icon;

 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
 import org.openstreetmap.josm.data.projection.ProjectionRegistry;
 import org.openstreetmap.josm.tools.ImageProvider;
@@ -27,12 +30,16 @@
  * to collect several MoveCommands into one command.
  *
  * @author imi
+ * @param <O> the base type of primitives
+ * @param <N> type representing nodes
+ * @param <W> type representing ways
+ * @param <R> type representing relations
  */
-public class MoveCommand extends Command {
+public class MoveCommand<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> extends Command<O, N, W, R> {
     /**
      * The objects that should be moved.
      */
-    private Collection<Node> nodes = new LinkedList<>();
+    private Collection<N> nodes = new LinkedList<>();
     /**
      * Starting position, base command point, current (mouse-drag) position = startEN + (x,y) =
      */
@@ -61,7 +68,7 @@
      * @param x X difference movement. Coordinates are in northern/eastern
      * @param y Y difference movement. Coordinates are in northern/eastern
      */
-    public MoveCommand(OsmPrimitive osm, double x, double y) {
+    public MoveCommand(O osm, double x, double y) {
         this(Collections.singleton(osm), x, y);
     }

@@ -70,8 +77,8 @@
      * @param node The node to move
      * @param position The new location (lat/lon)
      */
-    public MoveCommand(Node node, LatLon position) {
-        this(Collections.singleton((OsmPrimitive) node),
+    public MoveCommand(N node, LatLon position) {
+        this(Collections.singleton((O) node),
                 ProjectionRegistry.getProjection().latlon2eastNorth(position).subtract(node.getEastNorth()));
     }

@@ -80,7 +87,7 @@
      * @param objects The primitives to move
      * @param offset The movement vector
      */
-    public MoveCommand(Collection<OsmPrimitive> objects, EastNorth offset) {
+    public MoveCommand(Collection<O> objects, EastNorth offset) {
         this(objects, offset.getX(), offset.getY());
     }

@@ -92,7 +99,7 @@
      * @throws NullPointerException if objects is null or contain null item
      * @throws NoSuchElementException if objects is empty
      */
-    public MoveCommand(Collection<OsmPrimitive> objects, double x, double y) {
+    public MoveCommand(Collection<O> objects, double x, double y) {
         this(objects.iterator().next().getDataSet(), objects, x, y);
     }

@@ -106,7 +113,7 @@
      * @throws NoSuchElementException if objects is empty
      * @since 12759
      */
-    public MoveCommand(DataSet ds, Collection<OsmPrimitive> objects, double x, double y) {
+    public MoveCommand(OsmData<O, N, W, R> ds, Collection<O> objects, double x, double y) {
         super(ds);
         startEN = null;
         saveCheckpoint(); // (0,0) displacement will be saved
@@ -114,7 +121,7 @@
         this.y = y;
         Objects.requireNonNull(objects, "objects");
         this.nodes = AllNodesVisitor.getAllNodes(objects);
-        for (Node n : this.nodes) {
+        for (INode n : this.nodes) {
             oldState.add(new OldNodeState(n));
         }
     }
@@ -127,7 +134,7 @@
      * @param end The ending position (northern/eastern)
      * @since 12759
      */
-    public MoveCommand(DataSet ds, Collection<OsmPrimitive> objects, EastNorth start, EastNorth end) {
+    public MoveCommand(OsmData<O, N, W, R> ds, Collection<O> objects, EastNorth start, EastNorth end) {
         this(Objects.requireNonNull(ds, "ds"),
              Objects.requireNonNull(objects, "objects"),
              Objects.requireNonNull(end, "end").getX() - Objects.requireNonNull(start, "start").getX(),
@@ -141,7 +148,7 @@
      * @param start The starting position (northern/eastern)
      * @param end The ending position (northern/eastern)
      */
-    public MoveCommand(Collection<OsmPrimitive> objects, EastNorth start, EastNorth end) {
+    public MoveCommand(Collection<O> objects, EastNorth start, EastNorth end) {
         this(Objects.requireNonNull(objects, "objects").iterator().next().getDataSet(), objects, start, end);
     }

@@ -153,7 +160,7 @@
      * @param end The ending position (northern/eastern)
      * @since 12759
      */
-    public MoveCommand(DataSet ds, OsmPrimitive p, EastNorth start, EastNorth end) {
+    public MoveCommand(OsmData<O, N, W, R> ds, O p, EastNorth start, EastNorth end) {
         this(ds, Collections.singleton(Objects.requireNonNull(p, "p")), start, end);
     }

@@ -163,7 +170,7 @@
      * @param start The starting position (northern/eastern)
      * @param end The ending position (northern/eastern)
      */
-    public MoveCommand(OsmPrimitive p, EastNorth start, EastNorth end) {
+    public MoveCommand(O p, EastNorth start, EastNorth end) {
         this(Collections.singleton(Objects.requireNonNull(p, "p")), start, end);
     }

@@ -179,7 +186,7 @@
      * @param y Y difference movement. Coordinates are in northern/eastern
      */
     public void moveAgain(double x, double y) {
-        for (Node n : nodes) {
+        for (INode n : nodes) {
             EastNorth eastNorth = n.getEastNorth();
             if (eastNorth != null) {
                 n.setEastNorth(eastNorth.add(x, y));
@@ -239,7 +246,7 @@

     private void updateCoordinates() {
         Iterator<OldNodeState> it = oldState.iterator();
-        for (Node n : nodes) {
+        for (INode n : nodes) {
             OldNodeState os = it.next();
             if (os.getEastNorth() != null) {
                 n.setEastNorth(os.getEastNorth().add(x, y));
@@ -251,7 +258,7 @@
     public boolean executeCommand() {
         ensurePrimitivesAreInDataset();

-        for (Node n : nodes) {
+        for (INode n : nodes) {
             // in case #3892 happens again
             if (n == null)
                 throw new AssertionError("null detected in node list");
@@ -269,7 +276,7 @@
         ensurePrimitivesAreInDataset();

         Iterator<OldNodeState> it = oldState.iterator();
-        for (Node n : nodes) {
+        for (INode n : nodes) {
             OldNodeState os = it.next();
             n.setCoor(os.getLatLon());
             n.setModified(os.isModified());
@@ -277,9 +284,9 @@
     }

     @Override
-    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
-        for (OsmPrimitive osm : nodes) {
-            modified.add(osm);
+    public void fillModifiedData(Collection<O> modified, Collection<O> deleted, Collection<O> added) {
+        for (N osm : nodes) {
+            modified.add((O) osm);
         }
     }

@@ -294,8 +301,8 @@
     }

     @Override
-    public Collection<Node> getParticipatingPrimitives() {
-        return nodes;
+    public Collection<? extends O> getParticipatingPrimitives() {
+        return nodes.stream().map(n -> (O) n).collect(Collectors.toList());
     }

     /**
Index: src/org/openstreetmap/josm/command/PseudoCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/PseudoCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/PseudoCommand.java	(working copy)
@@ -5,7 +5,7 @@

 import javax.swing.Icon;

-import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.IPrimitive;

 /**
  * PseudoCommand is a reduced form of a command. It can be presented in a tree view
@@ -34,7 +34,7 @@
      * Return the primitives that take part in this command.
      * @return primitives that take part in this command
      */
-    Collection<? extends OsmPrimitive> getParticipatingPrimitives();
+    Collection<? extends IPrimitive> getParticipatingPrimitives();

     /**
      * Returns the subcommands of this command.
Index: src/org/openstreetmap/josm/command/PurgeCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/PurgeCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/PurgeCommand.java	(working copy)
@@ -5,6 +5,7 @@

 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -17,9 +18,13 @@

 import org.openstreetmap.josm.data.conflict.Conflict;
 import org.openstreetmap.josm.data.conflict.ConflictCollection;
-import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.NodeData;
+import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.PrimitiveData;
 import org.openstreetmap.josm.data.osm.PrimitiveId;
@@ -34,9 +39,13 @@

 /**
  * Command, to purge a list of primitives.
+ * @param <O> the base type of primitives
+ * @param <N> type representing nodes
+ * @param <W> type representing ways
+ * @param <R> type representing relations
  */
-public class PurgeCommand extends Command {
-    protected List<OsmPrimitive> toPurge;
+public class PurgeCommand<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> extends Command<O, N, W, R> {
+    protected List<O> toPurge;
     protected Storage<PrimitiveData> makeIncompleteData;

     protected Map<PrimitiveId, PrimitiveData> makeIncompleteDataByPrimId;
@@ -54,12 +63,12 @@
      * @param makeIncomplete primitives to make incomplete
      * @since 11240
      */
-    public PurgeCommand(DataSet data, Collection<OsmPrimitive> toPurge, Collection<OsmPrimitive> makeIncomplete) {
+    public PurgeCommand(OsmData<O, N, W, R> data, Collection<O> toPurge, Collection<O> makeIncomplete) {
         super(data);
         init(toPurge, makeIncomplete);
     }

-    private void init(Collection<OsmPrimitive> toPurge, Collection<OsmPrimitive> makeIncomplete) {
+    private void init(Collection<O> toPurge, Collection<O> makeIncomplete) {
         /**
          * The topological sort is to avoid missing way nodes and missing
          * relation members when adding primitives back to the dataset on undo.
@@ -72,11 +81,11 @@
         saveIncomplete(makeIncomplete);
     }

-    protected final void saveIncomplete(Collection<OsmPrimitive> makeIncomplete) {
+    protected final void saveIncomplete(Collection<O> makeIncomplete) {
         makeIncompleteData = new Storage<>(new Storage.PrimitiveIdHash());
         makeIncompleteDataByPrimId = makeIncompleteData.foreignKey(new Storage.PrimitiveIdHash());

-        for (OsmPrimitive osm : makeIncomplete) {
+        for (O osm : makeIncomplete) {
             makeIncompleteData.add(osm.save());
         }
     }
@@ -89,7 +98,7 @@
             getAffectedDataSet().clearSelection(toPurge);
             // Loop from back to front to keep referential integrity.
             for (int i = toPurge.size()-1; i >= 0; --i) {
-                OsmPrimitive osm = toPurge.get(i);
+                O osm = toPurge.get(i);
                 if (makeIncompleteDataByPrimId.containsKey(osm)) {
                     // we could simply set the incomplete flag
                     // but that would not free memory in case the
@@ -124,7 +133,7 @@
             return;

         getAffectedDataSet().update(() -> {
-            for (OsmPrimitive osm : toPurge) {
+            for (O osm : toPurge) {
                 PrimitiveData data = makeIncompleteDataByPrimId.get(osm);
                 if (data != null) {
                     if (getAffectedDataSet().getPrimitiveById(osm) != osm)
@@ -151,31 +160,33 @@
      * @param sel collection of primitives to sort
      * @return sorted list
      */
-    public static List<OsmPrimitive> topoSort(Collection<OsmPrimitive> sel) {
-        Set<OsmPrimitive> in = new HashSet<>(sel);
-        List<OsmPrimitive> out = new ArrayList<>(in.size());
-        Set<Relation> inR = new HashSet<>();
+    public static <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> List<O> topoSort(Collection<O> sel) {
+        if (sel == null || sel.isEmpty()) return Collections.emptyList();
+        OsmData<O, N, W, R> data = sel.iterator().next().getDataSet();
+        Set<O> in = new HashSet<>(sel);
+        List<O> out = new ArrayList<>(in.size());
+        Set<R> inR = new HashSet<>();

         // Nodes not deleted in the first pass
-        Set<OsmPrimitive> remainingNodes = new HashSet<>(in.size());
+        Set<O> remainingNodes = new HashSet<>(in.size());

         /**
          *  First add nodes that have no way referrer.
          */
         outer:
-            for (Iterator<OsmPrimitive> it = in.iterator(); it.hasNext();) {
-                OsmPrimitive u = it.next();
-                if (u instanceof Node) {
-                    Node n = (Node) u;
-                    for (OsmPrimitive ref : n.getReferrers()) {
-                        if (ref instanceof Way && in.contains(ref)) {
+            for (Iterator<O> it = in.iterator(); it.hasNext();) {
+                O u = it.next();
+                if (u instanceof INode) {
+                    N n = (N) u;
+                    for (IPrimitive ref : n.getReferrers()) {
+                        if (ref instanceof IWay && in.contains(ref)) {
                             it.remove();
-                            remainingNodes.add(n);
+                            remainingNodes.add((O) n);
                             continue outer;
                         }
                     }
                     it.remove();
-                    out.add(n);
+                    out.add((O) n);
                 }
             }

@@ -182,20 +193,20 @@
         /**
          * Then add all ways, each preceded by its (remaining) nodes.
          */
-        for (Iterator<OsmPrimitive> it = in.iterator(); it.hasNext();) {
-            OsmPrimitive u = it.next();
-            if (u instanceof Way) {
-                Way w = (Way) u;
+        for (Iterator<O> it = in.iterator(); it.hasNext();) {
+            O u = it.next();
+            if (u instanceof IWay) {
+                W w = (W) u;
                 it.remove();
-                for (Node n : w.getNodes()) {
+                for (N n : w.getNodes()) {
                     if (remainingNodes.contains(n)) {
                         remainingNodes.remove(n);
-                        out.add(n);
+                        out.add((O) n);
                     }
                 }
-                out.add(w);
+                out.add((O) w);
             } else if (u instanceof Relation) {
-                inR.add((Relation) u);
+                inR.add((R) u);
             }
         }

@@ -205,39 +216,39 @@
         // Do topological sorting on a DAG where each arrow points from child to parent.
         //  (Because it is faster to loop over getReferrers() than getMembers().)

-        Map<Relation, Integer> numChilds = new HashMap<>();
+        Map<R, Integer> numChilds = new HashMap<>();

         // calculate initial number of childs
-        for (Relation r : inR) {
+        for (R r : inR) {
             numChilds.put(r, 0);
         }
-        for (Relation r : inR) {
-            for (OsmPrimitive parent : r.getReferrers()) {
-                if (!(parent instanceof Relation))
+        for (R r : inR) {
+            for (IPrimitive parent : r.getReferrers()) {
+                if (!(parent instanceof IRelation))
                     throw new AssertionError();
                 Integer i = numChilds.get(parent);
                 if (i != null) {
-                    numChilds.put((Relation) parent, i+1);
+                    numChilds.put((R) parent, i+1);
                 }
             }
         }
-        Set<Relation> childlessR = new HashSet<>();
-        for (Relation r : inR) {
+        Set<R> childlessR = new HashSet<>();
+        for (R r : inR) {
             if (numChilds.get(r).equals(0)) {
                 childlessR.add(r);
             }
         }

-        List<Relation> outR = new ArrayList<>(inR.size());
+        List<R> outR = new ArrayList<>(inR.size());
         while (!childlessR.isEmpty()) {
             // Identify one childless Relation and let it virtually die. This makes other relations childless.
-            Iterator<Relation> it = childlessR.iterator();
-            Relation next = it.next();
+            Iterator<R> it = childlessR.iterator();
+            R next = it.next();
             it.remove();
             outR.add(next);

-            for (OsmPrimitive parentPrim : next.getReferrers()) {
-                Relation parent = (Relation) parentPrim;
+            for (IPrimitive parentPrim : next.getReferrers()) {
+                R parent = (R) parentPrim;
                 Integer i = numChilds.get(parent);
                 if (i != null) {
                     numChilds.put(parent, i-1);
@@ -251,7 +262,7 @@
         if (outR.size() != inR.size())
             throw new AssertionError("topo sort algorithm failed");

-        out.addAll(outR);
+        outR.forEach(r -> out.add((O) r));

         return out;
     }
@@ -267,12 +278,12 @@
     }

     @Override
-    public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+    public Collection<O> getParticipatingPrimitives() {
         return toPurge;
     }

     @Override
-    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
+    public void fillModifiedData(Collection<O> modified, Collection<O> deleted, Collection<O> added) {
         // Do nothing
     }

@@ -286,7 +297,7 @@
         if (this == obj) return true;
         if (obj == null || getClass() != obj.getClass()) return false;
         if (!super.equals(obj)) return false;
-        PurgeCommand that = (PurgeCommand) obj;
+        PurgeCommand<?, ?, ?, ?> that = (PurgeCommand<?, ?, ?, ?>) obj;
         return Objects.equals(toPurge, that.toPurge) &&
                 Objects.equals(makeIncompleteData, that.makeIncompleteData) &&
                 Objects.equals(makeIncompleteDataByPrimId, that.makeIncompleteDataByPrimId) &&
Index: src/org/openstreetmap/josm/command/RemoveNodesCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/RemoveNodesCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/RemoveNodesCommand.java	(working copy)
@@ -8,6 +8,8 @@
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
 import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.Way;

 /**
@@ -17,7 +19,7 @@
  *
  * @author Giuseppe Bilotta
  */
-public class RemoveNodesCommand extends AbstractNodesCommand<Set<Node>> {
+public class RemoveNodesCommand extends AbstractNodesCommand<OsmPrimitive, Node, Way, Relation, Set<Node>> {

     /**
      * Constructs a new {@code RemoveNodesCommand}.
Index: src/org/openstreetmap/josm/command/RotateCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/RotateCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/RotateCommand.java	(working copy)
@@ -7,7 +7,7 @@
 import java.util.Objects;

 import org.openstreetmap.josm.data.coor.EastNorth;
-import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.INode;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;

 /**
@@ -84,7 +84,7 @@
     protected void transformNodes() {
         double cosPhi = Math.cos(rotationAngle);
         double sinPhi = Math.sin(rotationAngle);
-        for (Node n : nodes) {
+        for (INode n : nodes) {
             EastNorth oldEastNorth = oldStates.get(n).getEastNorth();
             double x = oldEastNorth.east() - pivot.east();
             double y = oldEastNorth.north() - pivot.north();
Index: src/org/openstreetmap/josm/command/SelectCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/SelectCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/SelectCommand.java	(working copy)
@@ -9,7 +9,10 @@
 import java.util.Objects;

 import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;

 /**
  * Command that selects OSM primitives
@@ -16,7 +19,7 @@
  *
  * @author Landwirt
  */
-public class SelectCommand extends Command {
+public class SelectCommand extends Command<OsmPrimitive, Node, Way, Relation> {

     /** the primitives to select when executing the command */
     private final Collection<OsmPrimitive> newSelection;
Index: src/org/openstreetmap/josm/command/SequenceCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/SequenceCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/SequenceCommand.java	(working copy)
@@ -10,8 +10,11 @@

 import javax.swing.Icon;

-import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Utils;

@@ -19,12 +22,16 @@
  * A command consisting of a sequence of other commands. Executes the other commands
  * and undo them in reverse order.
  * @author imi
+ * @param <O> the base type of primitives
+ * @param <N> type representing nodes
+ * @param <W> type representing ways
+ * @param <R> type representing relations
  * @since 31
  */
-public class SequenceCommand extends Command {
+public class SequenceCommand<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> extends Command<O, N, W, R> {

     /** The command sequence to be executed. */
-    private Command[] sequence;
+    private Command<O, N, W, R>[] sequence;
     private boolean sequenceComplete;
     private final String name;
     /** Determines if the sequence execution should continue after one of its commands fails. */
@@ -38,7 +45,7 @@
      * @param continueOnError Determines if the sequence execution should continue after one of its commands fails
      * @since 12726
      */
-    public SequenceCommand(DataSet ds, String name, Collection<Command> sequenz, boolean continueOnError) {
+    public SequenceCommand(OsmData<O, N, W, R> ds, String name, Collection<Command<O, N, W, R>> sequenz, boolean continueOnError) {
         super(ds);
         this.name = name;
         this.sequence = sequenz.toArray(new Command[0]);
@@ -52,7 +59,7 @@
      * @param continueOnError Determines if the sequence execution should continue after one of its commands fails
      * @since 11874
      */
-    public SequenceCommand(String name, Collection<Command> sequenz, boolean continueOnError) {
+    public SequenceCommand(String name, Collection<Command<O, N, W, R>> sequenz, boolean continueOnError) {
         this(sequenz.iterator().next().getAffectedDataSet(), name, sequenz, continueOnError);
     }

@@ -61,7 +68,7 @@
      * @param name The description text
      * @param sequenz The sequence that should be executed.
      */
-    public SequenceCommand(String name, Collection<Command> sequenz) {
+    public SequenceCommand(String name, Collection<Command<O, N, W, R>> sequenz) {
         this(name, sequenz, false);
     }

@@ -70,10 +77,46 @@
      * @param name The description text
      * @param sequenz The sequence that should be executed.
      */
-    public SequenceCommand(String name, Command... sequenz) {
+    public SequenceCommand(String name, Command<O, N, W, R>... sequenz) {
         this(name, Arrays.asList(sequenz));
     }

+    /**
+     * Convenient constructor, if the commands are known at compile time.
+     * @param name The description text to be used for the sequence command, if one is created.
+     * @param sequenz The sequence that should be executed.
+     * @param <O> the base type of primitives
+     * @param <N> type representing nodes
+     * @param <W> type representing ways
+     * @param <R> type representing relations
+     * @return Either a SequenceCommand, or the only command in the potential sequence
+     * @since xxx
+     */
+    public static <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> Command<O, N, W, R> createSimplifiedSequenceCommand(String name, Command<O, N, W, R>... sequenz) {
+        if (sequenz.length == 1) {
+            return sequenz[0];
+        }
+        return new SequenceCommand<>(name, sequenz);
+    }
+
+    /**
+     * Convenient constructor, if the commands are known at compile time.
+     * @param name The description text to be used for the sequence command, if one is created.
+     * @param sequenz The sequence that should be executed.
+     * @param <O> the base type of primitives
+     * @param <N> type representing nodes
+     * @param <W> type representing ways
+     * @param <R> type representing relations
+     * @return Either a SequenceCommand, or the only command in the potential sequence
+     * @since xxx
+     */
+    public static <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> Command<O, N, W, R> createSimplifiedSequenceCommand(String name, Collection<Command<O, N, W, R>> sequenz) {
+        if (sequenz.size() == 1) {
+            return sequenz.iterator().next();
+        }
+        return new SequenceCommand<>(name, sequenz);
+    }
+
     @Override public boolean executeCommand() {
         for (int i = 0; i < sequence.length; i++) {
             boolean result = sequence[i].executeCommand();
@@ -90,7 +133,7 @@
      * Returns the last command.
      * @return The last command, or {@code null} if the sequence is empty.
      */
-    public Command getLastCommand() {
+    public Command<O, N, W, R> getLastCommand() {
         if (sequence.length == 0)
             return null;
         return sequence[sequence.length-1];
@@ -112,8 +155,8 @@
     }

     @Override
-    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
-        for (Command c : sequence) {
+    public void fillModifiedData(Collection<O> modified, Collection<O> deleted, Collection<O> added) {
+        for (Command<O, N, W, R> c : sequence) {
             c.fillModifiedData(modified, deleted, added);
         }
     }
@@ -143,15 +186,15 @@
     }

     @Override
-    public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
-        Collection<OsmPrimitive> prims = new HashSet<>();
-        for (Command c : sequence) {
+    public Collection<O> getParticipatingPrimitives() {
+        Collection<O> prims = new HashSet<>();
+        for (Command<O, N, W, R> c : sequence) {
             prims.addAll(c.getParticipatingPrimitives());
         }
         return prims;
     }

-    protected final void setSequence(Command... sequence) {
+    protected final void setSequence(Command<O, N, W, R>... sequence) {
         this.sequence = Utils.copyArray(sequence);
     }

@@ -169,7 +212,7 @@
         if (this == obj) return true;
         if (obj == null || getClass() != obj.getClass()) return false;
         if (!super.equals(obj)) return false;
-        SequenceCommand that = (SequenceCommand) obj;
+        SequenceCommand<?, ?, ?, ?> that = (SequenceCommand<?, ?, ?, ?>) obj;
         return sequenceComplete == that.sequenceComplete &&
                 continueOnError == that.continueOnError &&
                 Arrays.equals(sequence, that.sequence) &&
Index: src/org/openstreetmap/josm/command/TransformNodesCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/TransformNodesCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/TransformNodesCommand.java	(working copy)
@@ -8,12 +8,16 @@
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.stream.Collectors;

 import javax.swing.Icon;

 import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.INode;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.visitor.AllNodesVisitor;
 import org.openstreetmap.josm.tools.ImageProvider;

@@ -22,23 +26,23 @@
  *
  * @author Olivier Croquette &lt;ocroquette@free.fr&gt;
  */
-public abstract class TransformNodesCommand extends Command {
+public abstract class TransformNodesCommand extends Command<OsmPrimitive, Node, Way, Relation> {

     /**
      * The nodes to transform.
      */
-    protected final Collection<Node> nodes;
+    protected final Collection<INode> nodes;

     /**
      * List of all old states of the nodes.
      */
-    protected final Map<Node, OldNodeState> oldStates = new HashMap<>();
+    protected final Map<INode, OldNodeState> oldStates = new HashMap<>();

     /**
      * Stores the state of the nodes before the command.
      */
     protected final void storeOldState() {
-        for (Node n : this.nodes) {
+        for (INode n : this.nodes) {
             oldStates.put(n, new OldNodeState(n));
         }
     }
@@ -85,7 +89,7 @@
      * Flag all nodes as modified.
      */
     public void flagNodesAsModified() {
-        for (Node n : nodes) {
+        for (INode n : nodes) {
             n.setModified(true);
         }
     }
@@ -95,7 +99,7 @@
      */
     @Override
     public void undoCommand() {
-        for (Node n : nodes) {
+        for (INode n : nodes) {
             OldNodeState os = oldStates.get(n);
             n.setCoor(os.getLatLon());
             n.setModified(os.isModified());
@@ -108,7 +112,7 @@

     @Override
     public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
-        return nodes;
+        return getTransformedNodes();
     }

     @Override
@@ -126,7 +130,7 @@
      * @return nodes with the current transformation applied
      */
     public Collection<Node> getTransformedNodes() {
-        return nodes;
+        return nodes.stream().filter(Node.class::isInstance).map(Node.class::cast).collect(Collectors.toList());
     }

     /**
@@ -138,7 +142,7 @@
     public EastNorth getNodesCenter() {
         EastNorth sum = new EastNorth(0, 0);

-        for (Node n : nodes) {
+        for (INode n : nodes) {
             EastNorth en = n.getEastNorth();
             sum = sum.add(en.east(), en.north());
         }
Index: src/org/openstreetmap/josm/command/conflict/ConflictAddCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/conflict/ConflictAddCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/conflict/ConflictAddCommand.java	(working copy)
@@ -13,8 +13,11 @@
 import org.openstreetmap.josm.data.conflict.Conflict;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmDataManager;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.tools.ImageProvider;
 import org.openstreetmap.josm.tools.Logging;
@@ -24,7 +27,7 @@
  * Command used to add a new conflict.
  * @since 1857
  */
-public class ConflictAddCommand extends Command {
+public class ConflictAddCommand extends Command<OsmPrimitive, Node, Way, Relation> {
     private final Conflict<? extends OsmPrimitive> conflict;

     /**
@@ -55,7 +58,7 @@
     @Override
     public boolean executeCommand() {
         try {
-            getAffectedDataSet().getConflicts().add(conflict);
+            ((DataSet) getAffectedDataSet()).getConflicts().add(conflict);
         } catch (IllegalStateException e) {
             Logging.error(e);
             warnBecauseOfDoubleConflict();
@@ -65,7 +68,7 @@

     @Override
     public void undoCommand() {
-        DataSet ds = getAffectedDataSet();
+        DataSet ds = (DataSet) getAffectedDataSet();
         if (!OsmDataManager.getInstance().containsDataSet(ds)) {
             Logging.warn(tr("Layer ''{0}'' does not exist any more. Cannot remove conflict for object ''{1}''.",
                     ds.getName(),
Index: src/org/openstreetmap/josm/command/conflict/ConflictResolveCommand.java
===================================================================
--- src/org/openstreetmap/josm/command/conflict/ConflictResolveCommand.java	(revision 16301)
+++ src/org/openstreetmap/josm/command/conflict/ConflictResolveCommand.java	(working copy)
@@ -9,7 +9,11 @@
 import org.openstreetmap.josm.data.conflict.Conflict;
 import org.openstreetmap.josm.data.conflict.ConflictCollection;
 import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmDataManager;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.tools.Logging;

 /**
@@ -20,7 +24,7 @@
  * it reconstitutes them.
  *
  */
-public abstract class ConflictResolveCommand extends Command {
+public abstract class ConflictResolveCommand extends Command<OsmPrimitive, Node, Way, Relation> {
     /** the list of resolved conflicts */
     private final ConflictCollection resolvedConflicts = new ConflictCollection();

@@ -49,7 +53,7 @@
      *
      */
     protected void reconstituteConflicts() {
-        DataSet ds = getAffectedDataSet();
+        DataSet ds = (DataSet) getAffectedDataSet();
         for (Conflict<?> c : resolvedConflicts) {
             if (!ds.getConflicts().hasConflictForMy(c.getMy())) {
                 ds.getConflicts().add(c);
@@ -61,7 +65,7 @@
     public void undoCommand() {
         super.undoCommand();

-        DataSet ds = getAffectedDataSet();
+        DataSet ds = (DataSet) getAffectedDataSet();
         if (!OsmDataManager.getInstance().containsDataSet(ds)) {
             Logging.warn(tr("Cannot undo command ''{0}'' because layer ''{1}'' is not present any more",
                     this.toString(),
Index: src/org/openstreetmap/josm/data/osm/DataSet.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/DataSet.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/DataSet.java	(working copy)
@@ -128,8 +128,8 @@
     private final CopyOnWriteArrayList<DataSetListener> listeners = new CopyOnWriteArrayList<>();

     // provide means to highlight map elements that are not osm primitives
-    private Collection<WaySegment> highlightedVirtualNodes = new LinkedList<>();
-    private Collection<WaySegment> highlightedWaySegments = new LinkedList<>();
+    private Collection<WaySegment<Node, Way>> highlightedVirtualNodes = new LinkedList<>();
+    private Collection<WaySegment<Node, Way>> highlightedWaySegments = new LinkedList<>();
     private final ListenerList<HighlightUpdateListener> highlightUpdateListeners = ListenerList.create();

     // Number of open calls to beginUpdate
@@ -539,7 +539,8 @@
         primitive.setDataset(null);
     }

-    void removePrimitive(OsmPrimitive primitive) {
+    @Override
+    public void removePrimitive(OsmPrimitive primitive) {
         checkModifiable();
         update(() -> {
             removePrimitiveImpl(primitive);
@@ -571,12 +572,12 @@
     }

     @Override
-    public Collection<WaySegment> getHighlightedVirtualNodes() {
+    public Collection<WaySegment<Node, Way>> getHighlightedVirtualNodes() {
         return Collections.unmodifiableCollection(highlightedVirtualNodes);
     }

     @Override
-    public Collection<WaySegment> getHighlightedWaySegments() {
+    public Collection<WaySegment<Node, Way>> getHighlightedWaySegments() {
         return Collections.unmodifiableCollection(highlightedWaySegments);
     }

@@ -630,7 +631,7 @@
     }

     @Override
-    public void setHighlightedVirtualNodes(Collection<WaySegment> waySegments) {
+    public void setHighlightedVirtualNodes(Collection<WaySegment<Node, Way>> waySegments) {
         if (highlightedVirtualNodes.isEmpty() && waySegments.isEmpty())
             return;

@@ -639,7 +640,7 @@
     }

     @Override
-    public void setHighlightedWaySegments(Collection<WaySegment> waySegments) {
+    public void setHighlightedWaySegments(Collection<WaySegment<Node, Way>> waySegments) {
         if (highlightedWaySegments.isEmpty() && waySegments.isEmpty())
             return;

Index: src/org/openstreetmap/josm/data/osm/DefaultNameFormatter.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/DefaultNameFormatter.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/DefaultNameFormatter.java	(working copy)
@@ -667,7 +667,7 @@
      * @param maxElements the maximum number of elements to display
      * @return HTML unordered list
      */
-    public String formatAsHtmlUnorderedList(Collection<? extends OsmPrimitive> primitives, int maxElements) {
+    public String formatAsHtmlUnorderedList(Collection<? extends IPrimitive> primitives, int maxElements) {
         Collection<String> displayNames = primitives.stream().map(x -> x.getDisplayName(this)).collect(Collectors.toList());
         return Utils.joinAsHtmlUnorderedList(Utils.limit(displayNames, maxElements, "..."));
     }
Index: src/org/openstreetmap/josm/data/osm/INode.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/INode.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/INode.java	(working copy)
@@ -1,6 +1,7 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.osm;

+import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.ILatLon;
 import org.openstreetmap.josm.data.coor.LatLon;
@@ -70,4 +71,25 @@
     default String getDisplayName(NameFormatter formatter) {
         return formatter.format(this);
     }
+
+
+    /**
+     * Determines if this node is outside of the world. See also #13538.
+     * @return <code>true</code>, if the coordinate is outside the world, compared by using lat/lon and east/north
+     * @since 14960 (extracted to INode in xxx)
+     */
+    default public boolean isOutSideWorld() {
+        LatLon ll = getCoor();
+        if (ll != null) {
+            Bounds b = ProjectionRegistry.getProjection().getWorldBoundsLatLon();
+            if (lat() < b.getMinLat() || lat() > b.getMaxLat() || lon() < b.getMinLon() || lon() > b.getMaxLon()) {
+                return true;
+            }
+            if (!ProjectionRegistry.getProjection().latlon2eastNorth(ll).equalsEpsilon(getEastNorth(), 1.0)) {
+                // we get here if a node was moved or created left from -180 or right from +180
+                return true;
+            }
+        }
+        return false;
+    }
 }
Index: src/org/openstreetmap/josm/data/osm/IPrimitive.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/IPrimitive.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/IPrimitive.java	(working copy)
@@ -331,7 +331,7 @@
      * Makes the given visitor visit this primitive.
      * @param visitor visitor
      */
-    void accept(PrimitiveVisitor visitor);
+    void accept(PrimitiveVisitor<?, ?, ?> visitor);

     /**
      * <p>Visits {@code visitor} for all referrers.</p>
@@ -339,7 +339,7 @@
      * @param visitor the visitor. Ignored, if null.
      * @since 13806
      */
-    void visitReferrers(PrimitiveVisitor visitor);
+    void visitReferrers(PrimitiveVisitor<?, ?, ?> visitor);

     /**
      * Replies the name of this primitive. The default implementation replies the value
@@ -478,7 +478,7 @@
      * @return OsmData this primitive is part of.
      * @since 13807
      */
-    OsmData<?, ?, ?, ?> getDataSet();
+    <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> OsmData<O, N, W, R> getDataSet();

     /**
      * Returns {@link #getKeys()} for which {@code key} does not fulfill uninteresting criteria.
Index: src/org/openstreetmap/josm/data/osm/IRelation.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/IRelation.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/IRelation.java	(working copy)
@@ -14,7 +14,7 @@
  * @param <M> Type of OSM relation member
  * @since 4098
  */
-public interface IRelation<M extends IRelationMember<?>> extends IPrimitive {
+public interface IRelation<M extends IRelationMember<?, ?, ?, ?>> extends IPrimitive {

     /**
      * Returns the number of members.
Index: src/org/openstreetmap/josm/data/osm/IRelationMember.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/IRelationMember.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/IRelationMember.java	(working copy)
@@ -5,10 +5,13 @@

 /**
  * IRelationMember captures the common functions of {@link RelationMember} and {@link RelationMemberData}.
- * @param <P> the base type of OSM primitives
+ * @param <O> the base type of primitives
+ * @param <N> type representing nodes
+ * @param <W> type representing ways
+ * @param <R> type representing relations
  * @since 13677
  */
-public interface IRelationMember<P extends IPrimitive> extends PrimitiveId {
+public interface IRelationMember<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> extends PrimitiveId {

     /**
      * Returns the role of this relation member.
@@ -40,6 +43,13 @@
     boolean isNode();

     /**
+     * Returns the relation member as a node.
+     * @return Member as node
+     * @since 1937 (RelationMember), xxx (IRelationMember)
+     */
+    N getNode();
+
+    /**
      * Determines if this relation member is a way.
      * @return True if member is way
      */
@@ -46,6 +56,13 @@
     boolean isWay();

     /**
+     * Returns the relation member as a way.
+     * @return Member as way
+     * @since 1937 (RelationMember), xxx (IRelationMember)
+     */
+    W getWay();
+
+    /**
      * Determines if this relation member is a relation.
      * @return True if member is relation
      */
@@ -52,6 +69,13 @@
     boolean isRelation();

     /**
+     * Returns the relation member as a relation.
+     * @return Member as relation
+     * @since 1937 (RelationMember), xxx (IRelationMember)
+     */
+    R getRelation();
+
+    /**
      * Returns type of member for icon display.
      * @return type of member for icon display
      * @since 13766 (IRelationMember)
@@ -65,5 +89,5 @@
      * @return Member. Returned value is never null.
      * @since 13766 (IRelationMember)
      */
-    P getMember();
+    O getMember();
 }
Index: src/org/openstreetmap/josm/data/osm/IWay.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/IWay.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/IWay.java	(working copy)
@@ -1,8 +1,11 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.osm;

+import java.util.Collection;
 import java.util.List;

+import org.openstreetmap.josm.tools.Pair;
+
 /**
  * IWay captures the common functions of {@link Way} and {@link WayData}.
  * @param <N> type of OSM node
@@ -132,4 +135,50 @@
      * @since 13922
      */
     boolean isInnerNode(INode n);
+
+    /**
+     * Removes the given {@link Node} from this way. Ignored, if n is null.
+     * @param n The node to remove. Ignored, if null
+     * @since 1463 Way, xxx IWay
+     */
+    public void removeNode(N n);
+
+    /**
+     * Removes the given set of {@link Node nodes} from this way. Ignored, if selection is null.
+     * @param selection The selection of nodes to remove. Ignored, if null
+     * @since 5408 Way, xxx IWay
+     */
+    public void removeNodes(Collection<N> selection);
+
+    /**
+     * Adds a node to the end of the list of nodes. Ignored, if n is null.
+     *
+     * @param n the node. Ignored, if null
+     * @throws IllegalStateException if this way is marked as incomplete. We can't add a node
+     * to an incomplete way
+     * @since 1313 Way, xxx IWay
+     */
+    void addNode(N n);
+
+    /**
+     * Adds a node at position offs.
+     *
+     * @param offs the offset
+     * @param n the node. Ignored, if null.
+     * @throws IllegalStateException if this way is marked as incomplete. We can't add a node
+     * to an incomplete way
+     * @throws IndexOutOfBoundsException if offs is out of bounds
+     * @since 1313 Way, xxx IWay
+     */
+    void addNode(int offs, N n);
+
+    /**
+     * Replies the ordered {@link List} of chunks of this way. Each chunk is replied as a {@link Pair} of {@link Node nodes}.
+     * @param sort If true, the nodes of each pair are sorted as defined by {@link Pair#sort}.
+     *             If false, Pair.a and Pair.b are in the way order
+     *             (i.e for a given Pair(n), Pair(n-1).b == Pair(n).a, Pair(n).b == Pair(n+1).a, etc.)
+     * @return The ordered list of chunks of this way.
+     * @since 3348 (Way), xxx (IWay)
+     */
+    List<Pair<N, N>> getNodePairs(boolean sort);
 }
Index: src/org/openstreetmap/josm/data/osm/Node.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/Node.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/Node.java	(working copy)
@@ -10,7 +10,6 @@
 import java.util.function.Predicate;
 import java.util.stream.Collectors;

-import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
@@ -405,26 +404,6 @@
         return referrers(Way.class).collect(Collectors.toList());
     }

-    /**
-     * Determines if this node is outside of the world. See also #13538.
-     * @return <code>true</code>, if the coordinate is outside the world, compared by using lat/lon and east/north
-     * @since 14960
-     */
-    public boolean isOutSideWorld() {
-        LatLon ll = getCoor();
-        if (ll != null) {
-            Bounds b = ProjectionRegistry.getProjection().getWorldBoundsLatLon();
-            if (lat() < b.getMinLat() || lat() > b.getMaxLat() || lon() < b.getMinLon() || lon() > b.getMaxLon()) {
-                return true;
-            }
-            if (!ProjectionRegistry.getProjection().latlon2eastNorth(ll).equalsEpsilon(getEastNorth(), 1.0)) {
-                // we get here if a node was moved or created left from -180 or right from +180
-                return true;
-            }
-        }
-        return false;
-    }
-
     @Override
     public UniqueIdGenerator getIdGenerator() {
         return idGenerator;
Index: src/org/openstreetmap/josm/data/osm/NodePositionComparator.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/NodePositionComparator.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/NodePositionComparator.java	(working copy)
@@ -9,12 +9,12 @@
  *
  * @author viesturs
  */
-public class NodePositionComparator implements Comparator<Node>, Serializable {
+public class NodePositionComparator<N extends INode> implements Comparator<N>, Serializable {

     private static final long serialVersionUID = 1L;

     @Override
-    public int compare(Node n1, Node n2) {
+    public int compare(N n1, N n2) {

         if (n1.getCoor().equalsEpsilon(n2.getCoor()))
             return 0;
Index: src/org/openstreetmap/josm/data/osm/OsmData.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/OsmData.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/OsmData.java	(working copy)
@@ -61,6 +61,12 @@
     void addPrimitive(O primitive);

     /**
+     * Removes a primitive
+     * @param primitive the primitive
+     */
+    void removePrimitive(O primitive);
+
+    /**
      * Removes all primitives.
      */
     void clear();
@@ -269,7 +275,7 @@
      *
      * @return unmodifiable collection of WaySegments
      */
-    Collection<WaySegment> getHighlightedVirtualNodes();
+    Collection<WaySegment<N, W>> getHighlightedVirtualNodes();

     /**
      * Returns an unmodifiable collection of WaySegments that should be highlighted.
@@ -276,13 +282,13 @@
      *
      * @return unmodifiable collection of WaySegments
      */
-    Collection<WaySegment> getHighlightedWaySegments();
+    Collection<WaySegment<N, W>> getHighlightedWaySegments();

     /**
      * clear all highlights of virtual nodes
      */
     default void clearHighlightedVirtualNodes() {
-        setHighlightedVirtualNodes(new ArrayList<WaySegment>());
+        setHighlightedVirtualNodes(new ArrayList<WaySegment<N, W>>());
     }

     /**
@@ -289,7 +295,7 @@
      * clear all highlights of way segments
      */
     default void clearHighlightedWaySegments() {
-        setHighlightedWaySegments(new ArrayList<WaySegment>());
+        setHighlightedWaySegments(new ArrayList<WaySegment<N, W>>());
     }

     /**
@@ -297,13 +303,13 @@
      * *WaySegments* to avoid a VirtualNode class that wouldn't have much use otherwise.
      * @param waySegments Collection of way segments
      */
-    void setHighlightedVirtualNodes(Collection<WaySegment> waySegments);
+    void setHighlightedVirtualNodes(Collection<WaySegment<N, W>> waySegments);

     /**
      * set what virtual ways should be highlighted.
      * @param waySegments Collection of way segments
      */
-    void setHighlightedWaySegments(Collection<WaySegment> waySegments);
+    void setHighlightedWaySegments(Collection<WaySegment<N, W>> waySegments);

     /**
      * Adds a listener that gets notified whenever way segment / virtual nodes highlights change.
Index: src/org/openstreetmap/josm/data/osm/PrimitiveData.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/PrimitiveData.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/PrimitiveData.java	(working copy)
@@ -168,7 +168,7 @@
     }

     @Override
-    public OsmData<?, ?, ?, ?> getDataSet() {
+    public <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> OsmData<O, N, W, R> getDataSet() {
         return null;
     }

Index: src/org/openstreetmap/josm/data/osm/RelationMember.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/RelationMember.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/RelationMember.java	(working copy)
@@ -11,7 +11,7 @@
  * Since membership may be qualified by a "role", a simple list is not sufficient.
  * @since 343
  */
-public class RelationMember implements IRelationMember<OsmPrimitive> {
+public class RelationMember implements IRelationMember<OsmPrimitive, Node, Way, Relation> {

     /**
      *
@@ -66,6 +66,7 @@
      * @return Member as node
      * @since 1937
      */
+    @Override
     public Node getNode() {
         return (Node) member;
     }
Index: src/org/openstreetmap/josm/data/osm/Way.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/Way.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/Way.java	(working copy)
@@ -5,6 +5,7 @@

 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -151,14 +152,7 @@
         return neigh;
     }

-    /**
-     * Replies the ordered {@link List} of chunks of this way. Each chunk is replied as a {@link Pair} of {@link Node nodes}.
-     * @param sort If true, the nodes of each pair are sorted as defined by {@link Pair#sort}.
-     *             If false, Pair.a and Pair.b are in the way order
-     *             (i.e for a given Pair(n), Pair(n-1).b == Pair(n).a, Pair(n).b == Pair(n+1).a, etc.)
-     * @return The ordered list of chunks of this way.
-     * @since 3348
-     */
+    @Override
     public List<Pair<Node, Node>> getNodePairs(boolean sort) {
         List<Pair<Node, Node>> chunkSet = new ArrayList<>();
         if (isIncomplete()) return chunkSet;
@@ -182,7 +176,7 @@
         visitor.visit(this);
     }

-    @Override public void accept(PrimitiveVisitor visitor) {
+    @Override public void accept(PrimitiveVisitor<?, ?, ?> visitor) {
         visitor.visit(this);
     }

@@ -332,11 +326,7 @@
         return true;
     }

-    /**
-     * Removes the given {@link Node} from this way. Ignored, if n is null.
-     * @param n The node to remove. Ignored, if null
-     * @since 1463
-     */
+    @Override
     public void removeNode(Node n) {
         checkDatasetNotReadOnly();
         if (n == null || isIncomplete()) return;
@@ -361,12 +351,8 @@
         }
     }

-    /**
-     * Removes the given set of {@link Node nodes} from this way. Ignored, if selection is null.
-     * @param selection The selection of nodes to remove. Ignored, if null
-     * @since 5408
-     */
-    public void removeNodes(Set<? extends Node> selection) {
+    @Override
+    public void removeNodes(Collection<Node> selection) {
         checkDatasetNotReadOnly();
         if (selection == null || isIncomplete()) return;
         boolean locked = writeLock();
@@ -395,14 +381,7 @@
         }
     }

-    /**
-     * Adds a node to the end of the list of nodes. Ignored, if n is null.
-     *
-     * @param n the node. Ignored, if null
-     * @throws IllegalStateException if this way is marked as incomplete. We can't add a node
-     * to an incomplete way
-     * @since 1313
-     */
+    @Override
     public void addNode(Node n) {
         checkDatasetNotReadOnly();
         if (n == null) return;
@@ -421,16 +400,7 @@
         }
     }

-    /**
-     * Adds a node at position offs.
-     *
-     * @param offs the offset
-     * @param n the node. Ignored, if null.
-     * @throws IllegalStateException if this way is marked as incomplete. We can't add a node
-     * to an incomplete way
-     * @throws IndexOutOfBoundsException if offs is out of bounds
-     * @since 1313
-     */
+    @Override
     public void addNode(int offs, Node n) {
         checkDatasetNotReadOnly();
         if (n == null) return;
Index: src/org/openstreetmap/josm/data/osm/WaySegment.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/WaySegment.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/WaySegment.java	(working copy)
@@ -2,17 +2,20 @@
 package org.openstreetmap.josm.data.osm;

 import java.awt.geom.Line2D;
+import java.lang.reflect.InvocationTargetException;
 import java.util.Objects;

+import org.openstreetmap.josm.tools.Logging;
+
 /**
  * A segment consisting of 2 consecutive nodes out of a way.
  */
-public final class WaySegment implements Comparable<WaySegment> {
+public final class WaySegment<N extends INode, W extends IWay<N>> implements Comparable<WaySegment<N, W>> {

     /**
      * The way.
      */
-    public final Way way;
+    public final W way;

     /**
      * The index of one of the 2 nodes in the way.  The other node has the
@@ -26,7 +29,7 @@
      * @param i The node lower index
      * @throws IllegalArgumentException in case of invalid index
      */
-    public WaySegment(Way w, int i) {
+    public WaySegment(W w, int i) {
         way = w;
         lowerIndex = i;
         if (i < 0 || i >= w.getNodesCount() - 1) {
@@ -38,7 +41,7 @@
      * Returns the first node of the way segment.
      * @return the first node
      */
-    public Node getFirstNode() {
+    public N getFirstNode() {
         return way.getNode(lowerIndex);
     }

@@ -46,7 +49,7 @@
      * Returns the second (last) node of the way segment.
      * @return the second node
      */
-    public Node getSecondNode() {
+    public N getSecondNode() {
         return way.getNode(lowerIndex + 1);
     }

@@ -58,12 +61,12 @@
      * @return way segment
      * @throws IllegalArgumentException if the node pair is not part of way
      */
-    public static WaySegment forNodePair(Way way, Node first, Node second) {
+    public static <N extends INode, W extends IWay<N>> WaySegment<N, W> forNodePair(W way, N first, N second) {
         int endIndex = way.getNodesCount() - 1;
         while (endIndex > 0) {
             final int indexOfFirst = way.getNodes().subList(0, endIndex).lastIndexOf(first);
             if (second.equals(way.getNode(indexOfFirst + 1))) {
-                return new WaySegment(way, indexOfFirst);
+                return new WaySegment<>(way, indexOfFirst);
             }
             endIndex--;
         }
@@ -72,13 +75,21 @@

     /**
      * Returns this way segment as complete way.
-     * @return the way segment as {@code Way}
+     * @return the way segment as {@code W}
      */
-    public Way toWay() {
-        Way w = new Way();
-        w.addNode(getFirstNode());
-        w.addNode(getSecondNode());
-        return w;
+    public W toWay() {
+        try {
+            /** way is of type W, so it should always create a new W */
+            @SuppressWarnings("unchecked")
+            W w = (W) way.getClass().getConstructor().newInstance();
+            w.addNode(getFirstNode());
+            w.addNode(getSecondNode());
+            return w;
+        } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
+                | NoSuchMethodException | SecurityException e) {
+            Logging.trace(e);
+            return null;
+        }
     }

     @Override
@@ -85,7 +96,7 @@
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
-        WaySegment that = (WaySegment) o;
+        WaySegment<?, ?> that = (WaySegment<?, ?>) o;
         return lowerIndex == that.lowerIndex &&
                 Objects.equals(way, that.way);
     }
@@ -96,7 +107,7 @@
     }

     @Override
-    public int compareTo(WaySegment o) {
+    public int compareTo(WaySegment<N, W> o) {
         return o == null ? -1 : (equals(o) ? 0 : toWay().compareTo(o.toWay()));
     }

Index: src/org/openstreetmap/josm/data/osm/visitor/AllNodesVisitor.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/visitor/AllNodesVisitor.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/visitor/AllNodesVisitor.java	(working copy)
@@ -4,11 +4,11 @@
 import java.util.Collection;
 import java.util.HashSet;

-import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Relation;
-import org.openstreetmap.josm.data.osm.RelationMember;
-import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IRelationMember;
+import org.openstreetmap.josm.data.osm.IWay;

 /**
  * Collect all nodes a specific osm primitive has.
@@ -15,18 +15,18 @@
  *
  * @author imi
  */
-public class AllNodesVisitor implements OsmPrimitiveVisitor {
+public class AllNodesVisitor<N extends INode, W extends IWay<N>, R extends IRelation<? extends IRelationMember<?, N, W, R>>> implements PrimitiveVisitor<N, W, R> {

     /**
      * The resulting nodes collected so far.
      */
-    public Collection<Node> nodes = new HashSet<>();
+    public Collection<N> nodes = new HashSet<>();

     /**
      * Nodes have only itself as nodes.
      */
     @Override
-    public void visit(Node n) {
+    public void visit(N n) {
         nodes.add(n);
     }

@@ -34,9 +34,9 @@
      * Ways have their way nodes.
      */
     @Override
-    public void visit(Way w) {
+    public void visit(W w) {
         if (w.isIncomplete()) return;
-        for (Node n : w.getNodes()) {
+        for (N n : w.getNodes()) {
             visit(n);
         }
     }
@@ -47,8 +47,8 @@
      * if so, use AutomatchVisitor!
      */
     @Override
-    public void visit(Relation e) {
-        for (RelationMember m : e.getMembers()) {
+    public void visit(R e) {
+        for (IRelationMember<?, N, W, R> m : e.getMembers()) {
             if (m.isNode()) visit(m.getNode());
         }
     }
@@ -58,9 +58,9 @@
      * @param osms The OSM primitives to inspect
      * @return All nodes the given primitives have.
      */
-    public static Collection<Node> getAllNodes(Collection<? extends OsmPrimitive> osms) {
-        AllNodesVisitor v = new AllNodesVisitor();
-        for (OsmPrimitive osm : osms) {
+    public static <N extends INode, W extends IWay<N>, R extends IRelation<? extends IRelationMember<?, N, W, R>>> Collection<N> getAllNodes(Collection<? extends IPrimitive> osms) {
+        AllNodesVisitor<N, W, R> v = new AllNodesVisitor<>();
+        for (IPrimitive osm : osms) {
             osm.accept(v);
         }
         return v.nodes;
Index: src/org/openstreetmap/josm/data/osm/visitor/BoundingXYVisitor.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/visitor/BoundingXYVisitor.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/visitor/BoundingXYVisitor.java	(working copy)
@@ -14,10 +14,7 @@
 import org.openstreetmap.josm.data.osm.IRelation;
 import org.openstreetmap.josm.data.osm.IRelationMember;
 import org.openstreetmap.josm.data.osm.IWay;
-import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Relation;
-import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.projection.ProjectionRegistry;
 import org.openstreetmap.josm.spi.preferences.Config;

@@ -26,7 +23,7 @@
  * EastNorth values as reference.
  * @author imi
  */
-public class BoundingXYVisitor implements OsmPrimitiveVisitor, PrimitiveVisitor {
+public class BoundingXYVisitor implements PrimitiveVisitor<INode, IWay<INode>, IRelation<?>> {
     /** default value for setting "edit.zoom-enlarge-bbox" */
     private static final double ENLARGE_DEFAULT = 0.0002;

@@ -33,27 +30,12 @@
     private ProjectionBounds bounds;

     @Override
-    public void visit(Node n) {
-        visit((ILatLon) n);
-    }
-
-    @Override
-    public void visit(Way w) {
-        visit((IWay<?>) w);
-    }
-
-    @Override
-    public void visit(Relation r) {
-        visit((IRelation<?>) r);
-    }
-
-    @Override
     public void visit(INode n) {
         visit((ILatLon) n);
     }

     @Override
-    public void visit(IWay<?> w) {
+    public void visit(IWay<INode> w) {
         if (w.isIncomplete()) return;
         for (INode n : w.getNodes()) {
             visit(n);
@@ -63,7 +45,7 @@
     @Override
     public void visit(IRelation<?> r) {
         // only use direct members
-        for (IRelationMember<?> m : r.getMembers()) {
+        for (IRelationMember<?, ?, ?, ?> m : r.getMembers()) {
             if (!m.isRelation()) {
                 m.getMember().accept(this);
             }
Index: src/org/openstreetmap/josm/data/osm/visitor/OsmPrimitiveVisitor.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/visitor/OsmPrimitiveVisitor.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/visitor/OsmPrimitiveVisitor.java	(working copy)
@@ -10,11 +10,12 @@
  * types {@link Node}, {@link Way} and {@link Relation}.
  * @since 12810
  */
-public interface OsmPrimitiveVisitor {
+public interface OsmPrimitiveVisitor extends PrimitiveVisitor<Node, Way, Relation> {
     /**
      * Visiting call for points.
      * @param n The node to inspect.
      */
+    @Override
     void visit(Node n);

     /**
@@ -22,6 +23,7 @@
      * @param w The way to inspect.
      * @since 64
      */
+    @Override
     void visit(Way w);

     /**
@@ -29,6 +31,7 @@
      * @param r The relation to inspect.
      * @since 343
      */
+    @Override
     void visit(Relation r);

 }
Index: src/org/openstreetmap/josm/data/osm/visitor/PrimitiveVisitor.java
===================================================================
--- src/org/openstreetmap/josm/data/osm/visitor/PrimitiveVisitor.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/osm/visitor/PrimitiveVisitor.java	(working copy)
@@ -7,25 +7,25 @@

 /**
  * OSM primitives interfaces visitor, following conventional <a href="https://en.wikipedia.org/wiki/Visitor_pattern">visitor design pattern</a>.
- * @since 4100
+ * @since 4100, xxx for generics
  */
-public interface PrimitiveVisitor {
+public interface PrimitiveVisitor<N extends INode, W extends IWay<N>, R extends IRelation<?>> {

     /**
      * Visiting call for nodes.
      * @param n The node to inspect.
      */
-    void visit(INode n);
+    void visit(N n);

     /**
      * Visiting call for ways.
      * @param w The way to inspect.
      */
-    void visit(IWay<?> w);
+    void visit(W w);

     /**
      * Visiting call for relations.
      * @param r The relation to inspect.
      */
-    void visit(IRelation<?> r);
+    void visit(R r);
 }
Index: src/org/openstreetmap/josm/data/validation/Test.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/Test.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/validation/Test.java	(working copy)
@@ -282,7 +282,7 @@
      * @param testError error to fix
      * @return The command to fix the error
      */
-    public Command fixError(TestError testError) {
+    public Command<OsmPrimitive, Node, Way, Relation> fixError(TestError testError) {
         return null;
     }

@@ -334,7 +334,7 @@
      * @param primitives The primitives wanted for deletion
      * @return a Delete command on all primitives that have not yet been deleted, or null otherwise
      */
-    protected final Command deletePrimitivesIfNeeded(Collection<? extends OsmPrimitive> primitives) {
+    protected final Command<OsmPrimitive, Node, Way, Relation> deletePrimitivesIfNeeded(Collection<? extends OsmPrimitive> primitives) {
         Collection<OsmPrimitive> primitivesToDelete = new ArrayList<>();
         for (OsmPrimitive p : primitives) {
             if (!p.isDeleted()) {
Index: src/org/openstreetmap/josm/data/validation/TestError.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/TestError.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/validation/TestError.java	(working copy)
@@ -51,7 +51,7 @@
     /** If this error is selected */
     private boolean selected;
     /** Supplying a command to fix the error */
-    private final Supplier<Command> fixingCommand;
+    private final Supplier<Command<OsmPrimitive, Node, Way, Relation>> fixingCommand;

     /**
      * A builder for a {@code TestError}.
@@ -66,7 +66,7 @@
         private String descriptionEn;
         private Collection<? extends OsmPrimitive> primitives;
         private Collection<?> highlighted;
-        private Supplier<Command> fixingCommand;
+        private Supplier<Command<OsmPrimitive, Node, Way, Relation>> fixingCommand;

         Builder(Test tester, Severity severity, int code) {
             this.tester = tester;
@@ -172,7 +172,7 @@
          * @return {@code this}
          * @see ValidatorVisitor#visit(WaySegment)
          */
-        public Builder highlightWaySegments(Collection<WaySegment> highlighted) {
+        public Builder highlightWaySegments(Collection<WaySegment<Node, Way>> highlighted) {
             CheckParameterUtil.ensureParameterNotNull(highlighted, "highlighted");
             this.highlighted = highlighted;
             return this;
@@ -209,7 +209,7 @@
          * @param fixingCommand the fix supplier. Can be null
          * @return {@code this}
          */
-        public Builder fix(Supplier<Command> fixingCommand) {
+        public Builder fix(Supplier<Command<OsmPrimitive, Node, Way, Relation>> fixingCommand) {
             CheckParameterUtil.ensureThat(this.fixingCommand == null, "fixingCommand already set");
             this.fixingCommand = fixingCommand;
             return this;
@@ -409,9 +409,9 @@
      *
      * @return The command to fix the error
      */
-    public Command getFix() {
+    public Command<OsmPrimitive, Node, Way, Relation> getFix() {
         // obtain fix from the error
-        final Command fix = fixingCommand != null ? fixingCommand.get() : null;
+        final Command<OsmPrimitive, Node, Way, Relation> fix = fixingCommand != null ? fixingCommand.get() : null;
         if (fix != null) {
             return fix;
         }
@@ -441,7 +441,7 @@
             if (o instanceof OsmPrimitive) {
                 v.visit((OsmPrimitive) o);
             } else if (o instanceof WaySegment) {
-                v.visit((WaySegment) o);
+                v.visit((WaySegment<Node, Way>) o);
             } else if (o instanceof List<?>) {
                 v.visit((List<Node>) o);
             } else if (o instanceof Area) {
Index: src/org/openstreetmap/josm/data/validation/tests/CrossingWays.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/tests/CrossingWays.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/validation/tests/CrossingWays.java	(working copy)
@@ -14,6 +14,7 @@
 import java.util.Set;

 import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.OsmUtils;
 import org.openstreetmap.josm.data.osm.Relation;
@@ -75,9 +76,9 @@
     }

     /** All way segments, grouped by cells */
-    private final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
+    private final Map<Point2D, List<WaySegment<Node, Way>>> cellSegments = new HashMap<>(1000);
     /** The already detected ways in error */
-    private final Map<List<Way>, List<WaySegment>> seenWays = new HashMap<>(50);
+    private final Map<List<Way>, List<WaySegment<Node, Way>>> seenWays = new HashMap<>(50);

     protected final int code;

@@ -364,7 +365,7 @@

         int nodesSize = w.getNodesCount();
         for (int i = 0; i < nodesSize - 1; i++) {
-            final WaySegment es1 = new WaySegment(w, i);
+            final WaySegment<Node, Way> es1 = new WaySegment<>(w, i);
             final EastNorth en1 = es1.getFirstNode().getEastNorth();
             final EastNorth en2 = es1.getSecondNode().getEastNorth();
             if (en1 == null || en2 == null) {
@@ -371,10 +372,10 @@
                 Logging.warn("Crossing ways test skipped " + es1);
                 continue;
             }
-            for (List<WaySegment> segments : getSegments(cellSegments, en1, en2)) {
-                for (WaySegment es2 : segments) {
+            for (List<WaySegment<Node, Way>> segments : getSegments(cellSegments, en1, en2)) {
+                for (WaySegment<Node, Way> es2 : segments) {
                     List<Way> prims;
-                    List<WaySegment> highlight;
+                    List<WaySegment<Node, Way>> highlight;

                     if (!es1.intersects(es2) || ignoreWaySegmentCombination(es1.way, es2.way)) {
                         continue;
@@ -419,8 +420,8 @@
      * @param n2 The second EastNorth
      * @return A list with all the cells the segment crosses
      */
-    public static List<List<WaySegment>> getSegments(Map<Point2D, List<WaySegment>> cellSegments, EastNorth n1, EastNorth n2) {
-        List<List<WaySegment>> cells = new ArrayList<>();
+    public static List<List<WaySegment<Node, Way>>> getSegments(Map<Point2D, List<WaySegment<Node, Way>>> cellSegments, EastNorth n1, EastNorth n2) {
+        List<List<WaySegment<Node, Way>>> cells = new ArrayList<>();
         for (Point2D cell : ValUtil.getSegmentCells(n1, n2, OsmValidator.getGridDetail())) {
             cells.add(cellSegments.computeIfAbsent(cell, k -> new ArrayList<>()));
         }
@@ -434,11 +435,11 @@
      * @param crossingWays map to collect crossing ways and related segments
      * @param findSharedWaySegments true: find shared way segments instead of crossings
      */
-    public static void findIntersectingWay(Way w, Map<Point2D, List<WaySegment>> cellSegments,
-            Map<List<Way>, List<WaySegment>> crossingWays, boolean findSharedWaySegments) {
+    public static void findIntersectingWay(Way w, Map<Point2D, List<WaySegment<Node, Way>>> cellSegments,
+            Map<List<Way>, List<WaySegment<Node, Way>>> crossingWays, boolean findSharedWaySegments) {
         int nodesSize = w.getNodesCount();
         for (int i = 0; i < nodesSize - 1; i++) {
-            final WaySegment es1 = new WaySegment(w, i);
+            final WaySegment<Node, Way> es1 = new WaySegment<>(w, i);
             final EastNorth en1 = es1.getFirstNode().getEastNorth();
             final EastNorth en2 = es1.getSecondNode().getEastNorth();
             if (en1 == null || en2 == null) {
@@ -445,10 +446,10 @@
                 Logging.warn("Crossing ways test skipped " + es1);
                 continue;
             }
-            for (List<WaySegment> segments : CrossingWays.getSegments(cellSegments, en1, en2)) {
-                for (WaySegment es2 : segments) {
+            for (List<WaySegment<Node, Way>> segments : CrossingWays.getSegments(cellSegments, en1, en2)) {
+                for (WaySegment<Node, Way> es2 : segments) {

-                    List<WaySegment> highlight;
+                    List<WaySegment<Node, Way>> highlight;
                     if (es2.way == w // reported by CrossingWays.SelfIntersection
                             || (findSharedWaySegments && !es1.isSimilar(es2))
                             || (!findSharedWaySegments && !es1.intersects(es2)))
Index: src/org/openstreetmap/josm/data/validation/tests/OverlappingWays.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/tests/OverlappingWays.java	(revision 16301)
+++ src/org/openstreetmap/josm/data/validation/tests/OverlappingWays.java	(working copy)
@@ -40,7 +40,7 @@
 public class OverlappingWays extends Test {

     /** Bag of all way segments */
-    private MultiMap<Pair<Node, Node>, WaySegment> nodePairs;
+    private MultiMap<Pair<Node, Node>, WaySegment<Node, Way>> nodePairs;

     protected static final int OVERLAPPING_HIGHWAY = 101;
     protected static final int OVERLAPPING_RAILWAY = 102;
@@ -77,21 +77,21 @@

     @Override
     public void endTest() {
-        Map<List<Way>, Set<WaySegment>> seenWays = new HashMap<>(500);
+        Map<List<Way>, Set<WaySegment<Node, Way>>> seenWays = new HashMap<>(500);

         Collection<TestError> preliminaryErrors = new ArrayList<>();
-        for (Set<WaySegment> duplicated : nodePairs.values()) {
+        for (Set<WaySegment<Node, Way>> duplicated : nodePairs.values()) {
             int ways = duplicated.size();

             if (ways > 1) {
                 List<OsmPrimitive> prims = new ArrayList<>();
                 List<Way> currentWays = new ArrayList<>();
-                Collection<WaySegment> highlight;
+                Collection<WaySegment<Node, Way>> highlight;
                 int highway = 0;
                 int railway = 0;
                 int area = 0;

-                for (WaySegment ws : duplicated) {
+                for (WaySegment<Node, Way> ws : duplicated) {
                     if (ws.way.hasKey(HIGHWAY)) {
                         highway++;
                     } else if (ws.way.hasKey(RAILWAY)) {
@@ -166,9 +166,9 @@
         nodePairs = null;
     }

-    protected static Set<WaySegment> checkDuplicateWaySegment(Way w) {
+    protected static Set<WaySegment<Node, Way>> checkDuplicateWaySegment(Way w) {
         // test for ticket #4959
-        Set<WaySegment> segments = new TreeSet<>((o1, o2) -> {
+        Set<WaySegment<Node, Way>> segments = new TreeSet<>((o1, o2) -> {
             final List<Node> n1 = Arrays.asList(o1.getFirstNode(), o1.getSecondNode());
             final List<Node> n2 = Arrays.asList(o2.getFirstNode(), o2.getSecondNode());
             Collections.sort(n1);
@@ -177,10 +177,10 @@
             final int second = n1.get(1).compareTo(n2.get(1));
             return first != 0 ? first : second;
         });
-        final Set<WaySegment> duplicateWaySegments = new HashSet<>();
+        final Set<WaySegment<Node, Way>> duplicateWaySegments = new HashSet<>();

         for (int i = 0; i < w.getNodesCount() - 1; i++) {
-            final WaySegment segment = new WaySegment(w, i);
+            final WaySegment<Node, Way> segment = new WaySegment<>(w, i);
             final boolean wasInSet = !segments.add(segment);
             if (wasInSet) {
                 duplicateWaySegments.add(segment);
@@ -192,7 +192,7 @@
     @Override
     public void visit(Way w) {

-        final Set<WaySegment> duplicateWaySegment = checkDuplicateWaySegment(w);
+        final Set<WaySegment<Node, Way>> duplicateWaySegment = checkDuplicateWaySegment(w);
         if (!duplicateWaySegment.isEmpty()) {
             errors.add(TestError.builder(this, Severity.ERROR, DUPLICATE_WAY_SEGMENT)
                     .message(tr("Way contains segment twice"))
@@ -211,7 +211,7 @@
                 continue;
             }
             nodePairs.put(Pair.sort(new Pair<>(lastN, n)),
-                    new WaySegment(w, i));
+                    new WaySegment<>(w, i));
             lastN = n;
         }
     }
Index: src/org/openstreetmap/josm/gui/NavigatableComponent.java
===================================================================
--- src/org/openstreetmap/josm/gui/NavigatableComponent.java	(revision 16301)
+++ src/org/openstreetmap/josm/gui/NavigatableComponent.java	(working copy)
@@ -39,10 +39,12 @@
 import org.openstreetmap.josm.data.coor.ILatLon;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.BBox;
-import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
 import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.WaySegment;
 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
@@ -90,7 +92,7 @@
     /**
      * To determine if a primitive is currently selectable.
      */
-    public transient Predicate<OsmPrimitive> isSelectablePredicate = prim -> {
+    public transient Predicate<IPrimitive> isSelectablePredicate = prim -> {
         if (!prim.isSelectable()) return false;
         // if it isn't displayed on screen, you cannot click on it
         MapCSSStyleSource.STYLE_SOURCE_LOCK.readLock().lock();
@@ -549,7 +551,7 @@
      * @param n The node, where this geopoint would be drawn.
      * @return The point on screen where "node" would be drawn, relative to the own top/left.
      */
-    public Point2D getPoint2D(Node n) {
+    public Point2D getPoint2D(INode n) {
         return getPoint2D(n.getEastNorth());
     }

@@ -590,9 +592,9 @@
      * looses precision, may overflow (depends on p and current scale)
      * @param n node
      * @return point
-     * @see #getPoint2D(Node)
+     * @see #getPoint2D(INode)
      */
-    public Point getPoint(Node n) {
+    public Point getPoint(INode n) {
         Point2D d = getPoint2D(n);
         return new Point((int) d.getX(), (int) d.getY());
     }
@@ -982,15 +984,15 @@
      *
      * @return a sorted map with the keys representing the distance of their associated nodes to point p.
      */
-    private Map<Double, List<Node>> getNearestNodesImpl(Point p, Predicate<OsmPrimitive> predicate) {
-        Map<Double, List<Node>> nearestMap = new TreeMap<>();
-        DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
+    private Map<Double, List<INode>> getNearestNodesImpl(Point p, Predicate<IPrimitive> predicate) {
+        Map<Double, List<INode>> nearestMap = new TreeMap<>();
+        OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData();

         if (ds != null) {
             double dist, snapDistanceSq = PROP_SNAP_DISTANCE.get();
             snapDistanceSq *= snapDistanceSq;

-            for (Node n : ds.searchNodes(getBBox(p, PROP_SNAP_DISTANCE.get()))) {
+            for (INode n : ds.searchNodes(getBBox(p, PROP_SNAP_DISTANCE.get()))) {
                 if (predicate.test(n)
                         && (dist = getPoint2D(n).distanceSq(p)) < snapDistanceSq) {
                     nearestMap.computeIfAbsent(dist, k -> new LinkedList<>()).add(n);
@@ -1014,20 +1016,20 @@
      *      dist(nearest) to dist(nearest)+4px around p and
      *      that are not in ignore.
      */
-    public final List<Node> getNearestNodes(Point p,
-            Collection<Node> ignore, Predicate<OsmPrimitive> predicate) {
-        List<Node> nearestList = Collections.emptyList();
+    public final List<INode> getNearestNodes(Point p,
+            Collection<INode> ignore, Predicate<IPrimitive> predicate) {
+        List<INode> nearestList = Collections.emptyList();

         if (ignore == null) {
             ignore = Collections.emptySet();
         }

-        Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
+        Map<Double, List<INode>> nlists = getNearestNodesImpl(p, predicate);
         if (!nlists.isEmpty()) {
             Double minDistSq = null;
-            for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
+            for (Entry<Double, List<INode>> entry : nlists.entrySet()) {
                 Double distSq = entry.getKey();
-                List<Node> nlist = entry.getValue();
+                List<INode> nlist = entry.getValue();

                 // filter nodes to be ignored before determining minDistSq..
                 nlist.removeAll(ignore);
@@ -1060,7 +1062,7 @@
      *      dist(nearest) to dist(nearest)+4px around p.
      * @see #getNearestNodes(Point, Collection, Predicate)
      */
-    public final List<Node> getNearestNodes(Point p, Predicate<OsmPrimitive> predicate) {
+    public final List<INode> getNearestNodes(Point p, Predicate<IPrimitive> predicate) {
         return getNearestNodes(p, null, predicate);
     }

@@ -1084,7 +1086,7 @@
      *
      * @return A node within snap-distance to point p, that is chosen by the algorithm described.
      */
-    public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
+    public final INode getNearestNode(Point p, Predicate<IPrimitive> predicate, boolean useSelected) {
         return getNearestNode(p, predicate, useSelected, null);
     }

@@ -1112,20 +1114,20 @@
      * @return A node within snap-distance to point p, that is chosen by the algorithm described.
      * @since 6065
      */
-    public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate,
-            boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
+    public final INode getNearestNode(Point p, Predicate<IPrimitive> predicate,
+            boolean useSelected, Collection<IPrimitive> preferredRefs) {

-        Map<Double, List<Node>> nlists = getNearestNodesImpl(p, predicate);
+        Map<Double, List<INode>> nlists = getNearestNodesImpl(p, predicate);
         if (nlists.isEmpty()) return null;

         if (preferredRefs != null && preferredRefs.isEmpty()) preferredRefs = null;
-        Node ntsel = null, ntnew = null, ntref = null;
+        INode ntsel = null, ntnew = null, ntref = null;
         boolean useNtsel = useSelected;
         double minDistSq = nlists.keySet().iterator().next();

-        for (Entry<Double, List<Node>> entry : nlists.entrySet()) {
+        for (Entry<Double, List<INode>> entry : nlists.entrySet()) {
             Double distSq = entry.getKey();
-            for (Node nd : entry.getValue()) {
+            for (INode nd : entry.getValue()) {
                 // find the nearest selected node
                 if (ntsel == null && nd.isSelected()) {
                     ntsel = nd;
@@ -1136,8 +1138,8 @@
                     useNtsel |= Utils.equalsEpsilon(distSq, minDistSq);
                 }
                 if (ntref == null && preferredRefs != null && Utils.equalsEpsilon(distSq, minDistSq)) {
-                    List<OsmPrimitive> ndRefs = nd.getReferrers();
-                    for (OsmPrimitive ref: preferredRefs) {
+                    List<? extends IPrimitive> ndRefs = nd.getReferrers();
+                    for (IPrimitive ref: preferredRefs) {
                         if (ndRefs.contains(ref)) {
                             ntref = nd;
                             break;
@@ -1170,7 +1172,7 @@
      *
      * @return The nearest node to point p.
      */
-    public final Node getNearestNode(Point p, Predicate<OsmPrimitive> predicate) {
+    public final INode getNearestNode(Point p, Predicate<IPrimitive> predicate) {
         return getNearestNode(p, predicate, true);
     }

@@ -1184,21 +1186,20 @@
      * @return a sorted map with the keys representing the perpendicular
      *      distance of their associated way segments to point p.
      */
-    private Map<Double, List<WaySegment>> getNearestWaySegmentsImpl(Point p, Predicate<OsmPrimitive> predicate) {
-        Map<Double, List<WaySegment>> nearestMap = new TreeMap<>();
-        DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
+    private <N extends INode, W extends IWay<N>> Map<Double, List<WaySegment<N, W>>> getNearestWaySegmentsImpl(Point p, Predicate<IPrimitive> predicate, OsmData<?, N, W, ?> ds) {
+        Map<Double, List<WaySegment<N, W>>> nearestMap = new TreeMap<>();

         if (ds != null) {
             double snapDistanceSq = Config.getPref().getInt("mappaint.segment.snap-distance", 10);
             snapDistanceSq *= snapDistanceSq;

-            for (Way w : ds.searchWays(getBBox(p, Config.getPref().getInt("mappaint.segment.snap-distance", 10)))) {
+            for (W w : ds.searchWays(getBBox(p, Config.getPref().getInt("mappaint.segment.snap-distance", 10)))) {
                 if (!predicate.test(w)) {
                     continue;
                 }
-                Node lastN = null;
+                N lastN = null;
                 int i = -2;
-                for (Node n : w.getNodes()) {
+                for (N n : w.getNodes()) {
                     i++;
                     if (n.isDeleted() || n.isIncomplete()) { //FIXME: This shouldn't happen, raise exception?
                         continue;
@@ -1224,7 +1225,7 @@
                             >> 32 << 32); // resolution in numbers with large exponent not needed here..

                     if (perDistSq < snapDistanceSq && a < c + snapDistanceSq && b < c + snapDistanceSq) {
-                        nearestMap.computeIfAbsent(perDistSq, k -> new LinkedList<>()).add(new WaySegment(w, i));
+                        nearestMap.computeIfAbsent(perDistSq, k -> new LinkedList<>()).add(new WaySegment<>(w, i));
                     }

                     lastN = n;
@@ -1247,15 +1248,16 @@
      * @return all segments within 10px of p that are not in ignore,
      *          sorted by their perpendicular distance.
      */
-    public final List<WaySegment> getNearestWaySegments(Point p,
-            Collection<WaySegment> ignore, Predicate<OsmPrimitive> predicate) {
-        List<WaySegment> nearestList = new ArrayList<>();
-        List<WaySegment> unselected = new LinkedList<>();
+    public final <N extends INode, W extends IWay<N>> List<WaySegment<N, W>> getNearestWaySegments(Point p,
+            Collection<WaySegment<N, W>> ignore, Predicate<IPrimitive> predicate) {
+        OsmData<?, N, W, ?> ds = MainApplication.getLayerManager().getActiveData();
+        List<WaySegment<N, W>> nearestList = new ArrayList<>();
+        List<WaySegment<N, W>> unselected = new LinkedList<>();

-        for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
+        for (List<WaySegment<N, W>> wss : getNearestWaySegmentsImpl(p, predicate, ds).values()) {
             // put selected waysegs within each distance group first
             // makes the order of nearestList dependent on current selection state
-            for (WaySegment ws : wss) {
+            for (WaySegment<N, W> ws : wss) {
                 (ws.way.isSelected() ? nearestList : unselected).add(ws);
             }
             nearestList.addAll(unselected);
@@ -1277,7 +1279,7 @@
      * @return all segments within 10px of p, sorted by their perpendicular distance.
      * @see #getNearestWaySegments(Point, Collection, Predicate)
      */
-    public final List<WaySegment> getNearestWaySegments(Point p, Predicate<OsmPrimitive> predicate) {
+    public final List<WaySegment<Node, Way>> getNearestWaySegments(Point p, Predicate<IPrimitive> predicate) {
         return getNearestWaySegments(p, null, predicate);
     }

@@ -1292,15 +1294,16 @@
      *      and, depending on use_selected, prefers a selected way segment, if found.
      * @see #getNearestWaySegments(Point, Collection, Predicate)
      */
-    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
-        WaySegment wayseg = null;
-        WaySegment ntsel = null;
+    public final <N extends INode, W extends IWay<N>> WaySegment<N, W> getNearestWaySegment(Point p, Predicate<IPrimitive> predicate, boolean useSelected) {
+        OsmData<?, N, W, ?> ds = MainApplication.getLayerManager().getActiveData();
+        WaySegment<N, W> wayseg = null;
+        WaySegment<N, W> ntsel = null;

-        for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
+        for (List<WaySegment<N, W>> wslist : getNearestWaySegmentsImpl(p, predicate, ds).values()) {
             if (wayseg != null && ntsel != null) {
                 break;
             }
-            for (WaySegment ws : wslist) {
+            for (WaySegment<N, W> ws : wslist) {
                 if (wayseg == null) {
                     wayseg = ws;
                 }
@@ -1328,14 +1331,15 @@
      * @see #getNearestWaySegments(Point, Collection, Predicate)
      * @since 6065
      */
-    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate,
-            boolean useSelected, Collection<OsmPrimitive> preferredRefs) {
-        WaySegment wayseg = null;
+    public final <N extends INode, W extends IWay<N>> WaySegment<N, W> getNearestWaySegment(Point p, Predicate<IPrimitive> predicate,
+            boolean useSelected, Collection<IPrimitive> preferredRefs) {
+        OsmData<?, N, W, ?> ds = MainApplication.getLayerManager().getActiveData();
+        WaySegment<N, W> wayseg = null;
         if (preferredRefs != null && preferredRefs.isEmpty())
             preferredRefs = null;

-        for (List<WaySegment> wslist : getNearestWaySegmentsImpl(p, predicate).values()) {
-            for (WaySegment ws : wslist) {
+        for (List<WaySegment<N, W>> wslist : getNearestWaySegmentsImpl(p, predicate, ds).values()) {
+            for (WaySegment<N, W> ws : wslist) {
                 if (wayseg == null) {
                     wayseg = ws;
                 }
@@ -1347,10 +1351,10 @@
                     if (preferredRefs.contains(ws.getFirstNode()) || preferredRefs.contains(ws.getSecondNode())) {
                         return ws;
                     }
-                    Collection<OsmPrimitive> wayRefs = ws.way.getReferrers();
+                    Collection<? extends IPrimitive> wayRefs = ws.way.getReferrers();
                     // prefer member of the given relations
-                    for (OsmPrimitive ref: preferredRefs) {
-                        if (ref instanceof Relation && wayRefs.contains(ref)) {
+                    for (IPrimitive ref: preferredRefs) {
+                        if (ref instanceof IRelation && wayRefs.contains(ref)) {
                             return ws;
                         }
                     }
@@ -1367,7 +1371,7 @@
      *
      * @return The nearest way segment to point p.
      */
-    public final WaySegment getNearestWaySegment(Point p, Predicate<OsmPrimitive> predicate) {
+    public final <N extends INode, W extends IWay<N>> WaySegment<N, W> getNearestWaySegment(Point p, Predicate<IPrimitive> predicate) {
         return getNearestWaySegment(p, predicate, true);
     }

@@ -1383,13 +1387,14 @@
      * @return all nearest ways to the screen point given that are not in ignore.
      * @see #getNearestWaySegments(Point, Collection, Predicate)
      */
-    public final List<Way> getNearestWays(Point p,
-            Collection<Way> ignore, Predicate<OsmPrimitive> predicate) {
-        List<Way> nearestList = new ArrayList<>();
-        Set<Way> wset = new HashSet<>();
+    public final <N extends INode, W extends IWay<N>> List<W> getNearestWays(Point p,
+            Collection<W> ignore, Predicate<IPrimitive> predicate) {
+        OsmData<?, N, W, ?> ds = MainApplication.getLayerManager().getActiveData();
+        List<W> nearestList = new ArrayList<>();
+        Set<W> wset = new HashSet<>();

-        for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
-            for (WaySegment ws : wss) {
+        for (List<WaySegment<N, W>> wss : getNearestWaySegmentsImpl(p, predicate, ds).values()) {
+            for (WaySegment<N, W> ws : wss) {
                 if (wset.add(ws.way)) {
                     nearestList.add(ws.way);
                 }
@@ -1413,7 +1418,7 @@
      * @return all nearest ways to the screen point given.
      * @see #getNearestWays(Point, Collection, Predicate)
      */
-    public final List<Way> getNearestWays(Point p, Predicate<OsmPrimitive> predicate) {
+    public final <N extends INode, W extends IWay<N>> List<W> getNearestWays(Point p, Predicate<IPrimitive> predicate) {
         return getNearestWays(p, null, predicate);
     }

@@ -1426,8 +1431,8 @@
      * @return The nearest way to point p, prefer a selected way if there are multiple nearest.
      * @see #getNearestWaySegment(Point, Predicate)
      */
-    public final Way getNearestWay(Point p, Predicate<OsmPrimitive> predicate) {
-        WaySegment nearestWaySeg = getNearestWaySegment(p, predicate);
+    public final <N extends INode, W extends IWay<N>> W getNearestWay(Point p, Predicate<IPrimitive> predicate) {
+        WaySegment<N, W> nearestWaySeg = getNearestWaySegment(p, predicate);
         return (nearestWaySeg == null) ? null : nearestWaySeg.way;
     }

@@ -1452,15 +1457,15 @@
      * @see #getNearestNodes(Point, Collection, Predicate)
      * @see #getNearestWays(Point, Collection, Predicate)
      */
-    public final List<OsmPrimitive> getNearestNodesOrWays(Point p,
-            Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
-        List<OsmPrimitive> nearestList = Collections.emptyList();
-        OsmPrimitive osm = getNearestNodeOrWay(p, predicate, false);
+    public final List<IPrimitive> getNearestNodesOrWays(Point p,
+            Collection<IPrimitive> ignore, Predicate<IPrimitive> predicate) {
+        List<IPrimitive> nearestList = Collections.emptyList();
+        IPrimitive osm = getNearestNodeOrWay(p, predicate, false);

         if (osm != null) {
-            if (osm instanceof Node) {
+            if (osm instanceof INode) {
                 nearestList = new ArrayList<>(getNearestNodes(p, predicate));
-            } else if (osm instanceof Way) {
+            } else if (osm instanceof IWay) {
                 nearestList = new ArrayList<>(getNearestWays(p, predicate));
             }
             if (ignore != null) {
@@ -1481,7 +1486,7 @@
      * @return Primitives nearest to the given screen point.
      * @see #getNearestNodesOrWays(Point, Collection, Predicate)
      */
-    public final List<OsmPrimitive> getNearestNodesOrWays(Point p, Predicate<OsmPrimitive> predicate) {
+    public final List<IPrimitive> getNearestNodesOrWays(Point p, Predicate<IPrimitive> predicate) {
         return getNearestNodesOrWays(p, null, predicate);
     }

@@ -1494,7 +1499,7 @@
      * @param useSelected whether to prefer selected nodes
      * @return true, if the node fulfills the properties of the function body
      */
-    private boolean isPrecedenceNode(Node osm, Point p, boolean useSelected) {
+    private boolean isPrecedenceNode(INode osm, Point p, boolean useSelected) {
         if (osm != null) {
             if (p.distanceSq(getPoint2D(osm)) <= (4*4)) return true;
             if (osm.isTagged()) return true;
@@ -1527,18 +1532,18 @@
      * @see #getNearestNode(Point, Predicate)
      * @see #getNearestWay(Point, Predicate)
      */
-    public final OsmPrimitive getNearestNodeOrWay(Point p, Predicate<OsmPrimitive> predicate, boolean useSelected) {
-        Collection<OsmPrimitive> sel;
-        DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
+    public final IPrimitive getNearestNodeOrWay(Point p, Predicate<IPrimitive> predicate, boolean useSelected) {
+        Collection<IPrimitive> sel;
+        OsmData<? extends IPrimitive, ? extends INode, ? extends IWay<?>, ? extends IRelation<?>> ds = MainApplication.getLayerManager().getActiveData();
         if (useSelected && ds != null) {
-            sel = ds.getSelected();
+            sel = new ArrayList<>(ds.getSelected());
         } else {
             sel = null;
         }
-        OsmPrimitive osm = getNearestNode(p, predicate, useSelected, sel);
+        IPrimitive osm = getNearestNode(p, predicate, useSelected, sel);

-        if (isPrecedenceNode((Node) osm, p, useSelected)) return osm;
-        WaySegment ws;
+        if (isPrecedenceNode((INode) osm, p, useSelected)) return osm;
+        WaySegment<?, ?> ws;
         if (useSelected) {
             ws = getNearestWaySegment(p, predicate, useSelected, sel);
         } else {
@@ -1559,7 +1564,7 @@
             // is wayseg shorter than maxWaySegLenSq and
             // is p closer to the middle of wayseg  than  to the nearest node?
             if (wp1.distanceSq(wp2) < maxWaySegLenSq &&
-                    p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((Node) osm))) {
+                    p.distanceSq(project(0.5, wp1, wp2)) < p.distanceSq(getPoint2D((INode) osm))) {
                 osm = ws.way;
             }
         }
@@ -1596,14 +1601,14 @@
      * @return a list of all objects that are nearest to point p and
      *          not in ignore or an empty list if nothing was found.
      */
-    public final List<OsmPrimitive> getAllNearest(Point p,
-            Collection<OsmPrimitive> ignore, Predicate<OsmPrimitive> predicate) {
-        List<OsmPrimitive> nearestList = new ArrayList<>();
-        Set<Way> wset = new HashSet<>();
+    public final List<IPrimitive> getAllNearest(Point p,
+            Collection<IPrimitive> ignore, Predicate<IPrimitive> predicate) {
+        List<IPrimitive> nearestList = new ArrayList<>();
+        Set<IWay<?>> wset = new HashSet<>();

         // add nearby ways
-        for (List<WaySegment> wss : getNearestWaySegmentsImpl(p, predicate).values()) {
-            for (WaySegment ws : wss) {
+        for (List<WaySegment<INode, IWay<INode>>> wss : getNearestWaySegmentsImpl(p, predicate, MainApplication.getLayerManager().getActiveData()).values()) {
+            for (WaySegment<?, ?> ws : wss) {
                 if (wset.add(ws.way)) {
                     nearestList.add(ws.way);
                 }
@@ -1611,15 +1616,15 @@
         }

         // add nearby nodes
-        for (List<Node> nlist : getNearestNodesImpl(p, predicate).values()) {
+        for (List<INode> nlist : getNearestNodesImpl(p, predicate).values()) {
             nearestList.addAll(nlist);
         }

         // add parent relations of nearby nodes and ways
-        Set<OsmPrimitive> parentRelations = new HashSet<>();
-        for (OsmPrimitive o : nearestList) {
-            for (OsmPrimitive r : o.getReferrers()) {
-                if (r instanceof Relation && predicate.test(r)) {
+        Set<IPrimitive> parentRelations = new HashSet<>();
+        for (IPrimitive o : nearestList) {
+            for (IPrimitive r : o.getReferrers()) {
+                if (r instanceof IRelation && predicate.test(r)) {
                     parentRelations.add(r);
                 }
             }
@@ -1644,7 +1649,7 @@
      *          or an empty list if nothing was found.
      * @see #getAllNearest(Point, Collection, Predicate)
      */
-    public final List<OsmPrimitive> getAllNearest(Point p, Predicate<OsmPrimitive> predicate) {
+    public final List<IPrimitive> getAllNearest(Point p, Predicate<IPrimitive> predicate) {
         return getAllNearest(p, null, predicate);
     }

Index: src/org/openstreetmap/josm/gui/SelectionManager.java
===================================================================
--- src/org/openstreetmap/josm/gui/SelectionManager.java	(revision 16301)
+++ src/org/openstreetmap/josm/gui/SelectionManager.java	(working copy)
@@ -19,10 +19,10 @@

 import org.openstreetmap.josm.actions.SelectByInternalPointAction;
 import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors;
 import org.openstreetmap.josm.gui.layer.AbstractMapViewPaintable;
 import org.openstreetmap.josm.tools.ColorHelper;
@@ -363,8 +363,8 @@
      * objects that are touched, instead those which are completely covered.
      * @return The collection of selected objects.
      */
-    public Collection<OsmPrimitive> getSelectedObjects(boolean alt) {
-        Collection<OsmPrimitive> selection = new LinkedList<>();
+    public Collection<IPrimitive> getSelectedObjects(boolean alt) {
+        Collection<IPrimitive> selection = new LinkedList<>();

         // whether user only clicked, not dragged.
         boolean clicked = false;
@@ -373,16 +373,16 @@
             clicked = true;
         }

-        DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
+        OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData();
         if (clicked) {
             Point center = new Point(selectionResult.xpoints[0], selectionResult.ypoints[0]);
-            OsmPrimitive osm = nc.getNearestNodeOrWay(center, OsmPrimitive::isSelectable, false);
+            IPrimitive osm = nc.getNearestNodeOrWay(center, IPrimitive::isSelectable, false);
             if (osm != null) {
                 selection.add(osm);
             }
         } else if (ds != null) {
             // nodes
-            for (Node n : ds.getNodes()) {
+            for (INode n : ds.getNodes()) {
                 if (n.isSelectable() && selectionResult.contains(nc.getPoint2D(n))) {
                     selection.add(n);
                 }
@@ -389,12 +389,12 @@
             }

             // ways
-            for (Way w : ds.getWays()) {
+            for (IWay<?> w : ds.getWays()) {
                 if (!w.isSelectable() || w.isEmpty()) {
                     continue;
                 }
                 if (alt) {
-                    for (Node n : w.getNodes()) {
+                    for (INode n : w.getNodes()) {
                         if (!n.isIncomplete() && selectionResult.contains(nc.getPoint2D(n))) {
                             selection.add(w);
                             break;
@@ -402,7 +402,7 @@
                     }
                 } else {
                     boolean allIn = true;
-                    for (Node n : w.getNodes()) {
+                    for (INode n : w.getNodes()) {
                         if (!n.isIncomplete() && !selectionResult.contains(nc.getPoint(n))) {
                             allIn = false;
                             break;
Index: src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDataText.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDataText.java	(revision 16301)
+++ src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDataText.java	(working copy)
@@ -183,7 +183,7 @@

     void addRelationMembers(IRelation<?> r) {
         add(trn("{0} Member: ", "{0} Members: ", r.getMembersCount(), r.getMembersCount()));
-        for (IRelationMember<?> m : r.getMembers()) {
+        for (IRelationMember<?, ?, ?, ?> m : r.getMembers()) {
             s.append(INDENT).append(INDENT);
             addHeadline(m.getMember());
             s.append(tr(" as \"{0}\"", m.getRole()));
Index: src/org/openstreetmap/josm/gui/dialogs/ValidatorDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/ValidatorDialog.java	(revision 16301)
+++ src/org/openstreetmap/josm/gui/dialogs/ValidatorDialog.java	(working copy)
@@ -40,6 +40,8 @@
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.WaySegment;
 import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
 import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
@@ -632,7 +634,7 @@
         protected void fixError(TestError error) throws InterruptedException, InvocationTargetException {
             if (error.isFixable()) {
                 if (error.getPrimitives().stream().noneMatch(p -> p.isDeleted() || p.getDataSet() == null)) {
-                    final Command fixCommand = error.getFix();
+                    final Command<OsmPrimitive, Node, Way, Relation> fixCommand = error.getFix();
                     if (fixCommand != null) {
                         SwingUtilities.invokeAndWait(fixCommand::executeCommand);
                         fixCommands.add(fixCommand);
@@ -712,7 +714,7 @@
     }

     private static class AutofixCommand extends SequenceCommand {
-        AutofixCommand(Collection<Command> sequenz) {
+        AutofixCommand(Collection<Command<OsmPrimitive, Node, Way, Relation>> sequenz) {
             super(tr("auto-fixed validator issues"), sequenz, true);
             setSequenceComplete(true);
         }
@@ -719,12 +721,12 @@

         @Override
         public void undoCommand() {
-            getAffectedDataSet().update(super::undoCommand);
+            ((DataSet) getAffectedDataSet()).update(super::undoCommand);
         }

         @Override
         public boolean executeCommand() {
-            return getAffectedDataSet().update(super::executeCommand);
+            return ((DataSet) getAffectedDataSet()).update(super::executeCommand);
         }
     }

Index: src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java	(revision 16301)
+++ src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java	(working copy)
@@ -546,7 +546,7 @@
         OsmDataLayer layer = MainApplication.getLayerManager().getActiveDataLayer();
         if (!layer.isLocked()) {
             List<RelationMember> members = new ArrayList<>();
-            for (IRelationMember<?> rm : ((MemberInfo) membershipData.getValueAt(row, 1)).role) {
+            for (IRelationMember<?, ?, ?, ?> rm : ((MemberInfo) membershipData.getValueAt(row, 1)).role) {
                 if (rm instanceof RelationMember) {
                     members.add((RelationMember) rm);
                 }
@@ -681,7 +681,7 @@
                     MemberInfo mi = Optional.ofNullable(roles.get(r)).orElseGet(() -> new MemberInfo(newSel));
                     roles.put(r, mi);
                     int i = 1;
-                    for (IRelationMember<?> m : r.getMembers()) {
+                    for (IRelationMember<?, ?, ?, ?> m : r.getMembers()) {
                         if (m.getMember() == primitive) {
                             mi.add(m, i);
                         }
@@ -942,7 +942,7 @@
     static final class TaggingPresetCommandHandler implements TaggingPresetHandler {
         @Override
         public void updateTags(List<Tag> tags) {
-            Command command = TaggingPreset.createCommand(getSelection(), tags);
+            Command<?, ?, ?, ?> command = TaggingPreset.createCommand(getSelection(), tags);
             if (command != null) {
                 UndoRedoHandler.getInstance().add(command);
             }
@@ -1000,7 +1000,7 @@
     }

     static class MemberInfo {
-        private final List<IRelationMember<?>> role = new ArrayList<>();
+        private final List<IRelationMember<?, ?, ?, ?>> role = new ArrayList<>();
         private Set<IPrimitive> members = new HashSet<>();
         private List<Integer> position = new ArrayList<>();
         private Collection<? extends IPrimitive> selection;
@@ -1011,7 +1011,7 @@
             this.selection = selection;
         }

-        void add(IRelationMember<?> r, Integer p) {
+        void add(IRelationMember<?, ?, ?, ?> r, Integer p) {
             role.add(r);
             members.add(r.getMember());
             position.add(p);
@@ -1033,7 +1033,7 @@

         String getRoleString() {
             if (roleString == null) {
-                for (IRelationMember<?> r : role) {
+                for (IRelationMember<?, ?, ?, ?> r : role) {
                     if (roleString == null) {
                         roleString = r.getRole();
                     } else if (!roleString.equals(r.getRole())) {
@@ -1142,7 +1142,7 @@
             for (OsmPrimitive primitive: OsmDataManager.getInstance().getInProgressSelection()) {
                 rel.removeMembersFor(primitive);
             }
-            UndoRedoHandler.getInstance().add(new ChangeCommand(cur, rel));
+            UndoRedoHandler.getInstance().add(new ChangeCommand<>(cur, rel));

             tagTable.clearSelection();
             if (nextRelation != null) {
Index: src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java	(revision 16301)
+++ src/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditor.java	(working copy)
@@ -51,10 +51,12 @@
 import org.openstreetmap.josm.command.ChangeCommand;
 import org.openstreetmap.josm.command.Command;
 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.MainMenu;
@@ -903,7 +905,7 @@
      * @return The resulting command
      * @throws IllegalArgumentException if orig is null
      */
-    public static Command addPrimitivesToRelation(final Relation orig, Collection<? extends OsmPrimitive> primitivesToAdd) {
+    public static Command<OsmPrimitive, Node, Way, Relation> addPrimitivesToRelation(final Relation orig, Collection<? extends OsmPrimitive> primitivesToAdd) {
         CheckParameterUtil.ensureParameterNotNull(orig, "orig");
         try {
             final Collection<TaggingPreset> presets = TaggingPresets.getMatchingPresets(
@@ -922,7 +924,7 @@
                 relation.addMember(new RelationMember(roles.size() == 1 ? roles.iterator().next() : "", p));
                 modified = true;
             }
-            return modified ? new ChangeCommand(orig, relation) : null;
+            return modified ? new ChangeCommand<>(orig, relation) : null;
         } catch (AddAbortException ign) {
             Logging.trace(ign);
             return null;
Index: src/org/openstreetmap/josm/gui/layer/AbstractOsmDataLayer.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/AbstractOsmDataLayer.java	(revision 16301)
+++ src/org/openstreetmap/josm/gui/layer/AbstractOsmDataLayer.java	(working copy)
@@ -1,6 +1,10 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.layer;

+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
 import org.openstreetmap.josm.data.osm.OsmData;

 /**
@@ -17,7 +21,7 @@
      * Returns the {@link OsmData} behind this layer.
      * @return the {@link OsmData} behind this layer.
      */
-    public abstract OsmData<?, ?, ?, ?> getDataSet();
+    public abstract <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> OsmData<O, N, W, R> getDataSet();

     @Override
     public void lock() {
Index: src/org/openstreetmap/josm/gui/layer/MainLayerManager.java
===================================================================
--- src/org/openstreetmap/josm/gui/layer/MainLayerManager.java	(revision 16301)
+++ src/org/openstreetmap/josm/gui/layer/MainLayerManager.java	(working copy)
@@ -14,6 +14,10 @@

 import org.openstreetmap.josm.data.gpx.GpxData;
 import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.INode;
+import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
+import org.openstreetmap.josm.data.osm.IWay;
 import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.io.AsynchronousUploadPrimitivesTask;
@@ -406,7 +410,7 @@
      * @return That data set, <code>null</code> if there is no active data layer.
      * @since 13926
      */
-    public synchronized OsmData<?, ?, ?, ?> getActiveData() {
+    public synchronized  <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> OsmData<O, N, W, R> getActiveData() {
         if (dataLayer != null) {
             return dataLayer.getDataSet();
         } else {
Index: src/org/openstreetmap/josm/gui/mappaint/mapcss/Selector.java
===================================================================
--- src/org/openstreetmap/josm/gui/mappaint/mapcss/Selector.java	(revision 16301)
+++ src/org/openstreetmap/josm/gui/mappaint/mapcss/Selector.java	(working copy)
@@ -24,6 +24,7 @@
 import org.openstreetmap.josm.data.osm.IRelation;
 import org.openstreetmap.josm.data.osm.IRelationMember;
 import org.openstreetmap.josm.data.osm.IWay;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
 import org.openstreetmap.josm.data.osm.OsmUtils;
 import org.openstreetmap.josm.data.osm.Relation;
@@ -189,7 +190,7 @@
          * referrer was found.</p>
          *
          */
-        private class MatchingReferrerFinder implements PrimitiveVisitor {
+        private class MatchingReferrerFinder implements PrimitiveVisitor<INode, IWay<INode>, IRelation<?>> {
             private final Environment e;

             /**
@@ -243,7 +244,7 @@
             }

             @Override
-            public void visit(IWay<?> w) {
+            public void visit(IWay<INode> w) {
                 doVisit(w, w::getNodesCount, w::getNode);
             }

@@ -253,7 +254,7 @@
             }
         }

-        private abstract static class AbstractFinder implements PrimitiveVisitor {
+        private abstract static class AbstractFinder implements PrimitiveVisitor<INode, IWay<INode>, IRelation<?>> {
             protected final Environment e;

             protected AbstractFinder(Environment e) {
@@ -265,7 +266,7 @@
             }

             @Override
-            public void visit(IWay<?> w) {
+            public void visit(IWay<INode> w) {
             }

             @Override
@@ -298,7 +299,7 @@
         private class MultipolygonOpenEndFinder extends AbstractFinder {

             @Override
-            public void visit(IWay<?> w) {
+            public void visit(IWay<INode> w) {
                 w.visitReferrers(innerVisitor);
             }

@@ -306,7 +307,7 @@
                 super(e);
             }

-            private final PrimitiveVisitor innerVisitor = new AbstractFinder(e) {
+            private final PrimitiveVisitor<INode, IWay<INode>, IRelation<?>> innerVisitor = new AbstractFinder(e) {
                 @Override
                 public void visit(IRelation<?> r) {
                     if (r instanceof Relation && left.matches(e.withPrimitive(r))) {
@@ -327,7 +328,7 @@
             private final String layer;
             private Area area;
             /** Will contain all way segments, grouped by cells */
-            Map<Point2D, List<WaySegment>> cellSegments;
+            Map<Point2D, List<WaySegment<Node, Way>>> cellSegments;

             private CrossingFinder(Environment e) {
                 super(e);
@@ -347,10 +348,10 @@
                 return Geometry.getAreaEastNorth(p);
             }

-            private Map<List<Way>, List<WaySegment>> findCrossings(IPrimitive area,
-                    Map<Point2D, List<WaySegment>> cellSegments) {
+            private Map<List<Way>, List<WaySegment<Node, Way>>> findCrossings(IPrimitive area,
+                    Map<Point2D, List<WaySegment<Node, Way>>> cellSegments) {
                 /** The detected crossing ways */
-                Map<List<Way>, List<WaySegment>> crossingWays = new HashMap<>(50);
+                Map<List<Way>, List<WaySegment<Node, Way>>> crossingWays = new HashMap<>(50);
                 if (area instanceof Way) {
                     CrossingWays.findIntersectingWay((Way) area, cellSegments, crossingWays, false);
                 } else if (area instanceof Relation && area.isMultipolygon()) {
@@ -413,9 +414,9 @@
                     findCrossings(e.osm, cellSegments); // ignore self intersections etc. here
                 }
                 // need a copy
-                final Map<Point2D, List<WaySegment>> tmpCellSegments = new HashMap<>(cellSegments);
+                final Map<Point2D, List<WaySegment<Node, Way>>> tmpCellSegments = new HashMap<>(cellSegments);
                 // calculate all crossings between e.osm and p
-                Map<List<Way>, List<WaySegment>> crossingWays = findCrossings(p, tmpCellSegments);
+                Map<List<Way>, List<WaySegment<Node, Way>>> crossingWays = findCrossings(p, tmpCellSegments);
                 if (!crossingWays.isEmpty()) {
                     addToChildren(e, p);
                     if (e.crossingWaysMap == null) {
@@ -469,7 +470,7 @@
             }

             @Override
-            public void visit(IWay<?> w) {
+            public void visit(IWay<INode> w) {
                 if (left.matches(new Environment(w).withParent(e.osm))
                         && w.getBBox().bounds(e.osm.getBBox())
                         && !Geometry.filterInsidePolygon(Collections.singletonList(e.osm), w).isEmpty()) {
@@ -601,7 +602,7 @@
                         }
                     }
                 } else if (e.osm instanceof IRelation) {
-                    List<? extends IRelationMember<?>> members = ((IRelation<?>) e.osm).getMembers();
+                    List<? extends IRelationMember<?, ?, ?, ?>> members = ((IRelation<?>) e.osm).getMembers();
                     for (int i = 0; i < members.size(); i++) {
                         IPrimitive member = members.get(i).getMember();
                         if (left.matches(e.withPrimitive(member))
Index: src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java
===================================================================
--- src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java	(revision 16301)
+++ src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java	(working copy)
@@ -41,6 +41,7 @@
 import org.openstreetmap.josm.data.UndoRedoHandler;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.OsmDataManager;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -47,6 +48,7 @@
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
 import org.openstreetmap.josm.data.osm.search.SearchParseError;
@@ -412,7 +414,7 @@
         }

         if (!sel.isEmpty() && answer == DIALOG_ANSWER_APPLY) {
-            Command cmd = createCommand(sel, getChangedTags());
+            Command<?, ?, ?, ?> cmd = createCommand(sel, getChangedTags());
             if (cmd != null) {
                 UndoRedoHandler.getInstance().add(cmd);
             }
@@ -547,8 +549,8 @@
      * @param changedTags The tags to change
      * @return A command that changes the tags.
      */
-    public static Command createCommand(Collection<OsmPrimitive> sel, List<Tag> changedTags) {
-        List<Command> cmds = new ArrayList<>();
+    public static Command<?, ?, ?, ?> createCommand(Collection<OsmPrimitive> sel, List<Tag> changedTags) {
+        List<Command<OsmPrimitive, Node, Way, Relation>> cmds = new ArrayList<>();
         for (Tag tag: changedTags) {
             ChangePropertyCommand cmd = new ChangePropertyCommand(sel, tag.getKey(), tag.getValue());
             if (cmd.getObjectsNumber() > 0) {
Index: src/org/openstreetmap/josm/tools/Geometry.java
===================================================================
--- src/org/openstreetmap/josm/tools/Geometry.java	(revision 16301)
+++ src/org/openstreetmap/josm/tools/Geometry.java	(working copy)
@@ -26,14 +26,15 @@
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.ILatLon;
 import org.openstreetmap.josm.data.osm.BBox;
-import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.INode;
 import org.openstreetmap.josm.data.osm.IPrimitive;
+import org.openstreetmap.josm.data.osm.IRelation;
 import org.openstreetmap.josm.data.osm.IWay;
 import org.openstreetmap.josm.data.osm.MultipolygonBuilder;
 import org.openstreetmap.josm.data.osm.MultipolygonBuilder.JoinedPolygon;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.NodePositionComparator;
+import org.openstreetmap.josm.data.osm.OsmData;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.Way;
@@ -95,15 +96,15 @@
      *              the ways.
      * @return list of new nodes, if test is true the list might not contain all intersections
      */
-    public static Set<Node> addIntersections(List<Way> ways, boolean test, List<Command> cmds) {
+    public static <O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> Set<N> addIntersections(List<W> ways, boolean test, List<Command<O, N, W, R>> cmds) {

         int n = ways.size();
         @SuppressWarnings("unchecked")
-        List<Node>[] newNodes = new ArrayList[n];
+        List<N>[] newNodes = new ArrayList[n];
         BBox[] wayBounds = new BBox[n];
         boolean[] changedWays = new boolean[n];

-        Set<Node> intersectionNodes = new LinkedHashSet<>();
+        Set<N> intersectionNodes = new LinkedHashSet<>();

         //copy node arrays for local usage.
         for (int pos = 0; pos < n; pos++) {
@@ -112,10 +113,10 @@
             changedWays[pos] = false;
         }

-        DataSet dataset = ways.get(0).getDataSet();
+        OsmData<O, N, W, R> dataset = ways.get(0).getDataSet();

         //iterate over all way pairs and introduce the intersections
-        Comparator<Node> coordsComparator = new NodePositionComparator();
+        Comparator<N> coordsComparator = new NodePositionComparator<>();
         for (int seg1Way = 0; seg1Way < n; seg1Way++) {
             for (int seg2Way = seg1Way; seg2Way < n; seg2Way++) {

@@ -124,8 +125,8 @@
                     continue;
                 }

-                List<Node> way1Nodes = newNodes[seg1Way];
-                List<Node> way2Nodes = newNodes[seg2Way];
+                List<N> way1Nodes = newNodes[seg1Way];
+                List<N> way2Nodes = newNodes[seg2Way];

                 //iterate over primary segmemt
                 for (int seg1Pos = 0; seg1Pos + 1 < way1Nodes.size(); seg1Pos++) {
@@ -136,10 +137,10 @@
                     for (int seg2Pos = seg2Start; seg2Pos + 1 < way2Nodes.size(); seg2Pos++) {

                         //need to get them again every time, because other segments may be changed
-                        Node seg1Node1 = way1Nodes.get(seg1Pos);
-                        Node seg1Node2 = way1Nodes.get(seg1Pos + 1);
-                        Node seg2Node1 = way2Nodes.get(seg2Pos);
-                        Node seg2Node2 = way2Nodes.get(seg2Pos + 1);
+                        N seg1Node1 = way1Nodes.get(seg1Pos);
+                        N seg1Node2 = way1Nodes.get(seg1Pos + 1);
+                        N seg2Node1 = way2Nodes.get(seg2Pos);
+                        N seg2Node2 = way2Nodes.get(seg2Pos + 1);

                         int commonCount = 0;
                         //test if we have common nodes to add.
@@ -168,8 +169,10 @@
                                     seg2Node1.getEastNorth(), seg2Node2.getEastNorth());

                             if (intersection != null) {
-                                Node newNode = new Node(ProjectionRegistry.getProjection().eastNorth2latlon(intersection));
-                                Node intNode = newNode;
+                                N newNode = Utils.clone(seg1Node1);
+                                newNode.removeAll(); // Make certain we don't keep any keys from the original node
+                                newNode.setCoor(ProjectionRegistry.getProjection().eastNorth2latlon(intersection));
+                                N intNode = newNode;
                                 boolean insertInSeg1 = false;
                                 boolean insertInSeg2 = false;
                                 //find if the intersection point is at end point of one of the segments, if so use that point
@@ -218,7 +221,7 @@
                                 intersectionNodes.add(intNode);

                                 if (intNode == newNode) {
-                                    cmds.add(new AddCommand(dataset, intNode));
+                                    cmds.add(new AddCommand<>(dataset, (O) intNode));
                                 }
                             }
                         } else if (test && !intersectionNodes.isEmpty())
@@ -234,11 +237,11 @@
                 continue;
             }

-            Way way = ways.get(pos);
-            Way newWay = new Way(way);
+            W way = ways.get(pos);
+            W newWay = Utils.clone(way);
             newWay.setNodes(newNodes[pos]);

-            cmds.add(new ChangeCommand(dataset, way, newWay));
+            cmds.add(new ChangeCommand<>(dataset, (O) way, (O) newWay));
         }

         return intersectionNodes;
@@ -1540,7 +1543,7 @@
      * May return {@link Double#NaN}.
      * @since 15035
      */
-    public static double getDistanceSegmentSegment(WaySegment ws1, WaySegment ws2) {
+    public static <N extends INode, W extends IWay<N>> double getDistanceSegmentSegment(WaySegment<N, W> ws1, WaySegment<N, W> ws2) {
         return getDistanceSegmentSegment(ws1.getFirstNode(), ws1.getSecondNode(), ws2.getFirstNode(), ws2.getSecondNode());
     }

@@ -1555,7 +1558,7 @@
      * May return {@link Double#NaN}.
      * @since 15035
      */
-    public static double getDistanceSegmentSegment(Node ws1Node1, Node ws1Node2, Node ws2Node1, Node ws2Node2) {
+    public static double getDistanceSegmentSegment(INode ws1Node1, INode ws1Node2, INode ws2Node1, INode ws2Node2) {
         EastNorth enWs1Node1 = ws1Node1.getEastNorth();
         EastNorth enWs1Node2 = ws1Node2.getEastNorth();
         EastNorth enWs2Node1 = ws2Node1.getEastNorth();
Index: src/org/openstreetmap/josm/tools/Utils.java
===================================================================
--- src/org/openstreetmap/josm/tools/Utils.java	(revision 16301)
+++ src/org/openstreetmap/josm/tools/Utils.java	(working copy)
@@ -15,6 +15,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
+import java.lang.reflect.InvocationTargetException;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -68,9 +69,10 @@
 import javax.script.ScriptEngine;
 import javax.script.ScriptEngineManager;

-import com.kitfox.svg.xml.XMLParseUtil;
 import org.openstreetmap.josm.spi.preferences.Config;

+import com.kitfox.svg.xml.XMLParseUtil;
+
 /**
  * Basic utils, that can be useful in different parts of the program.
  */
@@ -1945,4 +1947,26 @@
         // remove extra whitespaces
         return rawString.trim();
     }
+
+    /**
+     * Clone an object that has a cloning constructor
+     *
+     * @param <T> The object class to clone
+     * @param object The object that is cloned
+     * @return The cloned object, or {@code null}
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T clone(T object) {
+        try {
+            Object obj = object.getClass().getConstructor(object.getClass()).newInstance(object);
+            if (object.getClass().isAssignableFrom(obj.getClass())) {
+                return (T) obj;
+            } else {
+                return null;
+            }
+        } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+            Logging.error(e);
+            return null;
+        }
+    }
 }
Index: test/unit/org/openstreetmap/josm/TestUtils.java
===================================================================
--- test/unit/org/openstreetmap/josm/TestUtils.java	(revision 16301)
+++ test/unit/org/openstreetmap/josm/TestUtils.java	(working copy)
@@ -389,8 +389,8 @@
      * @param ds data set
      * @return a new empty command
      */
-    public static Command newCommand(DataSet ds) {
-        return new Command(ds) {
+    public static Command<OsmPrimitive, Node, Way, Relation> newCommand(DataSet ds) {
+        return new Command<OsmPrimitive, Node, Way, Relation>(ds) {
             @Override
             public String getDescriptionText() {
                 return "";
Index: test/unit/org/openstreetmap/josm/command/SequenceCommandTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/command/SequenceCommandTest.java	(revision 16301)
+++ test/unit/org/openstreetmap/josm/command/SequenceCommandTest.java	(working copy)
@@ -21,7 +21,9 @@
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.User;
+import org.openstreetmap.josm.data.osm.Way;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
 import org.openstreetmap.josm.testutils.JOSMTestRules;

@@ -64,7 +66,7 @@
                 return super.executeCommand();
             }
         };
-        SequenceCommand command = new SequenceCommand("seq", Arrays.<Command>asList(command1, command2));
+        SequenceCommand<OsmPrimitive, Node, Way, Relation> command = new SequenceCommand<>("seq", Arrays.<Command<OsmPrimitive, Node, Way, Relation>>asList(command1, command2));

         command.executeCommand();

@@ -86,7 +88,7 @@
                 super.undoCommand();
             }
         };
-        SequenceCommand command = new SequenceCommand("seq", Arrays.<Command>asList(command1, command2));
+        SequenceCommand<OsmPrimitive, Node, Way, Relation> command = new SequenceCommand<>("seq", Arrays.<Command<OsmPrimitive, Node, Way, Relation>>asList(command1, command2));

         command.executeCommand();

@@ -110,7 +112,7 @@
         TestCommand command1 = new TestCommand(ds, null);
         FailingCommand command2 = new FailingCommand(ds);
         TestCommand command3 = new TestCommand(ds, null);
-        SequenceCommand command = new SequenceCommand("seq", Arrays.<Command>asList(command1, command2, command3));
+        SequenceCommand<OsmPrimitive, Node, Way, Relation> command = new SequenceCommand<>("seq", Arrays.<Command<OsmPrimitive, Node, Way, Relation>>asList(command1, command2, command3));
         assertFalse(command.executeCommand());
         assertFalse(command1.executed);
         // Don't check command2 executed state as it's possible but not necessary to undo failed commands
@@ -127,7 +129,7 @@
         TestCommand command1 = new TestCommand(ds, null);
         FailingCommand command2 = new FailingCommand(ds);
         TestCommand command3 = new TestCommand(ds, null);
-        SequenceCommand command = new SequenceCommand("seq", Arrays.<Command>asList(command1, command2, command3), true);
+        SequenceCommand<OsmPrimitive, Node, Way, Relation> command = new SequenceCommand<>("seq", Arrays.<Command<OsmPrimitive, Node, Way, Relation>>asList(command1, command2, command3), true);
         assertTrue(command.executeCommand());
         assertTrue(command1.executed);
         assertTrue(command3.executed);
@@ -146,8 +148,8 @@
         final TestCommand command1 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingNode));
         final TestCommand command2 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingNode2));

-        assertEquals(command2, new SequenceCommand(ds, "seq", Arrays.asList(command1, command2), false).getLastCommand());
-        assertNull(new SequenceCommand(ds, "seq", Collections.emptyList(), false).getLastCommand());
+        assertEquals(command2, new SequenceCommand<>(ds, "seq", Arrays.asList(command1, command2), false).getLastCommand());
+        assertNull(new SequenceCommand<>(ds, "seq", Collections.emptyList(), false).getLastCommand());
     }

     /**
@@ -156,9 +158,9 @@
     @Test
     public void testFillModifiedData() {
         DataSet ds = new DataSet();
-        Command command1 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingNode));
-        Command command2 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingNode2));
-        Command command3 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingWay)) {
+        Command<OsmPrimitive, Node, Way, Relation> command1 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingNode));
+        Command<OsmPrimitive, Node, Way, Relation> command2 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingNode2));
+        Command<OsmPrimitive, Node, Way, Relation> command3 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingWay)) {
             @Override
             public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
                     Collection<OsmPrimitive> added) {
@@ -165,7 +167,7 @@
                 deleted.addAll(primitives);
             }
         };
-        Command command4 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingRelation)) {
+        Command<OsmPrimitive, Node, Way, Relation> command4 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingRelation)) {
             @Override
             public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
                     Collection<OsmPrimitive> added) {
@@ -176,7 +178,7 @@
         ArrayList<OsmPrimitive> modified = new ArrayList<>();
         ArrayList<OsmPrimitive> deleted = new ArrayList<>();
         ArrayList<OsmPrimitive> added = new ArrayList<>();
-        SequenceCommand command = new SequenceCommand("seq", command1, command2, command3, command4);
+        SequenceCommand<OsmPrimitive, Node, Way, Relation> command = new SequenceCommand<>("seq", command1, command2, command3, command4);
         command.fillModifiedData(modified, deleted, added);
         assertArrayEquals(new Object[] {testData.existingNode, testData.existingNode2}, modified.toArray());
         assertArrayEquals(new Object[] {testData.existingWay}, deleted.toArray());
@@ -189,10 +191,10 @@
     @Test
     public void testGetParticipatingPrimitives() {
         DataSet ds = new DataSet();
-        Command command1 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingNode));
-        Command command2 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingNode2));
+        Command<OsmPrimitive, Node, Way, Relation> command1 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingNode));
+        Command<OsmPrimitive, Node, Way, Relation> command2 = new TestCommand(ds, Arrays.<OsmPrimitive>asList(testData.existingNode2));

-        SequenceCommand command = new SequenceCommand("seq", command1, command2);
+        SequenceCommand<OsmPrimitive, Node, Way, Relation> command = new SequenceCommand<>("seq", command1, command2);
         command.executeCommand();
         Collection<? extends OsmPrimitive> primitives = command.getParticipatingPrimitives();
         assertEquals(2, primitives.size());
@@ -205,7 +207,7 @@
      */
     @Test
     public void testDescription() {
-        assertTrue(new SequenceCommand(new DataSet(), "test", Collections.emptyList(), false).getDescriptionText().matches("Sequence: test"));
+        assertTrue(new SequenceCommand<>(new DataSet(), "test", Collections.emptyList(), false).getDescriptionText().matches("Sequence: test"));
     }

     /**
@@ -217,7 +219,7 @@
         TestUtils.assumeWorkingEqualsVerifier();
         EqualsVerifier.forClass(SequenceCommand.class).usingGetClass()
             .withPrefabValues(Command.class,
-                new AddCommand(ds, new Node(1)), new AddCommand(ds, new Node(2)))
+                new AddCommand<>(ds, new Node(1)), new AddCommand<>(ds, new Node(2)))
             .withPrefabValues(DataSet.class,
                     new DataSet(), new DataSet())
             .withPrefabValues(User.class,
@@ -228,7 +230,7 @@
             .verify();
     }

-    private static class TestCommand extends Command {
+    private static class TestCommand extends Command<OsmPrimitive, Node, Way, Relation> {
         protected final Collection<? extends OsmPrimitive> primitives;
         protected boolean executed;

