// License: GPL. For details, see LICENSE file. package cadastre_fr; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Cursor; import java.awt.GridBagLayout; import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.ButtonGroup; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JTextField; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.mapmode.MapMode; import org.openstreetmap.josm.command.AddCommand; import org.openstreetmap.josm.command.ChangeCommand; import org.openstreetmap.josm.command.ChangePropertyCommand; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.data.coor.EastNorth; 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.RelationMember; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.data.osm.WaySegment; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.tools.GBC; import org.openstreetmap.josm.tools.ImageProvider; import org.openstreetmap.josm.tools.Pair; import org.openstreetmap.josm.tools.Shortcut; public class Address extends MapMode { // perhaps make all these tags configurable in the future private String tagHighway = "highway"; private String tagHighwayName = "name"; private String tagHouseNumber = "addr:housenumber"; private String tagHouseStreet = "addr:street"; private String tagBuilding = "building"; private String relationAddrType = "associatedStreet"; private String relationAddrName = "name"; private String relationAddrStreetRole = "street"; private String relationMemberHouse = "house"; private JRadioButton plusOne = new JRadioButton("+1", false); private JRadioButton plusTwo = new JRadioButton("+2", true); // enable this by default private JRadioButton minusOne = new JRadioButton("-1", false); private JRadioButton minusTwo = new JRadioButton("-2", false); final JCheckBox tagPolygon = new JCheckBox(tr("on polygon")); JDialog dialog; JButton clearButton; final JTextField inputNumber = new JTextField(); final JTextField inputStreet = new JTextField(); JLabel link = new JLabel(); private transient Way selectedWay; private boolean shift; private boolean ctrl; /** * Constructs a new {@code Address} map mode. */ public Address() { super(tr("Add address"), "buildings", tr("Helping tool for tag address"), // CHECKSTYLE.OFF: LineLength Shortcut.registerShortcut("mapmode:cadastre-fr-buildings", tr("Mode: {0}", tr("CadastreFR - Buildings")), KeyEvent.VK_E, Shortcut.DIRECT), // CHECKSTYLE.ON: LineLength getCursor()); } @Override public void enterMode() { super.enterMode(); if (dialog == null) { createDialog(); } dialog.setVisible(true); Main.map.mapView.addMouseListener(this); } @Override public void exitMode() { if (Main.map.mapView != null) { super.exitMode(); Main.map.mapView.removeMouseListener(this); } // kill the window completely to fix an issue on some linux distro and full screen mode. if (dialog != null) { dialog.dispose(); dialog = null; } } @Override public void mousePressed(MouseEvent e) { if (e.getButton() != MouseEvent.BUTTON1) return; shift = (e.getModifiers() & ActionEvent.SHIFT_MASK) != 0; ctrl = (e.getModifiers() & ActionEvent.CTRL_MASK) != 0; MapView mv = Main.map.mapView; Point mousePos = e.getPoint(); List<Way> mouseOnExistingWays = new ArrayList<>(); List<Way> mouseOnExistingBuildingWays = new ArrayList<>(); Node currentMouseNode = mv.getNearestNode(mousePos, OsmPrimitive::isSelectable); if (currentMouseNode != null) { // click on existing node setNewSelection(currentMouseNode); String num = currentMouseNode.get(tagHouseNumber); if (num != null && currentMouseNode.get(tagHouseStreet) == null && findWayInRelationAddr(currentMouseNode) == null && !inputStreet.getText().isEmpty()) { // house number already present but not linked to a street Collection<Command> cmds = new LinkedList<>(); addStreetNameOrRelation(currentMouseNode, cmds); Command c = new SequenceCommand("Add node address", cmds); Main.main.undoRedo.add(c); setNewSelection(currentMouseNode); } else { if (num != null) { try { // add new address Integer.parseInt(num); inputNumber.setText(num); applyInputNumberChange(); } catch (NumberFormatException en) { Main.warn("Unable to parse house number \"" + num + "\""); } } if (currentMouseNode.get(tagHouseStreet) != null) { if (Main.pref.getBoolean("cadastrewms.addr.dontUseRelation", false)) { inputStreet.setText(currentMouseNode.get(tagHouseStreet)); if (ctrl) { Collection<Command> cmds = new LinkedList<>(); addAddrToPrimitive(currentMouseNode, cmds); if (num == null) applyInputNumberChange(); } setSelectedWay((Way) null); } } else { // check if the node belongs to an associatedStreet relation Way wayInRelationAddr = findWayInRelationAddr(currentMouseNode); if (wayInRelationAddr == null) { // node exists but doesn't carry address information : add tags like a new node if (ctrl) { applyInputNumberChange(); } Collection<Command> cmds = new LinkedList<>(); addAddrToPrimitive(currentMouseNode, cmds); } else { inputStreet.setText(wayInRelationAddr.get(tagHighwayName)); setSelectedWay(wayInRelationAddr); } } } } else { List<WaySegment> wss = mv.getNearestWaySegments(mousePos, OsmPrimitive::isSelectable); for (WaySegment ws : wss) { if (ws.way.get(tagHighway) != null && ws.way.get(tagHighwayName) != null) mouseOnExistingWays.add(ws.way); else if (ws.way.get(tagBuilding) != null && ws.way.get(tagHouseNumber) == null) mouseOnExistingBuildingWays.add(ws.way); } if (mouseOnExistingWays.size() == 1) { // clicked on existing highway => set new street name inputStreet.setText(mouseOnExistingWays.get(0).get(tagHighwayName)); setSelectedWay(mouseOnExistingWays.get(0)); inputNumber.setText(""); setNewSelection(mouseOnExistingWays.get(0)); } else if (mouseOnExistingWays.isEmpty()) { // clicked a non highway and not a node => add the new address if (inputStreet.getText().isEmpty() || inputNumber.getText().isEmpty()) { Toolkit.getDefaultToolkit().beep(); } else { Collection<Command> cmds = new LinkedList<>(); if (ctrl) { applyInputNumberChange(); } if (tagPolygon.isSelected()) { addAddrToPolygon(mouseOnExistingBuildingWays, cmds); } else { Node n = createNewNode(e, cmds); addAddrToPrimitive(n, cmds); } } } } } private Way findWayInRelationAddr(Node n) { List<OsmPrimitive> l = n.getReferrers(); for (OsmPrimitive osm : l) { if (osm instanceof Relation && osm.hasKey("type") && osm.get("type").equals(relationAddrType)) { for (RelationMember rm : ((Relation) osm).getMembers()) { if (rm.getRole().equals(relationAddrStreetRole)) { OsmPrimitive osp = rm.getMember(); if (osp instanceof Way && osp.hasKey(tagHighwayName)) { return (Way) osp; } } } } } return null; } private void addAddrToPolygon(List<Way> mouseOnExistingBuildingWays, Collection<Command> cmds) { for (Way w:mouseOnExistingBuildingWays) { addAddrToPrimitive(w, cmds); } } private void addAddrToPrimitive(OsmPrimitive osm, Collection<Command> cmds) { // add the current tag addr:housenumber in node and member in relation (if so configured) if (shift) { try { revertInputNumberChange(); } catch (NumberFormatException en) { Main.warn("Unable to parse house number \"" + inputNumber.getText() + "\""); } } cmds.add(new ChangePropertyCommand(osm, tagHouseNumber, inputNumber.getText())); addStreetNameOrRelation(osm, cmds); try { applyInputNumberChange(); Command c = new SequenceCommand("Add node address", cmds); Main.main.undoRedo.add(c); setNewSelection(osm); } catch (NumberFormatException en) { Main.warn("Unable to parse house number \"" + inputNumber.getText() + "\""); } } private Relation findRelationAddr(Way w) { List<OsmPrimitive> l = w.getReferrers(); for (OsmPrimitive osm : l) { if (osm instanceof Relation && osm.hasKey("type") && osm.get("type").equals(relationAddrType)) { return (Relation) osm; } } return null; } private void addStreetNameOrRelation(OsmPrimitive osm, Collection<Command> cmds) { if (Main.pref.getBoolean("cadastrewms.addr.dontUseRelation", false)) { cmds.add(new ChangePropertyCommand(osm, tagHouseStreet, inputStreet.getText())); } else if (selectedWay != null) { Relation selectedRelation = findRelationAddr(selectedWay); // add the node to its relation if (selectedRelation != null) { RelationMember rm = new RelationMember(relationMemberHouse, osm); Relation newRel = new Relation(selectedRelation); newRel.addMember(rm); cmds.add(new ChangeCommand(selectedRelation, newRel)); } else { // create new relation Relation newRel = new Relation(); newRel.put("type", relationAddrType); newRel.put(relationAddrName, selectedWay.get(tagHighwayName)); newRel.addMember(new RelationMember(relationAddrStreetRole, selectedWay)); newRel.addMember(new RelationMember(relationMemberHouse, osm)); cmds.add(new AddCommand(newRel)); } } } private static Node createNewNode(MouseEvent e, Collection<Command> cmds) { // DrawAction.mouseReleased() but without key modifiers Node n = new Node(Main.map.mapView.getLatLon(e.getX(), e.getY())); cmds.add(new AddCommand(n)); List<WaySegment> wss = Main.map.mapView.getNearestWaySegments(e.getPoint(), OsmPrimitive::isSelectable); Map<Way, List<Integer>> insertPoints = new HashMap<>(); for (WaySegment ws : wss) { List<Integer> is; if (insertPoints.containsKey(ws.way)) { is = insertPoints.get(ws.way); } else { is = new ArrayList<>(); insertPoints.put(ws.way, is); } is.add(ws.lowerIndex); } Set<Pair<Node, Node>> segSet = new HashSet<>(); ArrayList<Way> replacedWays = new ArrayList<>(); ArrayList<Way> reuseWays = new ArrayList<>(); for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) { Way w = insertPoint.getKey(); List<Integer> is = insertPoint.getValue(); Way wnew = new Way(w); pruneSuccsAndReverse(is); for (int i : is) { segSet.add(Pair.sort(new Pair<>(w.getNode(i), w.getNode(i+1)))); } for (int i : is) { wnew.addNode(i + 1, n); } cmds.add(new ChangeCommand(insertPoint.getKey(), wnew)); replacedWays.add(insertPoint.getKey()); reuseWays.add(wnew); } adjustNode(segSet, n); return n; } private static void adjustNode(Collection<Pair<Node, Node>> segs, Node n) { switch (segs.size()) { case 0: return; case 2: // This computes the intersection between // the two segments and adjusts the node position. Iterator<Pair<Node, Node>> i = segs.iterator(); Pair<Node, Node> seg = i.next(); EastNorth A = seg.a.getEastNorth(); EastNorth B = seg.b.getEastNorth(); seg = i.next(); EastNorth C = seg.a.getEastNorth(); EastNorth D = seg.b.getEastNorth(); double u = det(B.east() - A.east(), B.north() - A.north(), C.east() - D.east(), C.north() - D.north()); // Check for parallel segments and do nothing if they are // In practice this will probably only happen when a way has been duplicated if (u == 0) return; // q is a number between 0 and 1 // It is the point in the segment where the intersection occurs // if the segment is scaled to lenght 1 double q = det(B.north() - C.north(), B.east() - C.east(), D.north() - C.north(), D.east() - C.east()) / u; EastNorth intersection = new EastNorth( B.east() + q * (A.east() - B.east()), B.north() + q * (A.north() - B.north())); int snapToIntersectionThreshold = Main.pref.getInteger("edit.snap-intersection-threshold", 10); // only adjust to intersection if within snapToIntersectionThreshold pixel of mouse click; otherwise // fall through to default action. // (for semi-parallel lines, intersection might be miles away!) if (Main.map.mapView.getPoint(n).distance(Main.map.mapView.getPoint(intersection)) < snapToIntersectionThreshold) { n.setEastNorth(intersection); return; } default: EastNorth P = n.getEastNorth(); seg = segs.iterator().next(); A = seg.a.getEastNorth(); B = seg.b.getEastNorth(); double a = P.distanceSq(B); double b = P.distanceSq(A); double c = A.distanceSq(B); q = (a - b + c) / (2*c); n.setEastNorth(new EastNorth(B.east() + q * (A.east() - B.east()), B.north() + q * (A.north() - B.north()))); } } static double det(double a, double b, double c, double d) { return a * d - b * c; } private static void pruneSuccsAndReverse(List<Integer> is) { HashSet<Integer> is2 = new HashSet<>(); for (int i : is) { if (!is2.contains(i - 1) && !is2.contains(i + 1)) { is2.add(i); } } is.clear(); is.addAll(is2); Collections.sort(is); Collections.reverse(is); } private static Cursor getCursor() { try { return ImageProvider.getCursor("crosshair", null); } catch (RuntimeException e) { Main.warn(e); } return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); } private void applyInputNumberChange() { Integer num = Integer.parseInt(inputNumber.getText()); if (plusOne.isSelected()) num = num + 1; if (plusTwo.isSelected()) num = num + 2; if (minusOne.isSelected() && num > 1) num = num - 1; if (minusTwo.isSelected() && num > 2) num = num - 2; inputNumber.setText(num.toString()); } private void revertInputNumberChange() { Integer num = Integer.parseInt(inputNumber.getText()); if (plusOne.isSelected()) num = num - 1; if (plusTwo.isSelected()) num = num - 2; if (minusOne.isSelected() && num > 1) num = num + 1; if (minusTwo.isSelected() && num > 2) num = num + 2; inputNumber.setText(num.toString()); } private void createDialog() { ImageIcon iconLink = ImageProvider.get(null, "Mf_relation"); link.setIcon(iconLink); link.setEnabled(false); JPanel p = new JPanel(new GridBagLayout()); JLabel number = new JLabel(tr("Next no")); JLabel street = new JLabel(tr("Street")); p.add(number, GBC.std().insets(0, 0, 0, 0)); p.add(inputNumber, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 0, 5)); p.add(street, GBC.std().insets(0, 0, 0, 0)); JPanel p2 = new JPanel(new GridBagLayout()); inputStreet.setEditable(false); p2.add(inputStreet, GBC.std().fill(GBC.HORIZONTAL).insets(5, 0, 0, 0)); p2.add(link, GBC.eol().insets(10, 0, 0, 0)); p.add(p2, GBC.eol().fill(GBC.HORIZONTAL)); clearButton = new JButton("Clear"); clearButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { inputNumber.setText(""); inputStreet.setText(""); setSelectedWay((Way) null); } }); ButtonGroup bgIncremental = new ButtonGroup(); bgIncremental.add(plusOne); bgIncremental.add(plusTwo); bgIncremental.add(minusOne); bgIncremental.add(minusTwo); p.add(minusOne, GBC.std().insets(10, 0, 10, 0)); p.add(plusOne, GBC.std().insets(0, 0, 10, 0)); tagPolygon.setSelected(Main.pref.getBoolean("cadastrewms.addr.onBuilding", false)); tagPolygon.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent arg0) { Main.pref.put("cadastrewms.addr.onBuilding", tagPolygon.isSelected()); } }); p.add(tagPolygon, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 0)); p.add(minusTwo, GBC.std().insets(10, 0, 10, 0)); p.add(plusTwo, GBC.std().insets(0, 0, 10, 0)); p.add(clearButton, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 0)); final Object[] options = {}; final JOptionPane pane = new JOptionPane(p, JOptionPane.PLAIN_MESSAGE, JOptionPane.YES_NO_CANCEL_OPTION, null, options, null); dialog = pane.createDialog(Main.parent, tr("Enter addresses")); dialog.setModal(false); dialog.setAlwaysOnTop(true); dialog.addComponentListener(new ComponentAdapter() { protected void rememberGeometry() { Main.pref.put("cadastrewms.addr.bounds", dialog.getX()+","+dialog.getY()+","+dialog.getWidth()+","+dialog.getHeight()); } @Override public void componentMoved(ComponentEvent e) { rememberGeometry(); } @Override public void componentResized(ComponentEvent e) { rememberGeometry(); } }); dialog.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent arg) { Main.map.selectMapMode((MapMode) Main.map.getDefaultButtonAction()); } }); String bounds = Main.pref.get("cadastrewms.addr.bounds", null); if (bounds != null) { String[] b = bounds.split(","); dialog.setBounds(new Rectangle( Integer.parseInt(b[0]), Integer.parseInt(b[1]), Integer.parseInt(b[2]), Integer.parseInt(b[3]))); } } private void setSelectedWay(Way w) { this.selectedWay = w; if (w == null) { link.setEnabled(false); } else link.setEnabled(true); link.repaint(); } private static void setNewSelection(OsmPrimitive osm) { DataSet ds = Main.getLayerManager().getEditDataSet(); Collection<OsmPrimitive> newSelection = new LinkedList<>(ds.getSelected()); newSelection.clear(); newSelection.add(osm); ds.setSelected(osm); } }