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