StreetsideUtils.java
// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.streetside.utils;
import java.awt.Desktop;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.Set;
import javax.swing.SwingUtilities;
import org.apache.commons.imaging.common.RationalNumber;
import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
import org.openstreetmap.josm.plugins.streetside.StreetsideSequence;
import org.openstreetmap.josm.tools.I18n;
/**
* Set of utilities.
*
* @author nokutu
*/
public final class StreetsideUtils {
private static final double MIN_ZOOM_SQUARE_SIDE = 0.002;
private StreetsideUtils() {
// Private constructor to avoid instantiation
}
/**
* Open the default browser in the given URL.
*
* @param url The (not-null) URL that is going to be opened.
* @throws IOException when the URL could not be opened
*/
public static void browse(URL url) throws IOException {
if (url == null) {
throw new IllegalArgumentException();
}
Desktop desktop = Desktop.getDesktop();
if (desktop.isSupported(Desktop.Action.BROWSE)) {
try {
desktop.browse(url.toURI());
} catch (URISyntaxException e1) {
throw new IOException(e1);
}
} else {
Runtime runtime = Runtime.getRuntime();
runtime.exec("xdg-open " + url);
}
}
/**
* Returns the current date formatted as EXIF timestamp.
* As timezone the default timezone of the JVM is used ({@link java.util.TimeZone#getDefault()}).
*
* @return A {@code String} object containing the current date.
*/
public static String currentDate() {
return new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.UK).format(Calendar.getInstance().getTime());
}
/**
* Returns current time in Epoch format (milliseconds since 1970-01-01T00:00:00+0000)
*
* @return The current date in Epoch format.
*/
public static long currentTime() {
return Calendar.getInstance().getTimeInMillis();
}
/**
* Parses a string with a given format and returns the Epoch time.
* If no timezone information is given, the default timezone of the JVM is used
* ({@link java.util.TimeZone#getDefault()}).
*
* @param date The string containing the date.
* @param format The format of the date.
* @return The date in Epoch format.
* @throws ParseException if the date cannot be parsed with the given format
*/
public static long getEpoch(String date, String format) throws ParseException {
return new SimpleDateFormat(format, Locale.UK).parse(date).getTime();
}
/**
* Calculates the decimal degree-value from a degree value given in
* degrees-minutes-seconds-format
*
* @param degMinSec an array of length 3, the values in there are (in this order)
* degrees, minutes and seconds
* @param ref the latitude or longitude reference determining if the given value
* is:
* <ul>
* <li>north (
* {@link GpsTagConstants#GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH}) or
* south (
* {@link GpsTagConstants#GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH}) of
* the equator</li>
* <li>east (
* {@link GpsTagConstants#GPS_TAG_GPS_LONGITUDE_REF_VALUE_EAST}) or
* west ({@link GpsTagConstants#GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST}
* ) of the equator</li>
* </ul>
* @return the decimal degree-value for the given input, negative when west of
* 0-meridian or south of equator, positive otherwise
* @throws IllegalArgumentException if {@code degMinSec} doesn't have length 3 or if {@code ref} is
* not one of the values mentioned above
*/
public static double degMinSecToDouble(RationalNumber[] degMinSec, String ref) {
if (degMinSec == null || degMinSec.length != 3) {
throw new IllegalArgumentException("Array's length must be 3.");
}
for (int i = 0; i < 3; i++) {
if (degMinSec[i] == null)
throw new IllegalArgumentException("Null value in array.");
}
switch (ref) {
case GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH:
case GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH:
case GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_EAST:
case GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST:
break;
default:
throw new IllegalArgumentException("Invalid ref.");
}
double result = degMinSec[0].doubleValue(); // degrees
result += degMinSec[1].doubleValue() / 60; // minutes
result += degMinSec[2].doubleValue() / 3600; // seconds
if (GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH.equals(ref)
|| GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF_VALUE_WEST.equals(ref)) {
result *= -1;
}
result = 360 * ((result + 180) / 360 - Math.floor((result + 180) / 360)) - 180;
return result;
}
/**
* 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.
*
* @param imgA the first image, into whose sequence the images from the sequence of the second image are merged
* @param imgB the second image, whose sequence is merged into the sequence of the first image
*/
public static synchronized void join(StreetsideAbstractImage imgA, StreetsideAbstractImage imgB) {
if (imgA == null || imgB == null) {
throw new IllegalArgumentException("Both images must be non-null for joining.");
}
if (imgA.getSequence() == imgB.getSequence()) {
throw new IllegalArgumentException("You can only join images of different sequences.");
}
if ((imgA.next() != null || imgB.previous() != null) && (imgB.next() != null || imgA.previous() != null)) {
throw new IllegalArgumentException("You can only join an image at the end of a sequence with one at the beginning of another sequence.");
}
if (imgA.next() != null || imgB.previous() != null) {
join(imgB, imgA);
} else {
for (StreetsideAbstractImage img : imgB.getSequence().getImages()) {
imgA.getSequence().add(img);
}
StreetsideLayer.invalidateInstance();
}
}
/**
* Zooms to fit all the {@link StreetsideAbstractImage} objects stored in the
* database.
*/
public static void showAllPictures() {
showPictures(StreetsideLayer.getInstance().getData().getImages(), false);
}
/**
* Zooms to fit all the given {@link StreetsideAbstractImage} objects.
*
* @param images The images your are zooming to.
* @param select Whether the added images must be selected or not.
*/
public static void showPictures(final Set<StreetsideAbstractImage> images, final boolean select) {
if (!SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(() -> showPictures(images, select));
} else {
Bounds zoomBounds;
if (images.isEmpty()) {
zoomBounds = new Bounds(new LatLon(0, 0));
} else {
zoomBounds = new Bounds(images.iterator().next().getMovingLatLon());
for (StreetsideAbstractImage img : images) {
zoomBounds.extend(img.getMovingLatLon());
}
}
// The zoom rectangle must have a minimum size.
double latExtent = Math.max(zoomBounds.getMaxLat() - zoomBounds.getMinLat(), MIN_ZOOM_SQUARE_SIDE);
double lonExtent = Math.max(zoomBounds.getMaxLon() - zoomBounds.getMinLon(), MIN_ZOOM_SQUARE_SIDE);
zoomBounds = new Bounds(zoomBounds.getCenter(), latExtent, lonExtent);
MainApplication.getMap().mapView.zoomTo(zoomBounds);
StreetsideLayer.getInstance().getData().setSelectedImage(null);
if (select) {
StreetsideLayer.getInstance().getData().addMultiSelectedImage(images);
}
StreetsideLayer.invalidateInstance();
}
}
/**
* Separates two images belonging to the same sequence. The two images have to be consecutive in the same sequence.
* 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.
* All others are put into the second new sequence.
*
* @param imgA one of the images marking where to split the sequence
* @param imgB the other image marking where to split the sequence, needs to be a direct neighbour of {@code imgA} in the sequence.
*/
public static synchronized void unjoin(StreetsideAbstractImage imgA, StreetsideAbstractImage imgB) {
if (imgA == null || imgB == null) {
throw new IllegalArgumentException("Both images must be non-null for unjoining.");
}
if (imgA.getSequence() != imgB.getSequence()) {
throw new IllegalArgumentException("You can only unjoin with two images from the same sequence.");
}
if (imgB.equals(imgA.next()) && imgA.equals(imgB.next())) {
throw new IllegalArgumentException("When unjoining with two images these must be consecutive in one sequence.");
}
if (imgA.equals(imgB.next())) {
unjoin(imgB, imgA);
} else {
StreetsideSequence seqA = new StreetsideSequence();
StreetsideSequence seqB = new StreetsideSequence();
boolean insideFirstHalf = true;
for (StreetsideAbstractImage img : imgA.getSequence().getImages()) {
if (insideFirstHalf) {
seqA.add(img);
} else {
seqB.add(img);
}
if (img.equals(imgA)) {
insideFirstHalf = false;
}
}
StreetsideLayer.invalidateInstance();
}
}
/**
* Updates the help text at the bottom of the window.
*/
public static void updateHelpText() {
if (MainApplication.getMap() == null || MainApplication.getMap().statusLine == null) {
return;
}
StringBuilder ret = new StringBuilder();
if (PluginState.isDownloading()) {
ret.append(I18n.tr("Downloading Streetside images"));
} else if (StreetsideLayer.hasInstance() && !StreetsideLayer.getInstance().getData().getImages().isEmpty()) {
ret.append(I18n.tr("Total Streetside images: {0}", StreetsideLayer.getInstance().getToolTipText()));
} else if (PluginState.isSubmittingChangeset()) {
ret.append(I18n.tr("Submitting Streetside Changeset"));
} else {
ret.append(I18n.tr("No images found"));
}
if (StreetsideLayer.hasInstance() && StreetsideLayer.getInstance().mode != null) {
ret.append(" — ").append(I18n.tr(StreetsideLayer.getInstance().mode.toString()));
}
if (PluginState.isUploading()) {
ret.append(" — ").append(PluginState.getUploadString());
}
MainApplication.getMap().statusLine.setHelpText(ret.toString());
}
}