001 // License: GPL. See LICENSE file for details. 002 003 package org.openstreetmap.josm.gui.layer; 004 005 import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 006 import static org.openstreetmap.josm.tools.I18n.marktr; 007 import static org.openstreetmap.josm.tools.I18n.tr; 008 import static org.openstreetmap.josm.tools.I18n.trn; 009 010 import java.awt.BasicStroke; 011 import java.awt.Color; 012 import java.awt.Component; 013 import java.awt.Dimension; 014 import java.awt.Graphics2D; 015 import java.awt.GridBagLayout; 016 import java.awt.Point; 017 import java.awt.RenderingHints; 018 import java.awt.Stroke; 019 import java.awt.Toolkit; 020 import java.awt.event.ActionEvent; 021 import java.awt.event.MouseAdapter; 022 import java.awt.event.MouseEvent; 023 import java.awt.event.MouseListener; 024 import java.awt.geom.Area; 025 import java.awt.geom.Rectangle2D; 026 import java.io.File; 027 import java.io.IOException; 028 import java.net.MalformedURLException; 029 import java.net.URL; 030 import java.text.DateFormat; 031 import java.util.ArrayList; 032 import java.util.Arrays; 033 import java.util.Collection; 034 import java.util.Collections; 035 import java.util.Comparator; 036 import java.util.LinkedList; 037 import java.util.List; 038 import java.util.Map; 039 import java.util.concurrent.Future; 040 041 import javax.swing.AbstractAction; 042 import javax.swing.Action; 043 import javax.swing.BorderFactory; 044 import javax.swing.Icon; 045 import javax.swing.JComponent; 046 import javax.swing.JFileChooser; 047 import javax.swing.JLabel; 048 import javax.swing.JList; 049 import javax.swing.JMenuItem; 050 import javax.swing.JOptionPane; 051 import javax.swing.JPanel; 052 import javax.swing.JScrollPane; 053 import javax.swing.JTable; 054 import javax.swing.ListSelectionModel; 055 import javax.swing.SwingUtilities; 056 import javax.swing.event.ListSelectionEvent; 057 import javax.swing.event.ListSelectionListener; 058 import javax.swing.filechooser.FileFilter; 059 import javax.swing.table.TableCellRenderer; 060 061 import org.openstreetmap.josm.Main; 062 import org.openstreetmap.josm.actions.AbstractMergeAction.LayerListCellRenderer; 063 import org.openstreetmap.josm.actions.DiskAccessAction; 064 import org.openstreetmap.josm.actions.RenameLayerAction; 065 import org.openstreetmap.josm.actions.SaveActionBase; 066 import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTaskList; 067 import org.openstreetmap.josm.data.Bounds; 068 import org.openstreetmap.josm.data.coor.EastNorth; 069 import org.openstreetmap.josm.data.coor.LatLon; 070 import org.openstreetmap.josm.data.gpx.GpxData; 071 import org.openstreetmap.josm.data.gpx.GpxRoute; 072 import org.openstreetmap.josm.data.gpx.GpxTrack; 073 import org.openstreetmap.josm.data.gpx.GpxTrackSegment; 074 import org.openstreetmap.josm.data.gpx.WayPoint; 075 import org.openstreetmap.josm.data.osm.DataSet; 076 import org.openstreetmap.josm.data.osm.Node; 077 import org.openstreetmap.josm.data.osm.Way; 078 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 079 import org.openstreetmap.josm.data.projection.Projection; 080 import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil; 081 import org.openstreetmap.josm.gui.ExtendedDialog; 082 import org.openstreetmap.josm.gui.HelpAwareOptionPane; 083 import org.openstreetmap.josm.gui.MapView; 084 import org.openstreetmap.josm.gui.NavigatableComponent; 085 import org.openstreetmap.josm.gui.PleaseWaitRunnable; 086 import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 087 import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 088 import org.openstreetmap.josm.gui.layer.WMSLayer.PrecacheTask; 089 import org.openstreetmap.josm.gui.layer.markerlayer.AudioMarker; 090 import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer; 091 import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel; 092 import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 093 import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor; 094 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 095 import org.openstreetmap.josm.gui.progress.ProgressTaskId; 096 import org.openstreetmap.josm.gui.progress.ProgressTaskIds; 097 import org.openstreetmap.josm.gui.widgets.HtmlPanel; 098 import org.openstreetmap.josm.gui.widgets.JFileChooserManager; 099 import org.openstreetmap.josm.gui.widgets.JosmComboBox; 100 import org.openstreetmap.josm.io.GpxImporter; 101 import org.openstreetmap.josm.io.JpgImporter; 102 import org.openstreetmap.josm.io.OsmTransferException; 103 import org.openstreetmap.josm.tools.AudioUtil; 104 import org.openstreetmap.josm.tools.DateUtils; 105 import org.openstreetmap.josm.tools.GBC; 106 import org.openstreetmap.josm.tools.ImageProvider; 107 import org.openstreetmap.josm.tools.OpenBrowser; 108 import org.openstreetmap.josm.tools.UrlLabel; 109 import org.openstreetmap.josm.tools.Utils; 110 import org.openstreetmap.josm.tools.WindowGeometry; 111 import org.xml.sax.SAXException; 112 113 public class GpxLayer extends Layer { 114 115 private static final String PREF_DOWNLOAD_ALONG_TRACK_DISTANCE = "gpxLayer.downloadAlongTrack.distance"; 116 private static final String PREF_DOWNLOAD_ALONG_TRACK_AREA = "gpxLayer.downloadAlongTrack.area"; 117 private static final String PREF_DOWNLOAD_ALONG_TRACK_NEAR = "gpxLayer.downloadAlongTrack.near"; 118 119 public GpxData data; 120 protected static final double PHI = Math.toRadians(15); 121 private boolean computeCacheInSync; 122 private int computeCacheMaxLineLengthUsed; 123 private Color computeCacheColorUsed; 124 private boolean computeCacheColorDynamic; 125 private colorModes computeCacheColored; 126 private int computeCacheColorTracksTune; 127 private boolean isLocalFile; 128 // used by ChooseTrackVisibilityAction to determine which tracks to show/hide 129 private boolean[] trackVisibility = new boolean[0]; 130 131 private final List<GpxTrack> lastTracks = new ArrayList<GpxTrack>(); // List of tracks at last paint 132 private int lastUpdateCount; 133 134 private static class Markers { 135 public boolean timedMarkersOmitted = false; 136 public boolean untimedMarkersOmitted = false; 137 } 138 139 public GpxLayer(GpxData d) { 140 super((String) d.attr.get("name")); 141 data = d; 142 computeCacheInSync = false; 143 ensureTrackVisibilityLength(); 144 } 145 146 public GpxLayer(GpxData d, String name) { 147 this(d); 148 this.setName(name); 149 } 150 151 public GpxLayer(GpxData d, String name, boolean isLocal) { 152 this(d); 153 this.setName(name); 154 this.isLocalFile = isLocal; 155 } 156 157 /** 158 * returns a human readable string that shows the timespan of the given track 159 */ 160 private static String getTimespanForTrack(GpxTrack trk) { 161 WayPoint earliest = null, latest = null; 162 163 for (GpxTrackSegment seg : trk.getSegments()) { 164 for (WayPoint pnt : seg.getWayPoints()) { 165 if (latest == null) { 166 latest = earliest = pnt; 167 } else { 168 if (pnt.compareTo(earliest) < 0) { 169 earliest = pnt; 170 } else { 171 latest = pnt; 172 } 173 } 174 } 175 } 176 177 String ts = ""; 178 179 if (earliest != null && latest != null) { 180 DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT); 181 String earliestDate = df.format(earliest.getTime()); 182 String latestDate = df.format(latest.getTime()); 183 184 if (earliestDate.equals(latestDate)) { 185 DateFormat tf = DateFormat.getTimeInstance(DateFormat.SHORT); 186 ts += earliestDate + " "; 187 ts += tf.format(earliest.getTime()) + " - " + tf.format(latest.getTime()); 188 } else { 189 DateFormat dtf = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); 190 ts += dtf.format(earliest.getTime()) + " - " + dtf.format(latest.getTime()); 191 } 192 193 int diff = (int) (latest.time - earliest.time); 194 ts += String.format(" (%d:%02d)", diff / 3600, (diff % 3600) / 60); 195 } 196 return ts; 197 } 198 199 @Override 200 public Icon getIcon() { 201 return ImageProvider.get("layer", "gpx_small"); 202 } 203 204 @Override 205 public Object getInfoComponent() { 206 StringBuilder info = new StringBuilder(); 207 208 if (data.attr.containsKey("name")) { 209 info.append(tr("Name: {0}", data.attr.get(GpxData.META_NAME))).append("<br>"); 210 } 211 212 if (data.attr.containsKey("desc")) { 213 info.append(tr("Description: {0}", data.attr.get(GpxData.META_DESC))).append("<br>"); 214 } 215 216 if (data.tracks.size() > 0) { 217 info.append("<table><thead align='center'><tr><td colspan='5'>" 218 + trn("{0} track", "{0} tracks", data.tracks.size(), data.tracks.size()) 219 + "</td></tr><tr align='center'><td>" + tr("Name") + "</td><td>" 220 + tr("Description") + "</td><td>" + tr("Timespan") 221 + "</td><td>" + tr("Length") + "</td><td>" + tr("URL") 222 + "</td></tr></thead>"); 223 224 for (GpxTrack trk : data.tracks) { 225 info.append("<tr><td>"); 226 if (trk.getAttributes().containsKey("name")) { 227 info.append(trk.getAttributes().get("name")); 228 } 229 info.append("</td><td>"); 230 if (trk.getAttributes().containsKey("desc")) { 231 info.append(" ").append(trk.getAttributes().get("desc")); 232 } 233 info.append("</td><td>"); 234 info.append(getTimespanForTrack(trk)); 235 info.append("</td><td>"); 236 info.append(NavigatableComponent.getSystemOfMeasurement().getDistText(trk.length())); 237 info.append("</td><td>"); 238 if (trk.getAttributes().containsKey("url")) { 239 info.append(trk.getAttributes().get("url")); 240 } 241 info.append("</td></tr>"); 242 } 243 244 info.append("</table><br><br>"); 245 246 } 247 248 info.append(tr("Length: {0}", NavigatableComponent.getSystemOfMeasurement().getDistText(data.length()))).append("<br>"); 249 250 info.append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())).append( 251 trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>"); 252 253 final JScrollPane sp = new JScrollPane(new HtmlPanel(info.toString()), JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); 254 sp.setPreferredSize(new Dimension(sp.getPreferredSize().width, 350)); 255 SwingUtilities.invokeLater(new Runnable() { 256 @Override 257 public void run() { 258 sp.getVerticalScrollBar().setValue(0); 259 } 260 }); 261 return sp; 262 } 263 264 @Override 265 public Color getColor(boolean ignoreCustom) { 266 Color c = Main.pref.getColor(marktr("gps point"), "layer " + getName(), Color.gray); 267 268 return ignoreCustom || getColorMode() == colorModes.none ? c : null; 269 } 270 271 public colorModes getColorMode() { 272 try { 273 int i=Main.pref.getInteger("draw.rawgps.colors", "layer " + getName(), 0); 274 return colorModes.values()[i]; 275 } catch (Exception e) { 276 } 277 return colorModes.none; 278 } 279 280 /* for preferences */ 281 static public Color getGenericColor() { 282 return Main.pref.getColor(marktr("gps point"), Color.gray); 283 } 284 285 @Override 286 public Action[] getMenuEntries() { 287 if (Main.applet) 288 return new Action[] { 289 LayerListDialog.getInstance().createShowHideLayerAction(), 290 LayerListDialog.getInstance().createDeleteLayerAction(), 291 SeparatorLayerAction.INSTANCE, 292 new CustomizeColor(this), 293 new CustomizeDrawing(this), 294 new ConvertToDataLayerAction(), 295 SeparatorLayerAction.INSTANCE, 296 new ChooseTrackVisibilityAction(), 297 new RenameLayerAction(getAssociatedFile(), this), 298 SeparatorLayerAction.INSTANCE, 299 new LayerListPopup.InfoAction(this) }; 300 return new Action[] { 301 LayerListDialog.getInstance().createShowHideLayerAction(), 302 LayerListDialog.getInstance().createDeleteLayerAction(), 303 SeparatorLayerAction.INSTANCE, 304 new LayerSaveAction(this), 305 new LayerSaveAsAction(this), 306 new CustomizeColor(this), 307 new CustomizeDrawing(this), 308 new ImportImages(), 309 new ImportAudio(), 310 new MarkersFromNamedPoins(), 311 new ConvertToDataLayerAction(), 312 new DownloadAlongTrackAction(), 313 new DownloadWmsAlongTrackAction(), 314 SeparatorLayerAction.INSTANCE, 315 new ChooseTrackVisibilityAction(), 316 new RenameLayerAction(getAssociatedFile(), this), 317 SeparatorLayerAction.INSTANCE, 318 new LayerListPopup.InfoAction(this) }; 319 } 320 321 @Override 322 public String getToolTipText() { 323 StringBuilder info = new StringBuilder().append("<html>"); 324 325 if (data.attr.containsKey("name")) { 326 info.append(tr("Name: {0}", data.attr.get(GpxData.META_NAME))).append("<br>"); 327 } 328 329 if (data.attr.containsKey("desc")) { 330 info.append(tr("Description: {0}", data.attr.get(GpxData.META_DESC))).append("<br>"); 331 } 332 333 info.append(trn("{0} track, ", "{0} tracks, ", data.tracks.size(), data.tracks.size())); 334 info.append(trn("{0} route, ", "{0} routes, ", data.routes.size(), data.routes.size())); 335 info.append(trn("{0} waypoint", "{0} waypoints", data.waypoints.size(), data.waypoints.size())).append("<br>"); 336 337 info.append(tr("Length: {0}", NavigatableComponent.getSystemOfMeasurement().getDistText(data.length()))); 338 info.append("<br>"); 339 340 return info.append("</html>").toString(); 341 } 342 343 @Override 344 public boolean isMergable(Layer other) { 345 return other instanceof GpxLayer; 346 } 347 348 private int sumUpdateCount() { 349 int updateCount = 0; 350 for (GpxTrack track: data.tracks) { 351 updateCount += track.getUpdateCount(); 352 } 353 return updateCount; 354 } 355 356 @Override 357 public boolean isChanged() { 358 if (data.tracks.equals(lastTracks)) 359 return sumUpdateCount() != lastUpdateCount; 360 else 361 return true; 362 } 363 364 @Override 365 public void mergeFrom(Layer from) { 366 data.mergeFrom(((GpxLayer) from).data); 367 computeCacheInSync = false; 368 } 369 370 private final static Color[] colors = new Color[256]; 371 static { 372 for (int i = 0; i < colors.length; i++) { 373 colors[i] = Color.getHSBColor(i / 300.0f, 1, 1); 374 } 375 } 376 377 private final static Color[] colors_cyclic = new Color[256]; 378 static { 379 for (int i = 0; i < colors_cyclic.length; i++) { 380 // red yellow green blue red 381 int[] h = new int[] { 0, 59, 127, 244, 360}; 382 int[] s = new int[] { 100, 84, 99, 100 }; 383 int[] b = new int[] { 90, 93, 74, 83 }; 384 385 float angle = 4 - i / 256f * 4; 386 int quadrant = (int) angle; 387 angle -= quadrant; 388 quadrant = Utils.mod(quadrant+1, 4); 389 390 float vh = h[quadrant] * w(angle) + h[quadrant+1] * (1 - w(angle)); 391 float vs = s[quadrant] * w(angle) + s[Utils.mod(quadrant+1, 4)] * (1 - w(angle)); 392 float vb = b[quadrant] * w(angle) + b[Utils.mod(quadrant+1, 4)] * (1 - w(angle)); 393 394 colors_cyclic[i] = Color.getHSBColor(vh/360f, vs/100f, vb/100f); 395 } 396 } 397 398 /** 399 * transition function: 400 * w(0)=1, w(1)=0, 0<=w(x)<=1 401 * @param x number: 0<=x<=1 402 * @return the weighted value 403 */ 404 private static float w(float x) { 405 if (x < 0.5) 406 return 1 - 2*x*x; 407 else 408 return 2*(1-x)*(1-x); 409 } 410 411 // lookup array to draw arrows without doing any math 412 private final static int ll0 = 9; 413 private final static int sl4 = 5; 414 private final static int sl9 = 3; 415 private final static int[][] dir = { { +sl4, +ll0, +ll0, +sl4 }, { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 }, 416 { -ll0, -sl9, -ll0, +sl9 }, { -sl4, -ll0, -ll0, -sl4 }, { +sl9, -ll0, -sl9, -ll0 }, 417 { +ll0, -sl4, +sl4, -ll0 }, { +ll0, +sl9, +ll0, -sl9 }, { +sl4, +ll0, +ll0, +sl4 }, 418 { -sl9, +ll0, +sl9, +ll0 }, { -ll0, +sl4, -sl4, +ll0 }, { -ll0, -sl9, -ll0, +sl9 } }; 419 420 // the different color modes 421 enum colorModes { 422 none, velocity, dilution, direction, time 423 } 424 425 @Override 426 public void paint(Graphics2D g, MapView mv, Bounds box) { 427 lastUpdateCount = sumUpdateCount(); 428 lastTracks.clear(); 429 lastTracks.addAll(data.tracks); 430 431 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 432 Main.pref.getBoolean("mappaint.gpx.use-antialiasing", false) ? 433 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); 434 435 /**************************************************************** 436 ********** STEP 1 - GET CONFIG VALUES ************************** 437 ****************************************************************/ 438 // Long startTime = System.currentTimeMillis(); 439 Color neutralColor = getColor(true); 440 String spec="layer "+getName(); 441 442 // also draw lines between points belonging to different segments 443 boolean forceLines = Main.pref.getBoolean("draw.rawgps.lines.force", spec, false); 444 // draw direction arrows on the lines 445 boolean direction = Main.pref.getBoolean("draw.rawgps.direction", spec, false); 446 // don't draw lines if longer than x meters 447 int lineWidth = Main.pref.getInteger("draw.rawgps.linewidth", spec, 0); 448 449 int maxLineLength; 450 boolean lines; 451 if (!this.data.fromServer) { 452 maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length.local", spec, -1); 453 lines = Main.pref.getBoolean("draw.rawgps.lines.local", spec, true); 454 } else { 455 maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length", spec, 200); 456 lines = Main.pref.getBoolean("draw.rawgps.lines", spec, true); 457 } 458 // paint large dots for points 459 boolean large = Main.pref.getBoolean("draw.rawgps.large", spec, false); 460 int largesize = Main.pref.getInteger("draw.rawgps.large.size", spec, 3); 461 boolean hdopcircle = Main.pref.getBoolean("draw.rawgps.hdopcircle", spec, false); 462 // color the lines 463 colorModes colored = getColorMode(); 464 // paint direction arrow with alternate math. may be faster 465 boolean alternatedirection = Main.pref.getBoolean("draw.rawgps.alternatedirection", spec, false); 466 // don't draw arrows nearer to each other than this 467 int delta = Main.pref.getInteger("draw.rawgps.min-arrow-distance", spec, 40); 468 // allows to tweak line coloring for different speed levels. 469 int colorTracksTune = Main.pref.getInteger("draw.rawgps.colorTracksTune", spec, 45); 470 boolean colorModeDynamic = Main.pref.getBoolean("draw.rawgps.colors.dynamic", spec, false); 471 int hdopfactor = Main.pref.getInteger("hdop.factor", 25); 472 473 Stroke storedStroke = g.getStroke(); 474 if(lineWidth != 0) 475 { 476 g.setStroke(new BasicStroke(lineWidth,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND)); 477 largesize += lineWidth; 478 } 479 480 /**************************************************************** 481 ********** STEP 2a - CHECK CACHE VALIDITY ********************** 482 ****************************************************************/ 483 if ((computeCacheMaxLineLengthUsed != maxLineLength) || (!neutralColor.equals(computeCacheColorUsed)) 484 || (computeCacheColored != colored) || (computeCacheColorTracksTune != colorTracksTune) 485 || (computeCacheColorDynamic != colorModeDynamic)) { 486 computeCacheMaxLineLengthUsed = maxLineLength; 487 computeCacheInSync = false; 488 computeCacheColorUsed = neutralColor; 489 computeCacheColored = colored; 490 computeCacheColorTracksTune = colorTracksTune; 491 computeCacheColorDynamic = colorModeDynamic; 492 } 493 494 /**************************************************************** 495 ********** STEP 2b - RE-COMPUTE CACHE DATA ********************* 496 ****************************************************************/ 497 if (!computeCacheInSync) { // don't compute if the cache is good 498 double minval = +1e10; 499 double maxval = -1e10; 500 WayPoint oldWp = null; 501 if (colorModeDynamic) { 502 if (colored == colorModes.velocity) { 503 for (GpxTrack trk : data.tracks) { 504 for (GpxTrackSegment segment : trk.getSegments()) { 505 if(!forceLines) { 506 oldWp = null; 507 } 508 for (WayPoint trkPnt : segment.getWayPoints()) { 509 LatLon c = trkPnt.getCoor(); 510 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 511 continue; 512 } 513 if (oldWp != null && trkPnt.time > oldWp.time) { 514 double vel = c.greatCircleDistance(oldWp.getCoor()) 515 / (trkPnt.time - oldWp.time); 516 if(vel > maxval) { 517 maxval = vel; 518 } 519 if(vel < minval) { 520 minval = vel; 521 } 522 } 523 oldWp = trkPnt; 524 } 525 } 526 } 527 } else if (colored == colorModes.dilution) { 528 for (GpxTrack trk : data.tracks) { 529 for (GpxTrackSegment segment : trk.getSegments()) { 530 for (WayPoint trkPnt : segment.getWayPoints()) { 531 Object val = trkPnt.attr.get("hdop"); 532 if (val != null) { 533 double hdop = ((Float) val).doubleValue(); 534 if(hdop > maxval) { 535 maxval = hdop; 536 } 537 if(hdop < minval) { 538 minval = hdop; 539 } 540 } 541 } 542 } 543 } 544 } 545 oldWp = null; 546 } 547 if (colored == colorModes.time) { 548 for (GpxTrack trk : data.tracks) { 549 for (GpxTrackSegment segment : trk.getSegments()) { 550 for (WayPoint trkPnt : segment.getWayPoints()) { 551 double t=trkPnt.time; 552 if (t==0) { 553 continue; // skip non-dated trackpoints 554 } 555 if(t > maxval) { 556 maxval = t; 557 } 558 if(t < minval) { 559 minval = t; 560 } 561 } 562 } 563 } 564 } 565 566 for (GpxTrack trk : data.tracks) { 567 for (GpxTrackSegment segment : trk.getSegments()) { 568 if (!forceLines) { // don't draw lines between segments, unless forced to 569 oldWp = null; 570 } 571 for (WayPoint trkPnt : segment.getWayPoints()) { 572 LatLon c = trkPnt.getCoor(); 573 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 574 continue; 575 } 576 trkPnt.customColoring = neutralColor; 577 if(colored == colorModes.dilution && trkPnt.attr.get("hdop") != null) { 578 float hdop = ((Float) trkPnt.attr.get("hdop")).floatValue(); 579 int hdoplvl =(int) Math.round(colorModeDynamic ? ((hdop-minval)*255/(maxval-minval)) 580 : (hdop <= 0 ? 0 : hdop * hdopfactor)); 581 // High hdop is bad, but high values in colors are green. 582 // Therefore inverse the logic 583 int hdopcolor = 255 - (hdoplvl > 255 ? 255 : hdoplvl); 584 trkPnt.customColoring = colors[hdopcolor]; 585 } 586 if (oldWp != null) { 587 double dist = c.greatCircleDistance(oldWp.getCoor()); 588 boolean noDraw=false; 589 switch (colored) { 590 case velocity: 591 double dtime = trkPnt.time - oldWp.time; 592 if(dtime > 0) { 593 float vel = (float) (dist / dtime); 594 int velColor =(int) Math.round(colorModeDynamic ? ((vel-minval)*255/(maxval-minval)) 595 : (vel <= 0 ? 0 : vel / colorTracksTune * 255)); 596 trkPnt.customColoring = colors[Math.max(0, Math.min(velColor, 255))]; 597 } else { 598 trkPnt.customColoring = colors[255]; 599 } 600 break; 601 case direction: 602 double dirColor = oldWp.getCoor().heading(trkPnt.getCoor()) / (2.0 * Math.PI) * 256; 603 // Bad case first 604 if (dirColor != dirColor || dirColor < 0.0 || dirColor >= 256.0) { 605 trkPnt.customColoring = colors_cyclic[0]; 606 } else { 607 trkPnt.customColoring = colors_cyclic[(int) (dirColor)]; 608 } 609 break; 610 case time: 611 if (trkPnt.time>0){ 612 int tColor = (int) Math.round((trkPnt.time-minval)*255/(maxval-minval)); 613 trkPnt.customColoring = colors[tColor]; 614 } else { 615 trkPnt.customColoring = neutralColor; 616 } 617 break; 618 } 619 620 if (!noDraw && (maxLineLength == -1 || dist <= maxLineLength)) { 621 trkPnt.drawLine = true; 622 trkPnt.dir = (int) oldWp.getCoor().heading(trkPnt.getCoor()); 623 } else { 624 trkPnt.drawLine = false; 625 } 626 } else { // make sure we reset outdated data 627 trkPnt.drawLine = false; 628 } 629 oldWp = trkPnt; 630 } 631 } 632 } 633 computeCacheInSync = true; 634 } 635 636 LinkedList<WayPoint> visibleSegments = new LinkedList<WayPoint>(); 637 WayPoint last = null; 638 int i = 0; 639 ensureTrackVisibilityLength(); 640 for (GpxTrack trk: data.tracks) { 641 // hide tracks that were de-selected in ChooseTrackVisibilityAction 642 if(!trackVisibility[i++]) { 643 continue; 644 } 645 646 for (GpxTrackSegment trkSeg: trk.getSegments()) { 647 for(WayPoint pt : trkSeg.getWayPoints()) 648 { 649 Bounds b = new Bounds(pt.getCoor()); 650 // last should never be null when this is true! 651 if(pt.drawLine) { 652 b.extend(last.getCoor()); 653 } 654 if(b.intersects(box)) 655 { 656 if(last != null && (visibleSegments.isEmpty() 657 || visibleSegments.getLast() != last)) { 658 if(last.drawLine) { 659 WayPoint l = new WayPoint(last); 660 l.drawLine = false; 661 visibleSegments.add(l); 662 } else { 663 visibleSegments.add(last); 664 } 665 } 666 visibleSegments.add(pt); 667 } 668 last = pt; 669 } 670 } 671 } 672 if(visibleSegments.isEmpty()) 673 return; 674 675 /**************************************************************** 676 ********** STEP 3a - DRAW LINES ******************************** 677 ****************************************************************/ 678 if (lines) { 679 Point old = null; 680 for (WayPoint trkPnt : visibleSegments) { 681 LatLon c = trkPnt.getCoor(); 682 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 683 continue; 684 } 685 Point screen = mv.getPoint(trkPnt.getEastNorth()); 686 if (trkPnt.drawLine) { 687 // skip points that are on the same screenposition 688 if (old != null && ((old.x != screen.x) || (old.y != screen.y))) { 689 g.setColor(trkPnt.customColoring); 690 g.drawLine(old.x, old.y, screen.x, screen.y); 691 } 692 } 693 old = screen; 694 } // end for trkpnt 695 } // end if lines 696 697 /**************************************************************** 698 ********** STEP 3b - DRAW NICE ARROWS ************************** 699 ****************************************************************/ 700 if (lines && direction && !alternatedirection) { 701 Point old = null; 702 Point oldA = null; // last arrow painted 703 for (WayPoint trkPnt : visibleSegments) { 704 LatLon c = trkPnt.getCoor(); 705 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 706 continue; 707 } 708 if (trkPnt.drawLine) { 709 Point screen = mv.getPoint(trkPnt.getEastNorth()); 710 // skip points that are on the same screenposition 711 if (old != null 712 && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta 713 || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) { 714 g.setColor(trkPnt.customColoring); 715 double t = Math.atan2(screen.y - old.y, screen.x - old.x) + Math.PI; 716 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t - PHI)), 717 (int) (screen.y + 10 * Math.sin(t - PHI))); 718 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t + PHI)), 719 (int) (screen.y + 10 * Math.sin(t + PHI))); 720 oldA = screen; 721 } 722 old = screen; 723 } 724 } // end for trkpnt 725 } // end if lines 726 727 /**************************************************************** 728 ********** STEP 3c - DRAW FAST ARROWS ************************** 729 ****************************************************************/ 730 if (lines && direction && alternatedirection) { 731 Point old = null; 732 Point oldA = null; // last arrow painted 733 for (WayPoint trkPnt : visibleSegments) { 734 LatLon c = trkPnt.getCoor(); 735 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 736 continue; 737 } 738 if (trkPnt.drawLine) { 739 Point screen = mv.getPoint(trkPnt.getEastNorth()); 740 // skip points that are on the same screenposition 741 if (old != null 742 && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta 743 || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) { 744 g.setColor(trkPnt.customColoring); 745 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y 746 + dir[trkPnt.dir][1]); 747 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][2], screen.y 748 + dir[trkPnt.dir][3]); 749 oldA = screen; 750 } 751 old = screen; 752 } 753 } // end for trkpnt 754 } // end if lines 755 756 /**************************************************************** 757 ********** STEP 3d - DRAW LARGE POINTS AND HDOP CIRCLE ********* 758 ****************************************************************/ 759 if (large || hdopcircle) { 760 g.setColor(neutralColor); 761 for (WayPoint trkPnt : visibleSegments) { 762 LatLon c = trkPnt.getCoor(); 763 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 764 continue; 765 } 766 Point screen = mv.getPoint(trkPnt.getEastNorth()); 767 g.setColor(trkPnt.customColoring); 768 if (hdopcircle && trkPnt.attr.get("hdop") != null) { 769 // hdop value 770 float hdop = ((Float)trkPnt.attr.get("hdop")).floatValue(); 771 if (hdop < 0) { 772 hdop = 0; 773 } 774 // hdop pixels 775 int hdopp = mv.getPoint(new LatLon(trkPnt.getCoor().lat(), trkPnt.getCoor().lon() + 2*6*hdop*360/40000000)).x - screen.x; 776 g.drawArc(screen.x-hdopp/2, screen.y-hdopp/2, hdopp, hdopp, 0, 360); 777 } 778 if (large) { 779 g.fillRect(screen.x-1, screen.y-1, largesize, largesize); 780 } 781 } // end for trkpnt 782 } // end if large || hdopcircle 783 784 /**************************************************************** 785 ********** STEP 3e - DRAW SMALL POINTS FOR LINES *************** 786 ****************************************************************/ 787 if (!large && lines) { 788 g.setColor(neutralColor); 789 for (WayPoint trkPnt : visibleSegments) { 790 LatLon c = trkPnt.getCoor(); 791 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 792 continue; 793 } 794 if (!trkPnt.drawLine) { 795 Point screen = mv.getPoint(trkPnt.getEastNorth()); 796 g.drawRect(screen.x, screen.y, 0, 0); 797 } 798 } // end for trkpnt 799 } // end if large 800 801 /**************************************************************** 802 ********** STEP 3f - DRAW SMALL POINTS INSTEAD OF LINES ******** 803 ****************************************************************/ 804 if (!large && !lines) { 805 g.setColor(neutralColor); 806 for (WayPoint trkPnt : visibleSegments) { 807 LatLon c = trkPnt.getCoor(); 808 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) { 809 continue; 810 } 811 Point screen = mv.getPoint(trkPnt.getEastNorth()); 812 g.setColor(trkPnt.customColoring); 813 g.drawRect(screen.x, screen.y, 0, 0); 814 } // end for trkpnt 815 } // end if large 816 817 if(lineWidth != 0) 818 { 819 g.setStroke(storedStroke); 820 } 821 // Long duration = System.currentTimeMillis() - startTime; 822 // System.out.println(duration); 823 } // end paint 824 825 @Override 826 public void visitBoundingBox(BoundingXYVisitor v) { 827 v.visit(data.recalculateBounds()); 828 } 829 830 public class ConvertToDataLayerAction extends AbstractAction { 831 public ConvertToDataLayerAction() { 832 super(tr("Convert to data layer"), ImageProvider.get("converttoosm")); 833 putValue("help", ht("/Action/ConvertToDataLayer")); 834 } 835 836 @Override 837 public void actionPerformed(ActionEvent e) { 838 JPanel msg = new JPanel(new GridBagLayout()); 839 msg 840 .add( 841 new JLabel( 842 tr("<html>Upload of unprocessed GPS data as map data is considered harmful.<br>If you want to upload traces, look here:</html>")), 843 GBC.eol()); 844 msg.add(new UrlLabel(tr("http://www.openstreetmap.org/traces"),2), GBC.eop()); 845 if (!ConditionalOptionPaneUtil.showConfirmationDialog("convert_to_data", Main.parent, msg, tr("Warning"), 846 JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, JOptionPane.OK_OPTION)) 847 return; 848 DataSet ds = new DataSet(); 849 for (GpxTrack trk : data.tracks) { 850 for (GpxTrackSegment segment : trk.getSegments()) { 851 List<Node> nodes = new ArrayList<Node>(); 852 for (WayPoint p : segment.getWayPoints()) { 853 Node n = new Node(p.getCoor()); 854 String timestr = p.getString("time"); 855 if (timestr != null) { 856 n.setTimestamp(DateUtils.fromString(timestr)); 857 } 858 ds.addPrimitive(n); 859 nodes.add(n); 860 } 861 Way w = new Way(); 862 w.setNodes(nodes); 863 ds.addPrimitive(w); 864 } 865 } 866 Main.main 867 .addLayer(new OsmDataLayer(ds, tr("Converted from: {0}", GpxLayer.this.getName()), getAssociatedFile())); 868 Main.main.removeLayer(GpxLayer.this); 869 } 870 } 871 872 @Override 873 public File getAssociatedFile() { 874 return data.storageFile; 875 } 876 877 @Override 878 public void setAssociatedFile(File file) { 879 data.storageFile = file; 880 } 881 882 /** ensures the trackVisibility array has the correct length without losing data. 883 * additional entries are initialized to true; 884 */ 885 final private void ensureTrackVisibilityLength() { 886 final int l = data.tracks.size(); 887 if(l == trackVisibility.length) 888 return; 889 final boolean[] back = trackVisibility.clone(); 890 final int m = Math.min(l, back.length); 891 trackVisibility = new boolean[l]; 892 for(int i=0; i < m; i++) { 893 trackVisibility[i] = back[i]; 894 } 895 for(int i=m; i < l; i++) { 896 trackVisibility[i] = true; 897 } 898 } 899 900 /** 901 * allows the user to choose which of the downloaded tracks should be displayed. 902 * they can be chosen from the gpx layer context menu. 903 */ 904 public class ChooseTrackVisibilityAction extends AbstractAction { 905 public ChooseTrackVisibilityAction() { 906 super(tr("Choose visible tracks"), ImageProvider.get("dialogs/filter")); 907 putValue("help", ht("/Action/ChooseTrackVisibility")); 908 } 909 910 /** 911 * gathers all available data for the tracks and returns them as array of arrays 912 * in the expected column order */ 913 private Object[][] buildTableContents() { 914 Object[][] tracks = new Object[data.tracks.size()][5]; 915 int i = 0; 916 for (GpxTrack trk : data.tracks) { 917 Map<String, Object> attr = trk.getAttributes(); 918 String name = (String) (attr.containsKey("name") ? attr.get("name") : ""); 919 String desc = (String) (attr.containsKey("desc") ? attr.get("desc") : ""); 920 String time = getTimespanForTrack(trk); 921 String length = NavigatableComponent.getSystemOfMeasurement().getDistText(trk.length()); 922 String url = (String) (attr.containsKey("url") ? attr.get("url") : ""); 923 tracks[i] = new String[] {name, desc, time, length, url}; 924 i++; 925 } 926 return tracks; 927 } 928 929 /** 930 * Builds an non-editable table whose 5th column will open a browser when double clicked. 931 * The table will fill its parent. */ 932 private JTable buildTable(String[] headers, Object[][] content) { 933 final JTable t = new JTable(content, headers) { 934 @Override 935 public Component prepareRenderer(TableCellRenderer renderer, int row, int col) { 936 Component c = super.prepareRenderer(renderer, row, col); 937 if (c instanceof JComponent) { 938 JComponent jc = (JComponent)c; 939 jc.setToolTipText((String)getValueAt(row, col)); 940 } 941 return c; 942 } 943 944 @Override 945 public boolean isCellEditable(int rowIndex, int colIndex) { 946 return false; 947 } 948 }; 949 // default column widths 950 t.getColumnModel().getColumn(0).setPreferredWidth(220); 951 t.getColumnModel().getColumn(1).setPreferredWidth(300); 952 t.getColumnModel().getColumn(2).setPreferredWidth(200); 953 t.getColumnModel().getColumn(3).setPreferredWidth(50); 954 t.getColumnModel().getColumn(4).setPreferredWidth(100); 955 // make the link clickable 956 final MouseListener urlOpener = new MouseAdapter() { 957 @Override 958 public void mouseClicked(MouseEvent e) { 959 if (e.getClickCount() != 2) 960 return; 961 JTable t = (JTable)e.getSource(); 962 int col = t.convertColumnIndexToModel(t.columnAtPoint(e.getPoint())); 963 if(col != 4) // only accept clicks on the URL column 964 return; 965 int row = t.rowAtPoint(e.getPoint()); 966 String url = (String) t.getValueAt(row, col); 967 if (url == null || url.isEmpty()) 968 return; 969 OpenBrowser.displayUrl(url); 970 } 971 }; 972 t.addMouseListener(urlOpener); 973 t.setFillsViewportHeight(true); 974 return t; 975 } 976 977 /** selects all rows (=tracks) in the table that are currently visible */ 978 private void selectVisibleTracksInTable(JTable table) { 979 // don't select any tracks if the layer is not visible 980 if(!isVisible()) 981 return; 982 ListSelectionModel s = table.getSelectionModel(); 983 s.clearSelection(); 984 for(int i=0; i < trackVisibility.length; i++) 985 if(trackVisibility[i]) { 986 s.addSelectionInterval(i, i); 987 } 988 } 989 990 /** listens to selection changes in the table and redraws the map */ 991 private void listenToSelectionChanges(JTable table) { 992 table.getSelectionModel().addListSelectionListener(new ListSelectionListener(){ 993 public void valueChanged(ListSelectionEvent e) { 994 if(!(e.getSource() instanceof ListSelectionModel)) 995 return; 996 997 ListSelectionModel s = (ListSelectionModel) e.getSource(); 998 for(int i = 0; i < data.tracks.size(); i++) { 999 trackVisibility[i] = s.isSelectedIndex(i); 1000 } 1001 Main.map.mapView.preferenceChanged(null); 1002 Main.map.repaint(100); 1003 } 1004 }); 1005 } 1006 1007 @Override 1008 public void actionPerformed(ActionEvent arg0) { 1009 final JPanel msg = new JPanel(new GridBagLayout()); 1010 msg.add(new JLabel(tr("<html>Select all tracks that you want to be displayed. You can drag select a " 1011 + "range of tracks or use CTRL+Click to select specific ones. The map is updated live in the " 1012 + "background. Open the URLs by double clicking them.</html>")), 1013 GBC.eol().fill(GBC.HORIZONTAL)); 1014 1015 // build table 1016 final boolean[] trackVisibilityBackup = trackVisibility.clone(); 1017 final String[] headers = {tr("Name"), tr("Description"), tr("Timespan"), tr("Length"), tr("URL")}; 1018 final JTable table = buildTable(headers, buildTableContents()); 1019 selectVisibleTracksInTable(table); 1020 listenToSelectionChanges(table); 1021 1022 // make the table scrollable 1023 JScrollPane scrollPane = new JScrollPane(table); 1024 msg.add(scrollPane, GBC.eol().fill(GBC.BOTH)); 1025 1026 // build dialog 1027 ExtendedDialog ed = new ExtendedDialog( 1028 Main.parent, tr("Set track visibility for {0}", getName()), 1029 new String[] {tr("Show all"), tr("Show selected only"), tr("Cancel")}); 1030 ed.setButtonIcons(new String[] {"dialogs/layerlist/eye", "dialogs/filter", "cancel"}); 1031 ed.setContent(msg, false); 1032 ed.setDefaultButton(2); 1033 ed.setCancelButton(3); 1034 ed.configureContextsensitiveHelp("/Action/ChooseTrackVisibility", true); 1035 ed.setRememberWindowGeometry( 1036 getClass().getName() + ".geometry", 1037 WindowGeometry.centerInWindow(Main.parent, new Dimension(1000, 500)) 1038 ); 1039 ed.showDialog(); 1040 int v = ed.getValue(); 1041 // cancel for unknown buttons and copy back original settings 1042 if(v != 1 && v != 2) { 1043 for(int i = 0; i < data.tracks.size(); i++) { 1044 trackVisibility[i] = trackVisibilityBackup[i]; 1045 } 1046 Main.map.repaint(); 1047 return; 1048 } 1049 1050 // set visibility (1 = show all, 2 = filter). If no tracks are selected 1051 // set all of them visible and... 1052 ListSelectionModel s = table.getSelectionModel(); 1053 final boolean all = v == 1 || s.isSelectionEmpty(); 1054 for(int i = 0; i < data.tracks.size(); i++) { 1055 trackVisibility[i] = all || s.isSelectedIndex(i); 1056 } 1057 // ...sync with layer visibility instead to avoid having two ways to hide everything 1058 setVisible(v == 1 || !s.isSelectionEmpty()); 1059 Main.map.repaint(); 1060 } 1061 } 1062 1063 /** 1064 * Action that issues a series of download requests to the API, following the GPX track. 1065 * 1066 * @author fred 1067 */ 1068 public class DownloadAlongTrackAction extends AbstractAction { 1069 final static int NEAR_TRACK=0; 1070 final static int NEAR_WAYPOINTS=1; 1071 final static int NEAR_BOTH=2; 1072 final Integer dist[] = { 5000, 500, 50 }; 1073 final Integer area[] = { 20, 10, 5, 1 }; 1074 1075 public DownloadAlongTrackAction() { 1076 super(tr("Download from OSM along this track"), ImageProvider.get("downloadalongtrack")); 1077 } 1078 1079 @Override 1080 public void actionPerformed(ActionEvent e) { 1081 /* 1082 * build selection dialog 1083 */ 1084 JPanel msg = new JPanel(new GridBagLayout()); 1085 1086 msg.add(new JLabel(tr("Download everything within:")), GBC.eol()); 1087 String s[] = new String[dist.length]; 1088 for (int i = 0; i < dist.length; ++i) { 1089 s[i] = tr("{0} meters", dist[i]); 1090 } 1091 JList buffer = new JList(s); 1092 buffer.setSelectedIndex(Main.pref.getInteger(PREF_DOWNLOAD_ALONG_TRACK_DISTANCE, 0)); 1093 msg.add(buffer, GBC.eol()); 1094 1095 msg.add(new JLabel(tr("Maximum area per request:")), GBC.eol()); 1096 s = new String[area.length]; 1097 for (int i = 0; i < area.length; ++i) { 1098 s[i] = tr("{0} sq km", area[i]); 1099 } 1100 JList maxRect = new JList(s); 1101 maxRect.setSelectedIndex(Main.pref.getInteger(PREF_DOWNLOAD_ALONG_TRACK_AREA, 0)); 1102 msg.add(maxRect, GBC.eol()); 1103 1104 msg.add(new JLabel(tr("Download near:")), GBC.eol()); 1105 JList downloadNear = new JList(new String[] { tr("track only"), tr("waypoints only"), tr("track and waypoints") }); 1106 1107 downloadNear.setSelectedIndex(Main.pref.getInteger(PREF_DOWNLOAD_ALONG_TRACK_NEAR, 0)); 1108 msg.add(downloadNear, GBC.eol()); 1109 1110 int ret = JOptionPane.showConfirmDialog( 1111 Main.parent, 1112 msg, 1113 tr("Download from OSM along this track"), 1114 JOptionPane.OK_CANCEL_OPTION, 1115 JOptionPane.QUESTION_MESSAGE 1116 ); 1117 switch(ret) { 1118 case JOptionPane.CANCEL_OPTION: 1119 case JOptionPane.CLOSED_OPTION: 1120 return; 1121 default: 1122 // continue 1123 } 1124 1125 Main.pref.putInteger(PREF_DOWNLOAD_ALONG_TRACK_DISTANCE, buffer.getSelectedIndex()); 1126 Main.pref.putInteger(PREF_DOWNLOAD_ALONG_TRACK_AREA, maxRect.getSelectedIndex()); 1127 final int near = downloadNear.getSelectedIndex(); 1128 Main.pref.putInteger(PREF_DOWNLOAD_ALONG_TRACK_NEAR, near); 1129 1130 /* 1131 * Find the average latitude for the data we're contemplating, so we can know how many 1132 * metres per degree of longitude we have. 1133 */ 1134 double latsum = 0; 1135 int latcnt = 0; 1136 1137 if (near == NEAR_TRACK || near == NEAR_BOTH) { 1138 for (GpxTrack trk : data.tracks) { 1139 for (GpxTrackSegment segment : trk.getSegments()) { 1140 for (WayPoint p : segment.getWayPoints()) { 1141 latsum += p.getCoor().lat(); 1142 latcnt++; 1143 } 1144 } 1145 } 1146 } 1147 1148 if (near == NEAR_WAYPOINTS || near == NEAR_BOTH) { 1149 for (WayPoint p : data.waypoints) { 1150 latsum += p.getCoor().lat(); 1151 latcnt++; 1152 } 1153 } 1154 1155 double avglat = latsum / latcnt; 1156 double scale = Math.cos(Math.toRadians(avglat)); 1157 1158 /* 1159 * Compute buffer zone extents and maximum bounding box size. Note that the maximum we 1160 * ever offer is a bbox area of 0.002, while the API theoretically supports 0.25, but as 1161 * soon as you touch any built-up area, that kind of bounding box will download forever 1162 * and then stop because it has more than 50k nodes. 1163 */ 1164 Integer i = buffer.getSelectedIndex(); 1165 final int buffer_dist = dist[i < 0 ? 0 : i]; 1166 i = maxRect.getSelectedIndex(); 1167 final double max_area = area[i < 0 ? 0 : i] / 10000.0 / scale; 1168 final double buffer_y = buffer_dist / 100000.0; 1169 final double buffer_x = buffer_y / scale; 1170 1171 final int totalTicks = latcnt; 1172 // guess if a progress bar might be useful. 1173 final boolean displayProgress = totalTicks > 2000 && buffer_y < 0.01; 1174 1175 class CalculateDownloadArea extends PleaseWaitRunnable { 1176 private Area a = new Area(); 1177 private boolean cancel = false; 1178 private int ticks = 0; 1179 private Rectangle2D r = new Rectangle2D.Double(); 1180 1181 public CalculateDownloadArea() { 1182 super(tr("Calculating Download Area"), 1183 (displayProgress ? null : NullProgressMonitor.INSTANCE), 1184 false); 1185 } 1186 1187 @Override 1188 protected void cancel() { 1189 cancel = true; 1190 } 1191 1192 @Override 1193 protected void finish() { 1194 } 1195 1196 @Override 1197 protected void afterFinish() { 1198 if(cancel) 1199 return; 1200 confirmAndDownloadAreas(a, max_area, progressMonitor); 1201 } 1202 1203 /** 1204 * increase tick count by one, report progress every 100 ticks 1205 */ 1206 private void tick() { 1207 ticks++; 1208 if(ticks % 100 == 0) { 1209 progressMonitor.worked(100); 1210 } 1211 } 1212 1213 /** 1214 * calculate area for single, given way point and return new LatLon if the 1215 * way point has been used to modify the area. 1216 */ 1217 private LatLon calcAreaForWayPoint(WayPoint p, LatLon previous) { 1218 tick(); 1219 LatLon c = p.getCoor(); 1220 if (previous == null || c.greatCircleDistance(previous) > buffer_dist) { 1221 // we add a buffer around the point. 1222 r.setRect(c.lon() - buffer_x, c.lat() - buffer_y, 2 * buffer_x, 2 * buffer_y); 1223 a.add(new Area(r)); 1224 return c; 1225 } 1226 return previous; 1227 } 1228 1229 @Override 1230 protected void realRun() { 1231 progressMonitor.setTicksCount(totalTicks); 1232 /* 1233 * Collect the combined area of all gpx points plus buffer zones around them. We ignore 1234 * points that lie closer to the previous point than the given buffer size because 1235 * otherwise this operation takes ages. 1236 */ 1237 LatLon previous = null; 1238 if (near == NEAR_TRACK || near == NEAR_BOTH) { 1239 for (GpxTrack trk : data.tracks) { 1240 for (GpxTrackSegment segment : trk.getSegments()) { 1241 for (WayPoint p : segment.getWayPoints()) { 1242 if(cancel) 1243 return; 1244 previous = calcAreaForWayPoint(p, previous); 1245 } 1246 } 1247 } 1248 } 1249 if (near == NEAR_WAYPOINTS || near == NEAR_BOTH) { 1250 for (WayPoint p : data.waypoints) { 1251 if(cancel) 1252 return; 1253 previous = calcAreaForWayPoint(p, previous); 1254 } 1255 } 1256 } 1257 } 1258 1259 Main.worker.submit(new CalculateDownloadArea()); 1260 } 1261 1262 1263 /** 1264 * Area "a" contains the hull that we would like to download data for. however we 1265 * can only download rectangles, so the following is an attempt at finding a number of 1266 * rectangles to download. 1267 * 1268 * The idea is simply: Start out with the full bounding box. If it is too large, then 1269 * split it in half and repeat recursively for each half until you arrive at something 1270 * small enough to download. The algorithm is improved by always using the intersection 1271 * between the rectangle and the actual desired area. For example, if you have a track 1272 * that goes like this: +----+ | /| | / | | / | |/ | +----+ then we would first look at 1273 * downloading the whole rectangle (assume it's too big), after that we split it in half 1274 * (upper and lower half), but we donot request the full upper and lower rectangle, only 1275 * the part of the upper/lower rectangle that actually has something in it. 1276 * 1277 * This functions calculates the rectangles, asks the user to continue and downloads 1278 * the areas if applicable. 1279 */ 1280 private void confirmAndDownloadAreas(Area a, double max_area, ProgressMonitor progressMonitor) { 1281 List<Rectangle2D> toDownload = new ArrayList<Rectangle2D>(); 1282 1283 addToDownload(a, a.getBounds(), toDownload, max_area); 1284 1285 if(toDownload.size() == 0) 1286 return; 1287 1288 JPanel msg = new JPanel(new GridBagLayout()); 1289 1290 msg.add(new JLabel( 1291 tr("<html>This action will require {0} individual<br>" 1292 + "download requests. Do you wish<br>to continue?</html>", 1293 toDownload.size())), GBC.eol()); 1294 1295 if (toDownload.size() > 1) { 1296 int ret = JOptionPane.showConfirmDialog( 1297 Main.parent, 1298 msg, 1299 tr("Download from OSM along this track"), 1300 JOptionPane.OK_CANCEL_OPTION, 1301 JOptionPane.PLAIN_MESSAGE 1302 ); 1303 switch(ret) { 1304 case JOptionPane.CANCEL_OPTION: 1305 case JOptionPane.CLOSED_OPTION: 1306 return; 1307 default: 1308 // continue 1309 } 1310 } 1311 final PleaseWaitProgressMonitor monitor = new PleaseWaitProgressMonitor(tr("Download data")); 1312 final Future<?> future = new DownloadOsmTaskList().download(false, toDownload, monitor); 1313 Main.worker.submit( 1314 new Runnable() { 1315 @Override 1316 public void run() { 1317 try { 1318 future.get(); 1319 } catch(Exception e) { 1320 e.printStackTrace(); 1321 return; 1322 } 1323 monitor.close(); 1324 } 1325 } 1326 ); 1327 } 1328 } 1329 1330 1331 public class DownloadWmsAlongTrackAction extends AbstractAction { 1332 public DownloadWmsAlongTrackAction() { 1333 super(tr("Precache imagery tiles along this track"), ImageProvider.get("downloadalongtrack")); 1334 } 1335 1336 public void actionPerformed(ActionEvent e) { 1337 1338 final List<LatLon> points = new ArrayList<LatLon>(); 1339 1340 for (GpxTrack trk : data.tracks) { 1341 for (GpxTrackSegment segment : trk.getSegments()) { 1342 for (WayPoint p : segment.getWayPoints()) { 1343 points.add(p.getCoor()); 1344 } 1345 } 1346 } 1347 for (WayPoint p : data.waypoints) { 1348 points.add(p.getCoor()); 1349 } 1350 1351 1352 final WMSLayer layer = askWMSLayer(); 1353 if (layer != null) { 1354 PleaseWaitRunnable task = new PleaseWaitRunnable(tr("Precaching WMS")) { 1355 1356 private PrecacheTask precacheTask; 1357 1358 @Override 1359 protected void realRun() throws SAXException, IOException, OsmTransferException { 1360 precacheTask = new PrecacheTask(progressMonitor); 1361 layer.downloadAreaToCache(precacheTask, points, 0, 0); 1362 while (!precacheTask.isFinished() && !progressMonitor.isCanceled()) { 1363 synchronized (this) { 1364 try { 1365 wait(200); 1366 } catch (InterruptedException e) { 1367 e.printStackTrace(); 1368 } 1369 } 1370 } 1371 } 1372 1373 @Override 1374 protected void finish() { 1375 } 1376 1377 @Override 1378 protected void cancel() { 1379 precacheTask.cancel(); 1380 } 1381 1382 @Override 1383 public ProgressTaskId canRunInBackground() { 1384 return ProgressTaskIds.PRECACHE_WMS; 1385 } 1386 }; 1387 Main.worker.execute(task); 1388 } 1389 1390 1391 } 1392 1393 protected WMSLayer askWMSLayer() { 1394 List<WMSLayer> targetLayers = Main.map.mapView.getLayersOfType(WMSLayer.class); 1395 1396 if (targetLayers.isEmpty()) { 1397 warnNoImageryLayers(); 1398 return null; 1399 } 1400 1401 JosmComboBox layerList = new JosmComboBox(targetLayers.toArray()); 1402 layerList.setRenderer(new LayerListCellRenderer()); 1403 layerList.setSelectedIndex(0); 1404 1405 JPanel pnl = new JPanel(new GridBagLayout()); 1406 pnl.add(new JLabel(tr("Please select the imagery layer.")), GBC.eol()); 1407 pnl.add(layerList, GBC.eol()); 1408 1409 ExtendedDialog ed = new ExtendedDialog(Main.parent, 1410 tr("Select imagery layer"), 1411 new String[] { tr("Download"), tr("Cancel") }); 1412 ed.setButtonIcons(new String[] { "dialogs/down", "cancel" }); 1413 ed.setContent(pnl); 1414 ed.showDialog(); 1415 if (ed.getValue() != 1) 1416 return null; 1417 1418 return (WMSLayer) layerList.getSelectedItem(); 1419 } 1420 1421 protected void warnNoImageryLayers() { 1422 JOptionPane.showMessageDialog(Main.parent, 1423 tr("There are no imagery layers."), 1424 tr("No imagery layers"), JOptionPane.WARNING_MESSAGE); 1425 } 1426 } 1427 1428 private static void addToDownload(Area a, Rectangle2D r, Collection<Rectangle2D> results, double max_area) { 1429 Area tmp = new Area(r); 1430 // intersect with sought-after area 1431 tmp.intersect(a); 1432 if (tmp.isEmpty()) 1433 return; 1434 Rectangle2D bounds = tmp.getBounds2D(); 1435 if (bounds.getWidth() * bounds.getHeight() > max_area) { 1436 // the rectangle gets too large; split it and make recursive call. 1437 Rectangle2D r1; 1438 Rectangle2D r2; 1439 if (bounds.getWidth() > bounds.getHeight()) { 1440 // rectangles that are wider than high are split into a left and right half, 1441 r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth() / 2, bounds.getHeight()); 1442 r2 = new Rectangle2D.Double(bounds.getX() + bounds.getWidth() / 2, bounds.getY(), 1443 bounds.getWidth() / 2, bounds.getHeight()); 1444 } else { 1445 // others into a top and bottom half. 1446 r1 = new Rectangle2D.Double(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight() / 2); 1447 r2 = new Rectangle2D.Double(bounds.getX(), bounds.getY() + bounds.getHeight() / 2, bounds.getWidth(), 1448 bounds.getHeight() / 2); 1449 } 1450 addToDownload(a, r1, results, max_area); 1451 addToDownload(a, r2, results, max_area); 1452 } else { 1453 results.add(bounds); 1454 } 1455 } 1456 1457 /** 1458 * Makes a new marker layer derived from this GpxLayer containing at least one audio marker 1459 * which the given audio file is associated with. Markers are derived from the following (a) 1460 * explict waypoints in the GPX layer, or (b) named trackpoints in the GPX layer, or (d) 1461 * timestamp on the wav file (e) (in future) voice recognised markers in the sound recording (f) 1462 * a single marker at the beginning of the track 1463 * @param wavFile : the file to be associated with the markers in the new marker layer 1464 * @param markers : keeps track of warning messages to avoid repeated warnings 1465 */ 1466 private void importAudio(File wavFile, MarkerLayer ml, double firstStartTime, Markers markers) { 1467 URL url = null; 1468 try { 1469 url = wavFile.toURI().toURL(); 1470 } catch (MalformedURLException e) { 1471 System.err.println("Unable to convert filename " + wavFile.getAbsolutePath() + " to URL"); 1472 } 1473 Collection<WayPoint> waypoints = new ArrayList<WayPoint>(); 1474 boolean timedMarkersOmitted = false; 1475 boolean untimedMarkersOmitted = false; 1476 double snapDistance = Main.pref.getDouble("marker.audiofromuntimedwaypoints.distance", 1.0e-3); /* 1477 * about 1478 * 25 1479 * m 1480 */ 1481 WayPoint wayPointFromTimeStamp = null; 1482 1483 // determine time of first point in track 1484 double firstTime = -1.0; 1485 if (data.tracks != null && !data.tracks.isEmpty()) { 1486 for (GpxTrack track : data.tracks) { 1487 for (GpxTrackSegment seg : track.getSegments()) { 1488 for (WayPoint w : seg.getWayPoints()) { 1489 firstTime = w.time; 1490 break; 1491 } 1492 if (firstTime >= 0.0) { 1493 break; 1494 } 1495 } 1496 if (firstTime >= 0.0) { 1497 break; 1498 } 1499 } 1500 } 1501 if (firstTime < 0.0) { 1502 JOptionPane.showMessageDialog( 1503 Main.parent, 1504 tr("No GPX track available in layer to associate audio with."), 1505 tr("Error"), 1506 JOptionPane.ERROR_MESSAGE 1507 ); 1508 return; 1509 } 1510 1511 // (a) try explicit timestamped waypoints - unless suppressed 1512 if (Main.pref.getBoolean("marker.audiofromexplicitwaypoints", true) && data.waypoints != null 1513 && !data.waypoints.isEmpty()) { 1514 for (WayPoint w : data.waypoints) { 1515 if (w.time > firstTime) { 1516 waypoints.add(w); 1517 } else if (w.time > 0.0) { 1518 timedMarkersOmitted = true; 1519 } 1520 } 1521 } 1522 1523 // (b) try explicit waypoints without timestamps - unless suppressed 1524 if (Main.pref.getBoolean("marker.audiofromuntimedwaypoints", true) && data.waypoints != null 1525 && !data.waypoints.isEmpty()) { 1526 for (WayPoint w : data.waypoints) { 1527 if (waypoints.contains(w)) { 1528 continue; 1529 } 1530 WayPoint wNear = nearestPointOnTrack(w.getEastNorth(), snapDistance); 1531 if (wNear != null) { 1532 WayPoint wc = new WayPoint(w.getCoor()); 1533 wc.time = wNear.time; 1534 if (w.attr.containsKey("name")) { 1535 wc.attr.put("name", w.getString("name")); 1536 } 1537 waypoints.add(wc); 1538 } else { 1539 untimedMarkersOmitted = true; 1540 } 1541 } 1542 } 1543 1544 // (c) use explicitly named track points, again unless suppressed 1545 if ((Main.pref.getBoolean("marker.audiofromnamedtrackpoints", false)) && data.tracks != null 1546 && !data.tracks.isEmpty()) { 1547 for (GpxTrack track : data.tracks) { 1548 for (GpxTrackSegment seg : track.getSegments()) { 1549 for (WayPoint w : seg.getWayPoints()) { 1550 if (w.attr.containsKey("name") || w.attr.containsKey("desc")) { 1551 waypoints.add(w); 1552 } 1553 } 1554 } 1555 } 1556 } 1557 1558 // (d) use timestamp of file as location on track 1559 if ((Main.pref.getBoolean("marker.audiofromwavtimestamps", false)) && data.tracks != null 1560 && !data.tracks.isEmpty()) { 1561 double lastModified = wavFile.lastModified() / 1000.0; // lastModified is in 1562 // milliseconds 1563 double duration = AudioUtil.getCalibratedDuration(wavFile); 1564 double startTime = lastModified - duration; 1565 startTime = firstStartTime + (startTime - firstStartTime) 1566 / Main.pref.getDouble("audio.calibration", "1.0" /* default, ratio */); 1567 WayPoint w1 = null; 1568 WayPoint w2 = null; 1569 1570 for (GpxTrack track : data.tracks) { 1571 for (GpxTrackSegment seg : track.getSegments()) { 1572 for (WayPoint w : seg.getWayPoints()) { 1573 if (startTime < w.time) { 1574 w2 = w; 1575 break; 1576 } 1577 w1 = w; 1578 } 1579 if (w2 != null) { 1580 break; 1581 } 1582 } 1583 } 1584 1585 if (w1 == null || w2 == null) { 1586 timedMarkersOmitted = true; 1587 } else { 1588 wayPointFromTimeStamp = new WayPoint(w1.getCoor().interpolate(w2.getCoor(), 1589 (startTime - w1.time) / (w2.time - w1.time))); 1590 wayPointFromTimeStamp.time = startTime; 1591 String name = wavFile.getName(); 1592 int dot = name.lastIndexOf("."); 1593 if (dot > 0) { 1594 name = name.substring(0, dot); 1595 } 1596 wayPointFromTimeStamp.attr.put("name", name); 1597 waypoints.add(wayPointFromTimeStamp); 1598 } 1599 } 1600 1601 // (e) analyse audio for spoken markers here, in due course 1602 1603 // (f) simply add a single marker at the start of the track 1604 if ((Main.pref.getBoolean("marker.audiofromstart") || waypoints.isEmpty()) && data.tracks != null 1605 && !data.tracks.isEmpty()) { 1606 boolean gotOne = false; 1607 for (GpxTrack track : data.tracks) { 1608 for (GpxTrackSegment seg : track.getSegments()) { 1609 for (WayPoint w : seg.getWayPoints()) { 1610 WayPoint wStart = new WayPoint(w.getCoor()); 1611 wStart.attr.put("name", "start"); 1612 wStart.time = w.time; 1613 waypoints.add(wStart); 1614 gotOne = true; 1615 break; 1616 } 1617 if (gotOne) { 1618 break; 1619 } 1620 } 1621 if (gotOne) { 1622 break; 1623 } 1624 } 1625 } 1626 1627 /* we must have got at least one waypoint now */ 1628 1629 Collections.sort((ArrayList<WayPoint>) waypoints, new Comparator<WayPoint>() { 1630 @Override 1631 public int compare(WayPoint a, WayPoint b) { 1632 return a.time <= b.time ? -1 : 1; 1633 } 1634 }); 1635 1636 firstTime = -1.0; /* this time of the first waypoint, not first trackpoint */ 1637 for (WayPoint w : waypoints) { 1638 if (firstTime < 0.0) { 1639 firstTime = w.time; 1640 } 1641 double offset = w.time - firstTime; 1642 AudioMarker am = new AudioMarker(w.getCoor(), w, url, ml, w.time, offset); 1643 /* 1644 * timeFromAudio intended for future use to shift markers of this type on 1645 * synchronization 1646 */ 1647 if (w == wayPointFromTimeStamp) { 1648 am.timeFromAudio = true; 1649 } 1650 ml.data.add(am); 1651 } 1652 1653 if (timedMarkersOmitted && !markers.timedMarkersOmitted) { 1654 JOptionPane 1655 .showMessageDialog( 1656 Main.parent, 1657 tr("Some waypoints with timestamps from before the start of the track or after the end were omitted or moved to the start.")); 1658 markers.timedMarkersOmitted = timedMarkersOmitted; 1659 } 1660 if (untimedMarkersOmitted && !markers.untimedMarkersOmitted) { 1661 JOptionPane 1662 .showMessageDialog( 1663 Main.parent, 1664 tr("Some waypoints which were too far from the track to sensibly estimate their time were omitted.")); 1665 markers.untimedMarkersOmitted = untimedMarkersOmitted; 1666 } 1667 } 1668 1669 /** 1670 * Makes a WayPoint at the projection of point P onto the track providing P is less than 1671 * tolerance away from the track 1672 * 1673 * @param P : the point to determine the projection for 1674 * @param tolerance : must be no further than this from the track 1675 * @return the closest point on the track to P, which may be the first or last point if off the 1676 * end of a segment, or may be null if nothing close enough 1677 */ 1678 public WayPoint nearestPointOnTrack(EastNorth P, double tolerance) { 1679 /* 1680 * assume the coordinates of P are xp,yp, and those of a section of track between two 1681 * trackpoints are R=xr,yr and S=xs,ys. Let N be the projected point. 1682 * 1683 * The equation of RS is Ax + By + C = 0 where A = ys - yr B = xr - xs C = - Axr - Byr 1684 * 1685 * Also, note that the distance RS^2 is A^2 + B^2 1686 * 1687 * If RS^2 == 0.0 ignore the degenerate section of track 1688 * 1689 * PN^2 = (Axp + Byp + C)^2 / RS^2 that is the distance from P to the line 1690 * 1691 * so if PN^2 is less than PNmin^2 (initialized to tolerance) we can reject the line; 1692 * otherwise... determine if the projected poijnt lies within the bounds of the line: PR^2 - 1693 * PN^2 <= RS^2 and PS^2 - PN^2 <= RS^2 1694 * 1695 * where PR^2 = (xp - xr)^2 + (yp-yr)^2 and PS^2 = (xp - xs)^2 + (yp-ys)^2 1696 * 1697 * If so, calculate N as xn = xr + (RN/RS) B yn = y1 + (RN/RS) A 1698 * 1699 * where RN = sqrt(PR^2 - PN^2) 1700 */ 1701 1702 double PNminsq = tolerance * tolerance; 1703 EastNorth bestEN = null; 1704 double bestTime = 0.0; 1705 double px = P.east(); 1706 double py = P.north(); 1707 double rx = 0.0, ry = 0.0, sx, sy, x, y; 1708 if (data.tracks == null) 1709 return null; 1710 for (GpxTrack track : data.tracks) { 1711 for (GpxTrackSegment seg : track.getSegments()) { 1712 WayPoint R = null; 1713 for (WayPoint S : seg.getWayPoints()) { 1714 EastNorth c = S.getEastNorth(); 1715 if (R == null) { 1716 R = S; 1717 rx = c.east(); 1718 ry = c.north(); 1719 x = px - rx; 1720 y = py - ry; 1721 double PRsq = x * x + y * y; 1722 if (PRsq < PNminsq) { 1723 PNminsq = PRsq; 1724 bestEN = c; 1725 bestTime = R.time; 1726 } 1727 } else { 1728 sx = c.east(); 1729 sy = c.north(); 1730 double A = sy - ry; 1731 double B = rx - sx; 1732 double C = -A * rx - B * ry; 1733 double RSsq = A * A + B * B; 1734 if (RSsq == 0.0) { 1735 continue; 1736 } 1737 double PNsq = A * px + B * py + C; 1738 PNsq = PNsq * PNsq / RSsq; 1739 if (PNsq < PNminsq) { 1740 x = px - rx; 1741 y = py - ry; 1742 double PRsq = x * x + y * y; 1743 x = px - sx; 1744 y = py - sy; 1745 double PSsq = x * x + y * y; 1746 if (PRsq - PNsq <= RSsq && PSsq - PNsq <= RSsq) { 1747 double RNoverRS = Math.sqrt((PRsq - PNsq) / RSsq); 1748 double nx = rx - RNoverRS * B; 1749 double ny = ry + RNoverRS * A; 1750 bestEN = new EastNorth(nx, ny); 1751 bestTime = R.time + RNoverRS * (S.time - R.time); 1752 PNminsq = PNsq; 1753 } 1754 } 1755 R = S; 1756 rx = sx; 1757 ry = sy; 1758 } 1759 } 1760 if (R != null) { 1761 EastNorth c = R.getEastNorth(); 1762 /* if there is only one point in the seg, it will do this twice, but no matter */ 1763 rx = c.east(); 1764 ry = c.north(); 1765 x = px - rx; 1766 y = py - ry; 1767 double PRsq = x * x + y * y; 1768 if (PRsq < PNminsq) { 1769 PNminsq = PRsq; 1770 bestEN = c; 1771 bestTime = R.time; 1772 } 1773 } 1774 } 1775 } 1776 if (bestEN == null) 1777 return null; 1778 WayPoint best = new WayPoint(Main.getProjection().eastNorth2latlon(bestEN)); 1779 best.time = bestTime; 1780 return best; 1781 } 1782 1783 private class CustomizeDrawing extends AbstractAction implements LayerAction, MultiLayerAction { 1784 List<Layer> layers; 1785 1786 public CustomizeDrawing(List<Layer> l) { 1787 this(); 1788 layers = l; 1789 } 1790 1791 public CustomizeDrawing(Layer l) { 1792 this(); 1793 layers = new LinkedList<Layer>(); 1794 layers.add(l); 1795 } 1796 1797 private CustomizeDrawing() { 1798 super(tr("Customize track drawing"), ImageProvider.get("mapmode/addsegment")); 1799 putValue("help", ht("/Action/GPXLayerCustomizeLineDrawing")); 1800 } 1801 1802 @Override 1803 public boolean supportLayers(List<Layer> layers) { 1804 for(Layer layer: layers) { 1805 if(!(layer instanceof GpxLayer)) 1806 return false; 1807 } 1808 return true; 1809 } 1810 1811 @Override 1812 public Component createMenuComponent() { 1813 return new JMenuItem(this); 1814 } 1815 1816 @Override 1817 public Action getMultiLayerAction(List<Layer> layers) { 1818 return new CustomizeDrawing(layers); 1819 } 1820 1821 @Override 1822 public void actionPerformed(ActionEvent e) { 1823 boolean hasLocal = false, hasNonlocal = false; 1824 for (Layer layer : layers) { 1825 if (layer instanceof GpxLayer) { 1826 if (((GpxLayer) layer).isLocalFile) { 1827 hasLocal = true; 1828 } else { 1829 hasNonlocal = true; 1830 } 1831 } 1832 } 1833 GPXSettingsPanel panel=new GPXSettingsPanel(getName(), hasLocal, hasNonlocal); 1834 JScrollPane scrollpane = new JScrollPane(panel, 1835 JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); 1836 scrollpane.setBorder(BorderFactory.createEmptyBorder( 0, 0, 0, 0 )); 1837 int screenHeight = Toolkit.getDefaultToolkit().getScreenSize().height; 1838 if (screenHeight < 700) { // to fit on screen 800x600 1839 scrollpane.setPreferredSize(new Dimension(panel.getPreferredSize().width, Math.min(panel.getPreferredSize().height,450))); 1840 } 1841 int answer = JOptionPane.showConfirmDialog(Main.parent, scrollpane, 1842 tr("Customize track drawing"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); 1843 if (answer == JOptionPane.CANCEL_OPTION || answer == JOptionPane.CLOSED_OPTION) return; 1844 for(Layer layer : layers) { 1845 // save preferences for all layers 1846 boolean f=false; 1847 if (layer instanceof GpxLayer) { 1848 f=((GpxLayer)layer).isLocalFile; 1849 } 1850 panel.savePreferences(layer.getName(),f); 1851 } 1852 Main.map.repaint(); 1853 } 1854 } 1855 1856 private class MarkersFromNamedPoins extends AbstractAction { 1857 1858 public MarkersFromNamedPoins() { 1859 super(tr("Markers From Named Points"), ImageProvider.get("addmarkers")); 1860 putValue("help", ht("/Action/MarkersFromNamedPoints")); 1861 } 1862 1863 @Override 1864 public void actionPerformed(ActionEvent e) { 1865 GpxData namedTrackPoints = new GpxData(); 1866 for (GpxTrack track : data.tracks) { 1867 for (GpxTrackSegment seg : track.getSegments()) { 1868 for (WayPoint point : seg.getWayPoints()) 1869 if (point.attr.containsKey("name") || point.attr.containsKey("desc")) { 1870 namedTrackPoints.waypoints.add(point); 1871 } 1872 } 1873 } 1874 1875 MarkerLayer ml = new MarkerLayer(namedTrackPoints, tr("Named Trackpoints from {0}", getName()), 1876 getAssociatedFile(), GpxLayer.this); 1877 if (ml.data.size() > 0) { 1878 Main.main.addLayer(ml); 1879 } 1880 1881 } 1882 } 1883 1884 private class ImportAudio extends AbstractAction { 1885 1886 public ImportAudio() { 1887 super(tr("Import Audio"), ImageProvider.get("importaudio")); 1888 putValue("help", ht("/Action/ImportAudio")); 1889 } 1890 1891 private void warnCantImportIntoServerLayer(GpxLayer layer) { 1892 String msg = tr("<html>The data in the GPX layer ''{0}'' has been downloaded from the server.<br>" 1893 + "Because its way points do not include a timestamp we cannot correlate them with audio data.</html>", 1894 layer.getName() 1895 ); 1896 HelpAwareOptionPane.showOptionDialog( 1897 Main.parent, 1898 msg, 1899 tr("Import not possible"), 1900 JOptionPane.WARNING_MESSAGE, 1901 ht("/Action/ImportAudio#CantImportIntoGpxLayerFromServer") 1902 ); 1903 } 1904 1905 @Override 1906 public void actionPerformed(ActionEvent e) { 1907 if (GpxLayer.this.data.fromServer) { 1908 warnCantImportIntoServerLayer(GpxLayer.this); 1909 return; 1910 } 1911 FileFilter filter = new FileFilter() { 1912 @Override 1913 public boolean accept(File f) { 1914 return f.isDirectory() || f.getName().toLowerCase().endsWith(".wav"); 1915 } 1916 1917 @Override 1918 public String getDescription() { 1919 return tr("Wave Audio files (*.wav)"); 1920 } 1921 }; 1922 JFileChooser fc = DiskAccessAction.createAndOpenFileChooser(true, true, null, filter, JFileChooser.FILES_ONLY, "markers.lastaudiodirectory"); 1923 if (fc != null) { 1924 File sel[] = fc.getSelectedFiles(); 1925 // sort files in increasing order of timestamp (this is the end time, but so 1926 // long as they don't overlap, that's fine) 1927 if (sel.length > 1) { 1928 Arrays.sort(sel, new Comparator<File>() { 1929 @Override 1930 public int compare(File a, File b) { 1931 return a.lastModified() <= b.lastModified() ? -1 : 1; 1932 } 1933 }); 1934 } 1935 1936 String names = null; 1937 for (int i = 0; i < sel.length; i++) { 1938 if (names == null) { 1939 names = " ("; 1940 } else { 1941 names += ", "; 1942 } 1943 names += sel[i].getName(); 1944 } 1945 if (names != null) { 1946 names += ")"; 1947 } else { 1948 names = ""; 1949 } 1950 MarkerLayer ml = new MarkerLayer(new GpxData(), tr("Audio markers from {0}", getName()) + names, 1951 getAssociatedFile(), GpxLayer.this); 1952 double firstStartTime = sel[0].lastModified() / 1000.0 /* ms -> seconds */ 1953 - AudioUtil.getCalibratedDuration(sel[0]); 1954 1955 Markers m = new Markers(); 1956 for (int i = 0; i < sel.length; i++) { 1957 importAudio(sel[i], ml, firstStartTime, m); 1958 } 1959 Main.main.addLayer(ml); 1960 Main.map.repaint(); 1961 } 1962 } 1963 } 1964 1965 private class ImportImages extends AbstractAction { 1966 1967 public ImportImages() { 1968 super(tr("Import images"), ImageProvider.get("dialogs/geoimage")); 1969 putValue("help", ht("/Action/ImportImages")); 1970 } 1971 1972 private void warnCantImportIntoServerLayer(GpxLayer layer) { 1973 String msg = tr("<html>The data in the GPX layer ''{0}'' has been downloaded from the server.<br>" 1974 + "Because its way points do not include a timestamp we cannot correlate them with images.</html>", 1975 layer.getName() 1976 ); 1977 HelpAwareOptionPane.showOptionDialog( 1978 Main.parent, 1979 msg, 1980 tr("Import not possible"), 1981 JOptionPane.WARNING_MESSAGE, 1982 ht("/Action/ImportImages#CantImportIntoGpxLayerFromServer") 1983 ); 1984 } 1985 1986 private void addRecursiveFiles(LinkedList<File> files, File[] sel) { 1987 for (File f : sel) { 1988 if (f.isDirectory()) { 1989 addRecursiveFiles(files, f.listFiles()); 1990 } else if (f.getName().toLowerCase().endsWith(".jpg")) { 1991 files.add(f); 1992 } 1993 } 1994 } 1995 1996 @Override 1997 public void actionPerformed(ActionEvent e) { 1998 1999 if (GpxLayer.this.data.fromServer) { 2000 warnCantImportIntoServerLayer(GpxLayer.this); 2001 return; 2002 } 2003 2004 JpgImporter importer = new JpgImporter(GpxLayer.this); 2005 JFileChooser fc = new JFileChooserManager(true, "geoimage.lastdirectory", Main.pref.get("lastDirectory")). 2006 createFileChooser(true, null, importer.filter, JFileChooser.FILES_AND_DIRECTORIES).openFileChooser(); 2007 if (fc != null) { 2008 File[] sel = fc.getSelectedFiles(); 2009 if (sel != null && sel.length > 0) { 2010 LinkedList<File> files = new LinkedList<File>(); 2011 addRecursiveFiles(files, sel); 2012 importer.importDataHandleExceptions(files, NullProgressMonitor.INSTANCE); 2013 } 2014 } 2015 } 2016 } 2017 2018 @Override 2019 public void projectionChanged(Projection oldValue, Projection newValue) { 2020 if (newValue == null) return; 2021 if (data.waypoints != null) { 2022 for (WayPoint wp : data.waypoints){ 2023 wp.invalidateEastNorthCache(); 2024 } 2025 } 2026 if (data.tracks != null){ 2027 for (GpxTrack track: data.tracks) { 2028 for (GpxTrackSegment segment: track.getSegments()) { 2029 for (WayPoint wp: segment.getWayPoints()) { 2030 wp.invalidateEastNorthCache(); 2031 } 2032 } 2033 } 2034 } 2035 if (data.routes != null) { 2036 for (GpxRoute route: data.routes) { 2037 if (route.routePoints == null) { 2038 continue; 2039 } 2040 for (WayPoint wp: route.routePoints) { 2041 wp.invalidateEastNorthCache(); 2042 } 2043 } 2044 } 2045 } 2046 2047 @Override 2048 public boolean isSavable() { 2049 return true; // With GpxExporter 2050 } 2051 2052 @Override 2053 public boolean checkSaveConditions() { 2054 return data != null; 2055 } 2056 2057 @Override 2058 public File createAndOpenSaveFileChooser() { 2059 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save GPX file"), GpxImporter.FILE_FILTER); 2060 } 2061 }