001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.history;
003import static org.openstreetmap.josm.tools.I18n.tr;
004
005import java.awt.Color;
006import java.awt.GridBagConstraints;
007import java.awt.GridBagLayout;
008import java.awt.Insets;
009import java.awt.event.ActionEvent;
010import java.awt.event.MouseAdapter;
011import java.awt.event.MouseEvent;
012
013import javax.swing.AbstractAction;
014import javax.swing.BorderFactory;
015import javax.swing.JLabel;
016import javax.swing.JPanel;
017import javax.swing.JPopupMenu;
018import javax.swing.UIManager;
019import javax.swing.event.ChangeEvent;
020import javax.swing.event.ChangeListener;
021
022import org.openstreetmap.gui.jmapviewer.JMapViewer;
023import org.openstreetmap.gui.jmapviewer.MapMarkerDot;
024import org.openstreetmap.josm.command.MoveCommand;
025import org.openstreetmap.josm.data.UndoRedoHandler;
026import org.openstreetmap.josm.data.coor.LatLon;
027import org.openstreetmap.josm.data.coor.conversion.DecimalDegreesCoordinateFormat;
028import org.openstreetmap.josm.data.osm.Node;
029import org.openstreetmap.josm.data.osm.OsmPrimitive;
030import org.openstreetmap.josm.data.osm.history.HistoryNode;
031import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
032import org.openstreetmap.josm.gui.NavigatableComponent;
033import org.openstreetmap.josm.gui.bbox.JosmMapViewer;
034import org.openstreetmap.josm.gui.bbox.SlippyMapBBoxChooser;
035import org.openstreetmap.josm.gui.util.GuiHelper;
036import org.openstreetmap.josm.gui.widgets.JosmTextArea;
037import org.openstreetmap.josm.tools.CheckParameterUtil;
038import org.openstreetmap.josm.tools.Destroyable;
039import org.openstreetmap.josm.tools.ImageProvider;
040import org.openstreetmap.josm.tools.Pair;
041
042/**
043 * An UI widget for displaying differences in the coordinates of two
044 * {@link HistoryNode}s.
045 * @since 2243
046 */
047public class CoordinateInfoViewer extends HistoryBrowserPanel {
048
049    /** the info panel for coordinates for the node in role REFERENCE_POINT_IN_TIME */
050    private LatLonViewer referenceLatLonViewer;
051    /** the info panel for coordinates for the node in role CURRENT_POINT_IN_TIME */
052    private LatLonViewer currentLatLonViewer;
053    /** the info panel for distance between the two coordinates */
054    private DistanceViewer distanceViewer;
055    /** the map panel showing the old+new coordinate */
056    private MapViewer mapViewer;
057
058    protected void build() {
059        GridBagConstraints gc = new GridBagConstraints();
060
061        // ---------------------------
062        gc.gridx = 0;
063        gc.gridy = 0;
064        gc.gridwidth = 1;
065        gc.gridheight = 1;
066        gc.weightx = 0.5;
067        gc.weighty = 0.0;
068        gc.insets = new Insets(5, 5, 5, 0);
069        gc.fill = GridBagConstraints.HORIZONTAL;
070        gc.anchor = GridBagConstraints.FIRST_LINE_START;
071        referenceInfoPanel = new VersionInfoPanel(model, PointInTimeType.REFERENCE_POINT_IN_TIME);
072        add(referenceInfoPanel, gc);
073
074        gc.gridx = 1;
075        gc.gridy = 0;
076        gc.fill = GridBagConstraints.HORIZONTAL;
077        gc.weightx = 0.5;
078        gc.weighty = 0.0;
079        gc.anchor = GridBagConstraints.FIRST_LINE_START;
080        currentInfoPanel = new VersionInfoPanel(model, PointInTimeType.CURRENT_POINT_IN_TIME);
081        add(currentInfoPanel, gc);
082
083        // ---------------------------
084        // the two coordinate panels
085        gc.gridx = 0;
086        gc.gridy = 1;
087        gc.weightx = 0.5;
088        gc.weighty = 0.0;
089        gc.fill = GridBagConstraints.HORIZONTAL;
090        gc.anchor = GridBagConstraints.NORTHWEST;
091        referenceLatLonViewer = new LatLonViewer(model, PointInTimeType.REFERENCE_POINT_IN_TIME);
092        add(referenceLatLonViewer, gc);
093
094        gc.gridx = 1;
095        gc.gridy = 1;
096        gc.weightx = 0.5;
097        gc.weighty = 0.0;
098        gc.fill = GridBagConstraints.HORIZONTAL;
099        gc.anchor = GridBagConstraints.NORTHWEST;
100        currentLatLonViewer = new LatLonViewer(model, PointInTimeType.CURRENT_POINT_IN_TIME);
101        add(currentLatLonViewer, gc);
102
103        // --------------------
104        // the distance panel
105        gc.gridx = 0;
106        gc.gridy = 2;
107        gc.gridwidth = 2;
108        gc.fill = GridBagConstraints.HORIZONTAL;
109        gc.weightx = 1.0;
110        gc.weighty = 0.0;
111        distanceViewer = new DistanceViewer(model);
112        add(distanceViewer, gc);
113
114        // the map panel
115        gc.gridx = 0;
116        gc.gridy = 3;
117        gc.gridwidth = 2;
118        gc.fill = GridBagConstraints.BOTH;
119        gc.weightx = 1.0;
120        gc.weighty = 1.0;
121        mapViewer = new MapViewer(model);
122        add(mapViewer, gc);
123        mapViewer.setZoomControlsVisible(false);
124
125        JPopupMenu popupMenu = new JPopupMenu();
126        popupMenu.add(new RestoreCoordinateAction());
127        setComponentPopupMenu(popupMenu);
128        mapViewer.setComponentPopupMenu(popupMenu);
129    }
130
131    /**
132     * Constructs a new {@code CoordinateInfoViewer}.
133     * @param model the model. Must not be null.
134     * @throws IllegalArgumentException if model is null
135     */
136    public CoordinateInfoViewer(HistoryBrowserModel model) {
137        CheckParameterUtil.ensureParameterNotNull(model, "model");
138        setModel(model);
139        build();
140        registerAsChangeListener(model);
141    }
142
143    @Override
144    protected void unregisterAsChangeListener(HistoryBrowserModel model) {
145        super.unregisterAsChangeListener(model);
146        if (currentLatLonViewer != null) {
147            model.removeChangeListener(currentLatLonViewer);
148        }
149        if (referenceLatLonViewer != null) {
150            model.removeChangeListener(referenceLatLonViewer);
151        }
152        if (distanceViewer != null) {
153            model.removeChangeListener(distanceViewer);
154        }
155        if (mapViewer != null) {
156            model.removeChangeListener(mapViewer);
157        }
158    }
159
160    @Override
161    protected void registerAsChangeListener(HistoryBrowserModel model) {
162        super.registerAsChangeListener(model);
163        if (currentLatLonViewer != null) {
164            model.addChangeListener(currentLatLonViewer);
165        }
166        if (referenceLatLonViewer != null) {
167            model.addChangeListener(referenceLatLonViewer);
168        }
169        if (distanceViewer != null) {
170            model.addChangeListener(distanceViewer);
171        }
172        if (mapViewer != null) {
173            model.addChangeListener(mapViewer);
174        }
175    }
176
177    @Override
178    public void destroy() {
179        super.destroy();
180        referenceLatLonViewer.destroy();
181        currentLatLonViewer.destroy();
182        distanceViewer.destroy();
183    }
184
185    /**
186     * Pans the map to the old+new coordinate
187     * @see JMapViewer#setDisplayToFitMapMarkers()
188     */
189    public void setDisplayToFitMapMarkers() {
190        mapViewer.setDisplayToFitMapMarkers();
191    }
192
193    private static JosmTextArea newTextArea() {
194        JosmTextArea area = new JosmTextArea();
195        GuiHelper.setBackgroundReadable(area, Color.WHITE);
196        area.setEditable(false);
197        area.setOpaque(true);
198        area.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
199        area.setFont(UIManager.getFont("Label.font"));
200        return area;
201    }
202
203    private static class Updater {
204        private final HistoryBrowserModel model;
205        private final PointInTimeType role;
206
207        protected Updater(HistoryBrowserModel model, PointInTimeType role) {
208            this.model = model;
209            this.role = role;
210        }
211
212        protected HistoryOsmPrimitive getPrimitive() {
213            if (model == null || role == null)
214                return null;
215            return model.getPointInTime(role);
216        }
217
218        protected HistoryOsmPrimitive getOppositePrimitive() {
219            if (model == null || role == null)
220                return null;
221            return model.getPointInTime(role.opposite());
222        }
223
224        protected final Pair<LatLon, LatLon> getCoordinates() {
225            HistoryOsmPrimitive p = getPrimitive();
226            if (!(p instanceof HistoryNode)) return null;
227            HistoryOsmPrimitive opposite = getOppositePrimitive();
228            if (!(opposite instanceof HistoryNode)) return null;
229            HistoryNode node = (HistoryNode) p;
230            HistoryNode oppositeNode = (HistoryNode) opposite;
231
232            return Pair.create(node.getCoords(), oppositeNode.getCoords());
233        }
234    }
235
236    /**
237     * A UI widgets which displays the Lan/Lon-coordinates of a {@link HistoryNode}.
238     */
239    private static class LatLonViewer extends JPanel implements ChangeListener, Destroyable {
240
241        private final JosmTextArea lblLat = newTextArea();
242        private final JosmTextArea lblLon = newTextArea();
243        private final transient Updater updater;
244        private final Color modifiedColor;
245
246        protected void build() {
247            setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
248            GridBagConstraints gc = new GridBagConstraints();
249
250            // --------
251            gc.gridx = 0;
252            gc.gridy = 0;
253            gc.fill = GridBagConstraints.NONE;
254            gc.weightx = 0.0;
255            gc.insets = new Insets(5, 5, 5, 5);
256            gc.anchor = GridBagConstraints.NORTHWEST;
257            add(new JLabel(tr("Latitude: ")), gc);
258
259            // --------
260            gc.gridx = 1;
261            gc.gridy = 0;
262            gc.fill = GridBagConstraints.HORIZONTAL;
263            gc.weightx = 1.0;
264            add(lblLat, gc);
265
266            // --------
267            gc.gridx = 0;
268            gc.gridy = 1;
269            gc.fill = GridBagConstraints.NONE;
270            gc.weightx = 0.0;
271            gc.anchor = GridBagConstraints.NORTHWEST;
272            add(new JLabel(tr("Longitude: ")), gc);
273
274            // --------
275            gc.gridx = 1;
276            gc.gridy = 1;
277            gc.fill = GridBagConstraints.HORIZONTAL;
278            gc.weightx = 1.0;
279            add(lblLon, gc);
280        }
281
282        /**
283         * Constructs a new {@code LatLonViewer}.
284         * @param model a model
285         * @param role the role for this viewer.
286         */
287        LatLonViewer(HistoryBrowserModel model, PointInTimeType role) {
288            super(new GridBagLayout());
289            this.updater = new Updater(model, role);
290            this.modifiedColor = PointInTimeType.CURRENT_POINT_IN_TIME == role
291                    ? TwoColumnDiff.Item.DiffItemType.INSERTED.getColor()
292                    : TwoColumnDiff.Item.DiffItemType.DELETED.getColor();
293            build();
294        }
295
296        protected void refresh() {
297            final Pair<LatLon, LatLon> coordinates = updater.getCoordinates();
298            if (coordinates == null) return;
299            final LatLon coord = coordinates.a;
300            final LatLon oppositeCoord = coordinates.b;
301
302            // display the coordinates
303            lblLat.setText(coord != null ? DecimalDegreesCoordinateFormat.INSTANCE.latToString(coord) : tr("(none)"));
304            lblLon.setText(coord != null ? DecimalDegreesCoordinateFormat.INSTANCE.lonToString(coord) : tr("(none)"));
305
306            // update background color to reflect differences in the coordinates
307            if (coord == oppositeCoord ||
308                    (coord != null && oppositeCoord != null && coord.lat() == oppositeCoord.lat())) {
309                GuiHelper.setBackgroundReadable(lblLat, Color.WHITE);
310            } else {
311                GuiHelper.setBackgroundReadable(lblLat, modifiedColor);
312            }
313            if (coord == oppositeCoord ||
314                    (coord != null && oppositeCoord != null && coord.lon() == oppositeCoord.lon())) {
315                GuiHelper.setBackgroundReadable(lblLon, Color.WHITE);
316            } else {
317                GuiHelper.setBackgroundReadable(lblLon, modifiedColor);
318            }
319        }
320
321        @Override
322        public void stateChanged(ChangeEvent e) {
323            refresh();
324        }
325
326        @Override
327        public void destroy() {
328            lblLat.destroy();
329            lblLon.destroy();
330        }
331    }
332
333    private static class MapViewer extends JosmMapViewer implements ChangeListener {
334
335        private final transient Updater updater;
336
337        MapViewer(HistoryBrowserModel model) {
338            this.updater = new Updater(model, PointInTimeType.REFERENCE_POINT_IN_TIME);
339            setTileSource(SlippyMapBBoxChooser.DefaultOsmTileSourceProvider.get()); // for attribution
340            setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
341            addMouseListener(new MouseAdapter() {
342                @Override
343                public void mouseClicked(MouseEvent e) {
344                    if (e.getButton() == MouseEvent.BUTTON1) {
345                        getAttribution().handleAttribution(e.getPoint(), true);
346                    }
347                }
348            });
349        }
350
351        @Override
352        public void stateChanged(ChangeEvent e) {
353            final Pair<LatLon, LatLon> coordinates = updater.getCoordinates();
354            if (coordinates == null) {
355                return;
356            }
357
358            removeAllMapMarkers();
359
360            if (coordinates.a != null) {
361                final MapMarkerDot oldMarker = new MapMarkerDot(coordinates.a.lat(), coordinates.a.lon());
362                oldMarker.setBackColor(TwoColumnDiff.Item.DiffItemType.DELETED.getColor());
363                addMapMarker(oldMarker);
364            }
365            if (coordinates.b != null) {
366                final MapMarkerDot newMarker = new MapMarkerDot(coordinates.b.lat(), coordinates.b.lon());
367                newMarker.setBackColor(TwoColumnDiff.Item.DiffItemType.INSERTED.getColor());
368                addMapMarker(newMarker);
369            }
370
371            super.setDisplayToFitMapMarkers();
372        }
373    }
374
375    private static class DistanceViewer extends JPanel implements ChangeListener, Destroyable {
376
377        private final JosmTextArea lblDistance = newTextArea();
378        private final transient Updater updater;
379
380        DistanceViewer(HistoryBrowserModel model) {
381            super(new GridBagLayout());
382            updater = new Updater(model, PointInTimeType.REFERENCE_POINT_IN_TIME);
383            build();
384        }
385
386        protected void build() {
387            setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
388            GridBagConstraints gc = new GridBagConstraints();
389
390            // --------
391            gc.gridx = 0;
392            gc.gridy = 0;
393            gc.fill = GridBagConstraints.NONE;
394            gc.weightx = 0.0;
395            gc.insets = new Insets(5, 5, 5, 5);
396            gc.anchor = GridBagConstraints.NORTHWEST;
397            add(new JLabel(tr("Distance: ")), gc);
398
399            // --------
400            gc.gridx = 1;
401            gc.gridy = 0;
402            gc.fill = GridBagConstraints.HORIZONTAL;
403            gc.weightx = 1.0;
404            add(lblDistance, gc);
405        }
406
407        protected void refresh() {
408            final Pair<LatLon, LatLon> coordinates = updater.getCoordinates();
409            if (coordinates == null) return;
410            final LatLon coord = coordinates.a;
411            final LatLon oppositeCoord = coordinates.b;
412
413            // update distance
414            //
415            if (coord != null && oppositeCoord != null) {
416                double distance = coord.greatCircleDistance(oppositeCoord);
417                GuiHelper.setBackgroundReadable(lblDistance, distance > 0
418                        ? TwoColumnDiff.Item.DiffItemType.CHANGED.getColor()
419                        : Color.WHITE);
420                lblDistance.setText(NavigatableComponent.getDistText(distance));
421            } else {
422                GuiHelper.setBackgroundReadable(lblDistance, coord != oppositeCoord
423                        ? TwoColumnDiff.Item.DiffItemType.CHANGED.getColor()
424                        : Color.WHITE);
425                lblDistance.setText(tr("(none)"));
426            }
427        }
428
429        @Override
430        public void stateChanged(ChangeEvent e) {
431            refresh();
432        }
433
434        @Override
435        public void destroy() {
436            lblDistance.destroy();
437        }
438    }
439
440    private class RestoreCoordinateAction extends AbstractAction {
441
442        RestoreCoordinateAction() {
443            super(tr("Restore"));
444            new ImageProvider("undo").getResource().attachImageIcon(this, true);
445        }
446
447        @Override
448        public void actionPerformed(ActionEvent e) {
449            OsmPrimitive primitive = getPrimitiveFromDataSet(PointInTimeType.REFERENCE_POINT_IN_TIME);
450            if (!(primitive instanceof Node)) {
451                return;
452            }
453            HistoryOsmPrimitive historyPrimitive = model.getPointInTime(PointInTimeType.REFERENCE_POINT_IN_TIME);
454            if (!(historyPrimitive instanceof HistoryNode) || ((HistoryNode) historyPrimitive).getCoords() == null) {
455                return;
456            }
457            MoveCommand command = new MoveCommand(((Node) primitive), ((HistoryNode) historyPrimitive).getCoords());
458            UndoRedoHandler.getInstance().add(command);
459        }
460    }
461}