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