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