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}