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