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}