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}