source: subversion/applications/editors/josm/plugins/cadastre-fr/src/org/openstreetmap/josm/plugins/fr/cadastre/actions/mapmode/Address.java

Last change on this file was 34458, checked in by donvip, 8 months ago

update to JOSM 14153

File size: 22.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.plugins.fr.cadastre.actions.mapmode;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Cursor;
7import java.awt.GridBagLayout;
8import java.awt.Point;
9import java.awt.Rectangle;
10import java.awt.Toolkit;
11import java.awt.event.ActionEvent;
12import java.awt.event.ActionListener;
13import java.awt.event.ComponentAdapter;
14import java.awt.event.ComponentEvent;
15import java.awt.event.KeyEvent;
16import java.awt.event.MouseEvent;
17import java.awt.event.WindowAdapter;
18import java.awt.event.WindowEvent;
19import java.util.ArrayList;
20import java.util.Collection;
21import java.util.Collections;
22import java.util.HashMap;
23import java.util.HashSet;
24import java.util.Iterator;
25import java.util.LinkedList;
26import java.util.List;
27import java.util.Map;
28import java.util.Set;
29
30import javax.swing.ButtonGroup;
31import javax.swing.ImageIcon;
32import javax.swing.JButton;
33import javax.swing.JCheckBox;
34import javax.swing.JDialog;
35import javax.swing.JLabel;
36import javax.swing.JOptionPane;
37import javax.swing.JPanel;
38import javax.swing.JRadioButton;
39import javax.swing.JTextField;
40import javax.swing.event.ChangeEvent;
41import javax.swing.event.ChangeListener;
42
43import org.openstreetmap.josm.actions.mapmode.MapMode;
44import org.openstreetmap.josm.command.AddCommand;
45import org.openstreetmap.josm.command.ChangeCommand;
46import org.openstreetmap.josm.command.ChangePropertyCommand;
47import org.openstreetmap.josm.command.Command;
48import org.openstreetmap.josm.command.SequenceCommand;
49import org.openstreetmap.josm.data.UndoRedoHandler;
50import org.openstreetmap.josm.data.coor.EastNorth;
51import org.openstreetmap.josm.data.osm.DataSet;
52import org.openstreetmap.josm.data.osm.Node;
53import org.openstreetmap.josm.data.osm.OsmDataManager;
54import org.openstreetmap.josm.data.osm.OsmPrimitive;
55import org.openstreetmap.josm.data.osm.Relation;
56import org.openstreetmap.josm.data.osm.RelationMember;
57import org.openstreetmap.josm.data.osm.Way;
58import org.openstreetmap.josm.data.osm.WaySegment;
59import org.openstreetmap.josm.gui.MainApplication;
60import org.openstreetmap.josm.gui.MapView;
61import org.openstreetmap.josm.spi.preferences.Config;
62import org.openstreetmap.josm.tools.GBC;
63import org.openstreetmap.josm.tools.ImageProvider;
64import org.openstreetmap.josm.tools.Logging;
65import org.openstreetmap.josm.tools.Pair;
66import org.openstreetmap.josm.tools.Shortcut;
67
68public class Address extends MapMode {
69
70    // perhaps make all these tags configurable in the future
71    private String tagHighway = "highway";
72    private String tagHighwayName = "name";
73    private String tagHouseNumber = "addr:housenumber";
74    private String tagHouseStreet = "addr:street";
75    private String tagBuilding = "building";
76    private String relationAddrType = "associatedStreet";
77    private String relationAddrName = "name";
78    private String relationAddrStreetRole = "street";
79    private String relationMemberHouse = "house";
80
81    private JRadioButton plusOne = new JRadioButton("+1", false);
82    private JRadioButton plusTwo = new JRadioButton("+2", true); // enable this by default
83    private JRadioButton minusOne = new JRadioButton("-1", false);
84    private JRadioButton minusTwo = new JRadioButton("-2", false);
85    final JCheckBox tagPolygon = new JCheckBox(tr("on polygon"));
86
87    JDialog dialog;
88    JButton clearButton;
89    final JTextField inputNumber = new JTextField();
90    final JTextField inputStreet = new JTextField();
91    JLabel link = new JLabel();
92    private transient Way selectedWay;
93
94    /**
95     * Constructs a new {@code Address} map mode.
96     */
97    public Address() {
98        super(tr("Add address"), "buildings",
99                tr("Helping tool for tag address"),
100                // CHECKSTYLE.OFF: LineLength
101                Shortcut.registerShortcut("mapmode:cadastre-fr-buildings", tr("Mode: {0}", tr("CadastreFR - Buildings")), KeyEvent.VK_E, Shortcut.DIRECT),
102                // CHECKSTYLE.ON: LineLength
103                getCursor());
104    }
105
106    @Override public void enterMode() {
107        super.enterMode();
108        if (dialog == null) {
109            createDialog();
110        }
111        dialog.setVisible(true);
112        MainApplication.getMap().mapView.addMouseListener(this);
113    }
114
115    @Override public void exitMode() {
116        if (MainApplication.getMap().mapView != null) {
117            super.exitMode();
118            MainApplication.getMap().mapView.removeMouseListener(this);
119        }
120        // kill the window completely to fix an issue on some linux distro and full screen mode.
121        if (dialog != null) {
122            dialog.dispose();
123            dialog = null;
124        }
125    }
126
127    @Override
128    public void mousePressed(MouseEvent e) {
129        if (e.getButton() != MouseEvent.BUTTON1)
130            return;
131        updateKeyModifiers(e);
132        MapView mv = MainApplication.getMap().mapView;
133        Point mousePos = e.getPoint();
134        List<Way> mouseOnExistingWays = new ArrayList<>();
135        List<Way> mouseOnExistingBuildingWays = new ArrayList<>();
136        Node currentMouseNode = mv.getNearestNode(mousePos, OsmPrimitive::isSelectable);
137        if (currentMouseNode != null) {
138            // click on existing node
139            setNewSelection(currentMouseNode);
140            String num = currentMouseNode.get(tagHouseNumber);
141            if (num != null
142                    && currentMouseNode.get(tagHouseStreet) == null
143                    && findWayInRelationAddr(currentMouseNode) == null
144                    && !inputStreet.getText().isEmpty()) {
145                // house number already present but not linked to a street
146                Collection<Command> cmds = new LinkedList<>();
147                addStreetNameOrRelation(currentMouseNode, cmds);
148                Command c = new SequenceCommand("Add node address", cmds);
149                UndoRedoHandler.getInstance().add(c);
150                setNewSelection(currentMouseNode);
151            } else {
152                if (num != null) {
153                    try {
154                        // add new address
155                        Integer.parseInt(num);
156                        inputNumber.setText(num);
157                        applyInputNumberChange();
158                    } catch (NumberFormatException ex) {
159                        Logging.warn("Unable to parse house number \"" + num + "\"");
160                    }
161                }
162                if (currentMouseNode.get(tagHouseStreet) != null) {
163                    if (Config.getPref().getBoolean("cadastrewms.addr.dontUseRelation", false)) {
164                        inputStreet.setText(currentMouseNode.get(tagHouseStreet));
165                        if (ctrl) {
166                            Collection<Command> cmds = new LinkedList<>();
167                            addAddrToPrimitive(currentMouseNode, cmds);
168                            if (num == null)
169                                applyInputNumberChange();
170                        }
171                        setSelectedWay((Way) null);
172                    }
173                } else {
174                    // check if the node belongs to an associatedStreet relation
175                    Way wayInRelationAddr = findWayInRelationAddr(currentMouseNode);
176                    if (wayInRelationAddr == null) {
177                        // node exists but doesn't carry address information : add tags like a new node
178                        if (ctrl) {
179                            applyInputNumberChange();
180                        }
181                        Collection<Command> cmds = new LinkedList<>();
182                        addAddrToPrimitive(currentMouseNode, cmds);
183                    } else {
184                        inputStreet.setText(wayInRelationAddr.get(tagHighwayName));
185                        setSelectedWay(wayInRelationAddr);
186                    }
187                }
188            }
189        } else {
190            List<WaySegment> wss = mv.getNearestWaySegments(mousePos, OsmPrimitive::isSelectable);
191            for (WaySegment ws : wss) {
192                if (ws.way.get(tagHighway) != null && ws.way.get(tagHighwayName) != null)
193                    mouseOnExistingWays.add(ws.way);
194                else if (ws.way.get(tagBuilding) != null && ws.way.get(tagHouseNumber) == null)
195                    mouseOnExistingBuildingWays.add(ws.way);
196            }
197            if (mouseOnExistingWays.size() == 1) {
198                // clicked on existing highway => set new street name
199                inputStreet.setText(mouseOnExistingWays.get(0).get(tagHighwayName));
200                setSelectedWay(mouseOnExistingWays.get(0));
201                inputNumber.setText("");
202                setNewSelection(mouseOnExistingWays.get(0));
203            } else if (mouseOnExistingWays.isEmpty()) {
204                // clicked a non highway and not a node => add the new address
205                if (inputStreet.getText().isEmpty() || inputNumber.getText().isEmpty()) {
206                    Toolkit.getDefaultToolkit().beep();
207                } else {
208                    Collection<Command> cmds = new LinkedList<>();
209                    if (ctrl) {
210                        applyInputNumberChange();
211                    }
212                    if (tagPolygon.isSelected()) {
213                        addAddrToPolygon(mouseOnExistingBuildingWays, cmds);
214                    } else {
215                        Node n = createNewNode(e, cmds);
216                        addAddrToPrimitive(n, cmds);
217                    }
218                }
219            }
220        }
221    }
222
223    private Way findWayInRelationAddr(Node n) {
224        List<OsmPrimitive> l = n.getReferrers();
225        for (OsmPrimitive osm : l) {
226            if (osm instanceof Relation && osm.hasKey("type") && osm.get("type").equals(relationAddrType)) {
227                for (RelationMember rm : ((Relation) osm).getMembers()) {
228                    if (rm.getRole().equals(relationAddrStreetRole)) {
229                        OsmPrimitive osp = rm.getMember();
230                        if (osp instanceof Way && osp.hasKey(tagHighwayName)) {
231                            return (Way) osp;
232                        }
233                    }
234                }
235            }
236        }
237        return null;
238    }
239
240    private void addAddrToPolygon(List<Way> mouseOnExistingBuildingWays, Collection<Command> cmds) {
241        for (Way w:mouseOnExistingBuildingWays) {
242            addAddrToPrimitive(w, cmds);
243        }
244    }
245
246    private void addAddrToPrimitive(OsmPrimitive osm, Collection<Command> cmds) {
247        // add the current tag addr:housenumber in node and member in relation (if so configured)
248        if (shift) {
249            try {
250                revertInputNumberChange();
251            } catch (NumberFormatException ex) {
252                Logging.warn("Unable to parse house number \"" + inputNumber.getText() + "\"");
253            }
254        }
255        Map<String, String> tags = new HashMap<>();
256        tags.put(tagHouseNumber, inputNumber.getText());
257        cmds.add(new ChangePropertyCommand(OsmDataManager.getInstance().getEditDataSet(), Collections.singleton(osm), tags));
258        addStreetNameOrRelation(osm, cmds);
259        try {
260            applyInputNumberChange();
261            Command c = new SequenceCommand("Add node address", cmds);
262            UndoRedoHandler.getInstance().add(c);
263            setNewSelection(osm);
264        } catch (NumberFormatException ex) {
265            Logging.warn("Unable to parse house number \"" + inputNumber.getText() + "\"");
266        }
267    }
268
269    private Relation findRelationAddr(Way w) {
270        List<OsmPrimitive> l = w.getReferrers();
271        for (OsmPrimitive osm : l) {
272            if (osm instanceof Relation && osm.hasKey("type") && osm.get("type").equals(relationAddrType)) {
273                return (Relation) osm;
274            }
275        }
276        return null;
277    }
278
279    private void addStreetNameOrRelation(OsmPrimitive osm, Collection<Command> cmds) {
280        if (Config.getPref().getBoolean("cadastrewms.addr.dontUseRelation", false)) {
281            cmds.add(new ChangePropertyCommand(osm, tagHouseStreet, inputStreet.getText()));
282        } else if (selectedWay != null) {
283            Relation selectedRelation = findRelationAddr(selectedWay);
284            // add the node to its relation
285            if (selectedRelation != null) {
286                RelationMember rm = new RelationMember(relationMemberHouse, osm);
287                Relation newRel = new Relation(selectedRelation);
288                newRel.addMember(rm);
289                cmds.add(new ChangeCommand(selectedRelation, newRel));
290            } else {
291                // create new relation
292                Relation newRel = new Relation();
293                newRel.put("type", relationAddrType);
294                newRel.put(relationAddrName, selectedWay.get(tagHighwayName));
295                newRel.addMember(new RelationMember(relationAddrStreetRole, selectedWay));
296                newRel.addMember(new RelationMember(relationMemberHouse, osm));
297                cmds.add(new AddCommand(OsmDataManager.getInstance().getEditDataSet(), newRel));
298            }
299        }
300    }
301
302    private static Node createNewNode(MouseEvent e, Collection<Command> cmds) {
303        // DrawAction.mouseReleased() but without key modifiers
304        Node n = new Node(MainApplication.getMap().mapView.getLatLon(e.getX(), e.getY()));
305        cmds.add(new AddCommand(OsmDataManager.getInstance().getEditDataSet(), n));
306        List<WaySegment> wss = MainApplication.getMap().mapView.getNearestWaySegments(e.getPoint(), OsmPrimitive::isSelectable);
307        Map<Way, List<Integer>> insertPoints = new HashMap<>();
308        for (WaySegment ws : wss) {
309            List<Integer> is;
310            if (insertPoints.containsKey(ws.way)) {
311                is = insertPoints.get(ws.way);
312            } else {
313                is = new ArrayList<>();
314                insertPoints.put(ws.way, is);
315            }
316
317            is.add(ws.lowerIndex);
318        }
319        Set<Pair<Node, Node>> segSet = new HashSet<>();
320        ArrayList<Way> replacedWays = new ArrayList<>();
321        ArrayList<Way> reuseWays = new ArrayList<>();
322        for (Map.Entry<Way, List<Integer>> insertPoint : insertPoints.entrySet()) {
323            Way w = insertPoint.getKey();
324            List<Integer> is = insertPoint.getValue();
325            Way wnew = new Way(w);
326            pruneSuccsAndReverse(is);
327            for (int i : is) {
328                segSet.add(Pair.sort(new Pair<>(w.getNode(i), w.getNode(i+1))));
329            }
330            for (int i : is) {
331                wnew.addNode(i + 1, n);
332            }
333            cmds.add(new ChangeCommand(insertPoint.getKey(), wnew));
334            replacedWays.add(insertPoint.getKey());
335            reuseWays.add(wnew);
336        }
337        adjustNode(segSet, n);
338
339        return n;
340    }
341
342    private static void adjustNode(Collection<Pair<Node, Node>> segs, Node n) {
343
344        switch (segs.size()) {
345        case 0:
346            return;
347        case 2:
348            // This computes the intersection between
349            // the two segments and adjusts the node position.
350            Iterator<Pair<Node, Node>> i = segs.iterator();
351            Pair<Node, Node> seg = i.next();
352            EastNorth A = seg.a.getEastNorth();
353            EastNorth B = seg.b.getEastNorth();
354            seg = i.next();
355            EastNorth C = seg.a.getEastNorth();
356            EastNorth D = seg.b.getEastNorth();
357
358            double u = det(B.east() - A.east(), B.north() - A.north(), C.east() - D.east(), C.north() - D.north());
359
360            // Check for parallel segments and do nothing if they are
361            // In practice this will probably only happen when a way has been duplicated
362
363            if (u == 0) return;
364
365            // q is a number between 0 and 1
366            // It is the point in the segment where the intersection occurs
367            // if the segment is scaled to lenght 1
368
369            double q = det(B.north() - C.north(), B.east() - C.east(), D.north() - C.north(), D.east() - C.east()) / u;
370            EastNorth intersection = new EastNorth(
371                    B.east() + q * (A.east() - B.east()),
372                    B.north() + q * (A.north() - B.north()));
373
374            int snapToIntersectionThreshold
375            = Config.getPref().getInt("edit.snap-intersection-threshold", 10);
376
377            // only adjust to intersection if within snapToIntersectionThreshold pixel of mouse click; otherwise
378            // fall through to default action.
379            // (for semi-parallel lines, intersection might be miles away!)
380            MapView mv = MainApplication.getMap().mapView;
381            if (mv.getPoint(n).distance(mv.getPoint(intersection)) < snapToIntersectionThreshold) {
382                n.setEastNorth(intersection);
383                return;
384            }
385            // fall through
386        default:
387            EastNorth P = n.getEastNorth();
388            seg = segs.iterator().next();
389            A = seg.a.getEastNorth();
390            B = seg.b.getEastNorth();
391            double a = P.distanceSq(B);
392            double b = P.distanceSq(A);
393            double c = A.distanceSq(B);
394            q = (a - b + c) / (2*c);
395            n.setEastNorth(new EastNorth(B.east() + q * (A.east() - B.east()), B.north() + q * (A.north() - B.north())));
396        }
397    }
398
399    static double det(double a, double b, double c, double d) {
400        return a * d - b * c;
401    }
402
403    private static void pruneSuccsAndReverse(List<Integer> is) {
404        HashSet<Integer> is2 = new HashSet<>();
405        for (int i : is) {
406            if (!is2.contains(i - 1) && !is2.contains(i + 1)) {
407                is2.add(i);
408            }
409        }
410        is.clear();
411        is.addAll(is2);
412        Collections.sort(is);
413        Collections.reverse(is);
414    }
415
416    private static Cursor getCursor() {
417        try {
418            return ImageProvider.getCursor("crosshair", null);
419        } catch (RuntimeException e) {
420            Logging.warn(e);
421        }
422        return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
423    }
424
425    private void applyInputNumberChange() {
426        Integer num = Integer.parseInt(inputNumber.getText());
427        if (plusOne.isSelected())
428            num = num + 1;
429        if (plusTwo.isSelected())
430            num = num + 2;
431        if (minusOne.isSelected() && num > 1)
432            num = num - 1;
433        if (minusTwo.isSelected() && num > 2)
434            num = num - 2;
435        inputNumber.setText(num.toString());
436    }
437
438    private void revertInputNumberChange() {
439        Integer num = Integer.parseInt(inputNumber.getText());
440        if (plusOne.isSelected())
441            num = num - 1;
442        if (plusTwo.isSelected())
443            num = num - 2;
444        if (minusOne.isSelected() && num > 1)
445            num = num + 1;
446        if (minusTwo.isSelected() && num > 2)
447            num = num + 2;
448        inputNumber.setText(num.toString());
449    }
450
451    private void createDialog() {
452        ImageIcon iconLink = ImageProvider.get(null, "Mf_relation");
453        link.setIcon(iconLink);
454        link.setEnabled(false);
455        JPanel p = new JPanel(new GridBagLayout());
456        JLabel number = new JLabel(tr("Next no"));
457        JLabel street = new JLabel(tr("Street"));
458        p.add(number, GBC.std().insets(0, 0, 0, 0));
459        p.add(inputNumber, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 0, 5));
460        p.add(street, GBC.std().insets(0, 0, 0, 0));
461        JPanel p2 = new JPanel(new GridBagLayout());
462        inputStreet.setEditable(false);
463        p2.add(inputStreet, GBC.std().fill(GBC.HORIZONTAL).insets(5, 0, 0, 0));
464        p2.add(link, GBC.eol().insets(10, 0, 0, 0));
465        p.add(p2, GBC.eol().fill(GBC.HORIZONTAL));
466        clearButton = new JButton("Clear");
467        clearButton.addActionListener(new ActionListener() {
468            @Override
469            public void actionPerformed(ActionEvent e) {
470                inputNumber.setText("");
471                inputStreet.setText("");
472                setSelectedWay((Way) null);
473            }
474        });
475        ButtonGroup bgIncremental = new ButtonGroup();
476        bgIncremental.add(plusOne);
477        bgIncremental.add(plusTwo);
478        bgIncremental.add(minusOne);
479        bgIncremental.add(minusTwo);
480        p.add(minusOne, GBC.std().insets(10, 0, 10, 0));
481        p.add(plusOne, GBC.std().insets(0, 0, 10, 0));
482        tagPolygon.setSelected(Config.getPref().getBoolean("cadastrewms.addr.onBuilding", false));
483        tagPolygon.addChangeListener(new ChangeListener() {
484            @Override
485            public void stateChanged(ChangeEvent arg0) {
486                Config.getPref().putBoolean("cadastrewms.addr.onBuilding", tagPolygon.isSelected());
487            }
488        });
489        p.add(tagPolygon, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 0));
490        p.add(minusTwo, GBC.std().insets(10, 0, 10, 0));
491        p.add(plusTwo, GBC.std().insets(0, 0, 10, 0));
492        p.add(clearButton, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 0));
493
494        final Object[] options = {};
495        final JOptionPane pane = new JOptionPane(p,
496                JOptionPane.PLAIN_MESSAGE, JOptionPane.YES_NO_CANCEL_OPTION,
497                null, options, null);
498        dialog = pane.createDialog(MainApplication.getMainFrame(), tr("Enter addresses"));
499        dialog.setModal(false);
500        dialog.setAlwaysOnTop(true);
501        dialog.addComponentListener(new ComponentAdapter() {
502            protected void rememberGeometry() {
503                Config.getPref().put("cadastrewms.addr.bounds", dialog.getX()+","+dialog.getY()+","+dialog.getWidth()+","+dialog.getHeight());
504            }
505
506            @Override public void componentMoved(ComponentEvent e) {
507                rememberGeometry();
508            }
509
510            @Override public void componentResized(ComponentEvent e) {
511                rememberGeometry();
512            }
513        });
514        dialog.addWindowListener(new WindowAdapter() {
515            @Override
516            public void windowClosing(WindowEvent arg) {
517                MainApplication.getMap().selectMapMode((MapMode) MainApplication.getMap().getDefaultButtonAction());
518            }
519        });
520        String bounds = Config.getPref().get("cadastrewms.addr.bounds", null);
521        if (bounds != null) {
522            String[] b = bounds.split(",");
523            dialog.setBounds(new Rectangle(
524                    Integer.parseInt(b[0]), Integer.parseInt(b[1]), Integer.parseInt(b[2]), Integer.parseInt(b[3])));
525        }
526    }
527
528    private void setSelectedWay(Way w) {
529        this.selectedWay = w;
530        if (w == null) {
531            link.setEnabled(false);
532        } else
533            link.setEnabled(true);
534        link.repaint();
535    }
536
537    private static void setNewSelection(OsmPrimitive osm) {
538        DataSet ds = MainApplication.getLayerManager().getEditDataSet();
539        Collection<OsmPrimitive> newSelection = new LinkedList<>(ds.getSelected());
540        newSelection.clear();
541        newSelection.add(osm);
542        ds.setSelected(osm);
543    }
544}
Note: See TracBrowser for help on using the repository browser.