001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.plugins.streetside.utils; 003 004import java.awt.Desktop; 005import java.io.IOException; 006import java.net.URISyntaxException; 007import java.net.URL; 008import java.text.ParseException; 009import java.text.SimpleDateFormat; 010import java.util.Calendar; 011import java.util.Locale; 012import java.util.Set; 013 014import javax.swing.SwingUtilities; 015 016import org.apache.commons.imaging.common.RationalNumber; 017import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants; 018 019import org.openstreetmap.josm.data.Bounds; 020import org.openstreetmap.josm.data.coor.LatLon; 021import org.openstreetmap.josm.gui.MainApplication; 022import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage; 023import org.openstreetmap.josm.plugins.streetside.StreetsideLayer; 024import org.openstreetmap.josm.plugins.streetside.StreetsideSequence; 025import org.openstreetmap.josm.tools.I18n; 026 027/** 028 * Set of utilities. 029 * 030 * @author nokutu 031 */ 032public final class StreetsideUtils { 033 034 private static final double MIN_ZOOM_SQUARE_SIDE = 0.002; 035 036 private StreetsideUtils() { 037 // Private constructor to avoid instantiation 038 } 039 040 /** 041 * Open the default browser in the given URL. 042 * 043 * @param url The (not-null) URL that is going to be opened. 044 * @throws IOException when the URL could not be opened 045 */ 046 public static void browse(URL url) throws IOException { 047 if (url == null) { 048 throw new IllegalArgumentException(); 049 } 050 Desktop desktop = Desktop.getDesktop(); 051 if (desktop.isSupported(Desktop.Action.BROWSE)) { 052 try { 053 desktop.browse(url.toURI()); 054 } catch (URISyntaxException e1) { 055 throw new IOException(e1); 056 } 057 } else { 058 Runtime runtime = Runtime.getRuntime(); 059 runtime.exec("xdg-open " + url); 060 } 061 } 062 063 /** 064 * Returns the current date formatted as EXIF timestamp. 065 * As timezone the default timezone of the JVM is used ({@link java.util.TimeZone#getDefault()}). 066 * 067 * @return A {@code String} object containing the current date. 068 */ 069 public static String currentDate() { 070 return new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.UK).format(Calendar.getInstance().getTime()); 071 } 072 073 /** 074 * Returns current time in Epoch format (milliseconds since 1970-01-01T00:00:00+0000) 075 * 076 * @return The current date in Epoch format. 077 */ 078 public static long currentTime() { 079 return Calendar.getInstance().getTimeInMillis(); 080 } 081 082 /** 083 * Parses a string with a given format and returns the Epoch time. 084 * If no timezone information is given, the default timezone of the JVM is used 085 * ({@link java.util.TimeZone#getDefault()}). 086 * 087 * @param date The string containing the date. 088 * @param format The format of the date. 089 * @return The date in Epoch format. 090 * @throws ParseException if the date cannot be parsed with the given format 091 */ 092 public static long getEpoch(String date, String format) throws ParseException { 093 return new SimpleDateFormat(format, Locale.UK).parse(date).getTime(); 094 } 095 096 /** 097 * Calculates the decimal degree-value from a degree value given in 098 * degrees-minutes-seconds-format 099 * 100 * @param degMinSec an array of length 3, the values in there are (in this order) 101 * degrees, minutes and seconds 102 * @param ref the latitude or longitude reference determining if the given value 103 * is: 104 * <ul> 105 * <li>north ( 106 * {@link GpsTagConstants#GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH}) or 107 * south ( 108 * {@link GpsTagConstants#GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH}) of 109 * the equator</li> 110 * <li>east ( 111 * {@link GpsTagConstants#GPS_TAG_GPS_LONGITUDE_REF_VALUE_EAST}) or 112 * west ({@link GpsTagConstants#GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST} 113 * ) of the equator</li> 114 * </ul> 115 * @return the decimal degree-value for the given input, negative when west of 116 * 0-meridian or south of equator, positive otherwise 117 * @throws IllegalArgumentException if {@code degMinSec} doesn't have length 3 or if {@code ref} is 118 * not one of the values mentioned above 119 */ 120 public static double degMinSecToDouble(RationalNumber[] degMinSec, String ref) { 121 if (degMinSec == null || degMinSec.length != 3) { 122 throw new IllegalArgumentException("Array's length must be 3."); 123 } 124 for (int i = 0; i < 3; i++) { 125 if (degMinSec[i] == null) 126 throw new IllegalArgumentException("Null value in array."); 127 } 128 129 switch (ref) { 130 case GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH: 131 case GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH: 132 case GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_EAST: 133 case GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST: 134 break; 135 default: 136 throw new IllegalArgumentException("Invalid ref."); 137 } 138 139 double result = degMinSec[0].doubleValue(); // degrees 140 result += degMinSec[1].doubleValue() / 60; // minutes 141 result += degMinSec[2].doubleValue() / 3600; // seconds 142 143 if (GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH.equals(ref) 144 || GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST.equals(ref)) { 145 result *= -1; 146 } 147 148 result = 360 * ((result + 180) / 360 - Math.floor((result + 180) / 360)) - 180; 149 return result; 150 } 151 152 /** 153 * Joins two images into the same sequence. One of them must be the last image of a sequence, the other one the beginning of a different one. 154 * 155 * @param imgA the first image, into whose sequence the images from the sequence of the second image are merged 156 * @param imgB the second image, whose sequence is merged into the sequence of the first image 157 */ 158 public static synchronized void join(StreetsideAbstractImage imgA, StreetsideAbstractImage imgB) { 159 if (imgA == null || imgB == null) { 160 throw new IllegalArgumentException("Both images must be non-null for joining."); 161 } 162 if (imgA.getSequence() == imgB.getSequence()) { 163 throw new IllegalArgumentException("You can only join images of different sequences."); 164 } 165 if ((imgA.next() != null || imgB.previous() != null) && (imgB.next() != null || imgA.previous() != null)) { 166 throw new IllegalArgumentException("You can only join an image at the end of a sequence with one at the beginning of another sequence."); 167 } 168 if (imgA.next() != null || imgB.previous() != null) { 169 join(imgB, imgA); 170 } else { 171 for (StreetsideAbstractImage img : imgB.getSequence().getImages()) { 172 imgA.getSequence().add(img); 173 } 174 StreetsideLayer.invalidateInstance(); 175 } 176 } 177 178 /** 179 * Zooms to fit all the {@link StreetsideAbstractImage} objects stored in the 180 * database. 181 */ 182 public static void showAllPictures() { 183 showPictures(StreetsideLayer.getInstance().getData().getImages(), false); 184 } 185 186 /** 187 * Zooms to fit all the given {@link StreetsideAbstractImage} objects. 188 * 189 * @param images The images your are zooming to. 190 * @param select Whether the added images must be selected or not. 191 */ 192 public static void showPictures(final Set<StreetsideAbstractImage> images, final boolean select) { 193 if (!SwingUtilities.isEventDispatchThread()) { 194 SwingUtilities.invokeLater(() -> showPictures(images, select)); 195 } else { 196 Bounds zoomBounds; 197 if (images.isEmpty()) { 198 zoomBounds = new Bounds(new LatLon(0, 0)); 199 } else { 200 zoomBounds = new Bounds(images.iterator().next().getMovingLatLon()); 201 for (StreetsideAbstractImage img : images) { 202 zoomBounds.extend(img.getMovingLatLon()); 203 } 204 } 205 206 // The zoom rectangle must have a minimum size. 207 double latExtent = Math.max(zoomBounds.getMaxLat() - zoomBounds.getMinLat(), MIN_ZOOM_SQUARE_SIDE); 208 double lonExtent = Math.max(zoomBounds.getMaxLon() - zoomBounds.getMinLon(), MIN_ZOOM_SQUARE_SIDE); 209 zoomBounds = new Bounds(zoomBounds.getCenter(), latExtent, lonExtent); 210 211 MainApplication.getMap().mapView.zoomTo(zoomBounds); 212 StreetsideLayer.getInstance().getData().setSelectedImage(null); 213 if (select) { 214 StreetsideLayer.getInstance().getData().addMultiSelectedImage(images); 215 } 216 StreetsideLayer.invalidateInstance(); 217 } 218 219 } 220 221 /** 222 * Separates two images belonging to the same sequence. The two images have to be consecutive in the same sequence. 223 * Two new sequences are created and all images up to (and including) either {@code imgA} or {@code imgB} (whichever appears first in the sequence) are put into the first of the two sequences. 224 * All others are put into the second new sequence. 225 * 226 * @param imgA one of the images marking where to split the sequence 227 * @param imgB the other image marking where to split the sequence, needs to be a direct neighbour of {@code imgA} in the sequence. 228 */ 229 public static synchronized void unjoin(StreetsideAbstractImage imgA, StreetsideAbstractImage imgB) { 230 if (imgA == null || imgB == null) { 231 throw new IllegalArgumentException("Both images must be non-null for unjoining."); 232 } 233 if (imgA.getSequence() != imgB.getSequence()) { 234 throw new IllegalArgumentException("You can only unjoin with two images from the same sequence."); 235 } 236 if (imgB.equals(imgA.next()) && imgA.equals(imgB.next())) { 237 throw new IllegalArgumentException("When unjoining with two images these must be consecutive in one sequence."); 238 } 239 240 if (imgA.equals(imgB.next())) { 241 unjoin(imgB, imgA); 242 } else { 243 StreetsideSequence seqA = new StreetsideSequence(); 244 StreetsideSequence seqB = new StreetsideSequence(); 245 boolean insideFirstHalf = true; 246 for (StreetsideAbstractImage img : imgA.getSequence().getImages()) { 247 if (insideFirstHalf) { 248 seqA.add(img); 249 } else { 250 seqB.add(img); 251 } 252 if (img.equals(imgA)) { 253 insideFirstHalf = false; 254 } 255 } 256 StreetsideLayer.invalidateInstance(); 257 } 258 } 259 260 /** 261 * Updates the help text at the bottom of the window. 262 */ 263 public static void updateHelpText() { 264 if (MainApplication.getMap() == null || MainApplication.getMap().statusLine == null) { 265 return; 266 } 267 StringBuilder ret = new StringBuilder(); 268 if (PluginState.isDownloading()) { 269 ret.append(I18n.tr("Downloading Streetside images")); 270 } else if (StreetsideLayer.hasInstance() && !StreetsideLayer.getInstance().getData().getImages().isEmpty()) { 271 ret.append(I18n.tr("Total Streetside images: {0}", StreetsideLayer.getInstance().getToolTipText())); 272 } else if (PluginState.isSubmittingChangeset()) { 273 ret.append(I18n.tr("Submitting Streetside Changeset")); 274 } else { 275 ret.append(I18n.tr("No images found")); 276 } 277 if (StreetsideLayer.hasInstance() && StreetsideLayer.getInstance().mode != null) { 278 ret.append(" — ").append(I18n.tr(StreetsideLayer.getInstance().mode.toString())); 279 } 280 if (PluginState.isUploading()) { 281 ret.append(" — ").append(PluginState.getUploadString()); 282 } 283 MainApplication.getMap().statusLine.setHelpText(ret.toString()); 284 } 285}