/*
* Geopaparazzi - Digital field mapping on Android based devices
* Copyright (C) 2010 HydroloGIS (www.hydrologis.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.geopaparazzi.library.util;
import java.io.File;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.DialogInterface;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Looper;
import android.os.StatFs;
import android.telephony.TelephonyManager;
import android.text.Editable;
import android.widget.EditText;
import android.widget.Toast;
import eu.geopaparazzi.library.database.GPLog;
/**
* Utilities class.
*
* @author Andrea Antonello (www.hydrologis.com)
*/
public class Utilities {
private static double originShift = 2 * Math.PI * 6378137 / 2.0;
public static String getUniqueDeviceId( Context context ) {
// try to go for the imei
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String id = tm.getDeviceId();
if (id == null) {
// try the android id
id = android.provider.Settings.Secure.getString(context.getContentResolver(),
android.provider.Settings.Secure.ANDROID_ID);
}
return id;
}
/**
* Checks if we are on the UI thread.
*
* @return <code>true</code> if we are on the UI thread.
*/
public static boolean isCurrentThreadTheUiThread() {
return Looper.getMainLooper().getThread() == Thread.currentThread();
}
/**
* Convert decimal degrees to exif format.
*
* @param decimalDegree the angle in decimal format.
* @return the exif format string.
*/
@SuppressWarnings("nls")
public static String degreeDecimal2ExifFormat( double decimalDegree ) {
StringBuilder sb = new StringBuilder();
sb.append((int) decimalDegree);
sb.append("/1,");
decimalDegree = (decimalDegree - (int) decimalDegree) * 60;
sb.append((int) decimalDegree);
sb.append("/1,");
decimalDegree = (decimalDegree - (int) decimalDegree) * 60000;
sb.append((int) decimalDegree);
sb.append("/1000");
if (GPLog.LOG) {
GPLog.addLogEntry("UTILITIES", sb.toString());
}
return sb.toString();
}
/**
* Convert exif format to decimal degree.
*
* @param exifFormat the exif string of the gps position.
* @return the decimal degree.
*/
@SuppressWarnings("nls")
public static double exifFormat2degreeDecimal( String exifFormat ) {
// latitude=44/1,10/1,28110/1000
String[] exifSplit = exifFormat.trim().split(",");
String[] value = exifSplit[0].split("/");
double tmp1 = Double.parseDouble(value[0]);
double tmp2 = Double.parseDouble(value[1]);
double degree = tmp1 / tmp2;
value = exifSplit[1].split("/");
tmp1 = Double.parseDouble(value[0]);
tmp2 = Double.parseDouble(value[1]);
double minutes = tmp1 / tmp2;
value = exifSplit[2].split("/");
tmp1 = Double.parseDouble(value[0]);
tmp2 = Double.parseDouble(value[1]);
double seconds = tmp1 / tmp2;
double result = degree + (minutes / 60.0) + (seconds / 3600.0);
return result;
}
/**
* Calculates the hypothenuse as of the Pythagorean theorem.
*
* @param d1 the length of the first leg.
* @param d2 the length of the second leg.
* @return the length of the hypothenuse.
*/
public static double pythagoras( double d1, double d2 ) {
return Math.sqrt(Math.pow(d1, 2.0) + Math.pow(d2, 2.0));
}
/**
* Tries to adapt a value to the supplied type.
*
* @param value the value to adapt.
* @param adaptee the class to adapt to.
* @return the adapted object or <code>null</code>, if it fails.
*/
public static <T> T adapt( Object value, Class<T> adaptee ) {
if (value instanceof Number) {
Number num = (Number) value;
if (adaptee.isAssignableFrom(Double.class)) {
return adaptee.cast(num.doubleValue());
} else if (adaptee.isAssignableFrom(Float.class)) {
return adaptee.cast(num.floatValue());
} else if (adaptee.isAssignableFrom(Integer.class)) {
return adaptee.cast(num.intValue());
} else if (adaptee.isAssignableFrom(Long.class)) {
return adaptee.cast(num.longValue());
} else if (adaptee.isAssignableFrom(String.class)) {
return adaptee.cast(num.toString());
} else {
throw new IllegalArgumentException();
}
} else if (value instanceof String) {
if (adaptee.isAssignableFrom(Double.class)) {
try {
Double parsed = Double.parseDouble((String) value);
return adaptee.cast(parsed);
} catch (Exception e) {
return null;
}
} else if (adaptee.isAssignableFrom(Float.class)) {
try {
Float parsed = Float.parseFloat((String) value);
return adaptee.cast(parsed);
} catch (Exception e) {
return null;
}
} else if (adaptee.isAssignableFrom(Integer.class)) {
try {
Integer parsed = Integer.parseInt((String) value);
return adaptee.cast(parsed);
} catch (Exception e) {
return null;
}
} else if (adaptee.isAssignableFrom(String.class)) {
return adaptee.cast(value);
} else {
throw new IllegalArgumentException();
}
} else {
throw new IllegalArgumentException("Can't adapt attribute of type: " + value.getClass().getCanonicalName()); //$NON-NLS-1$
}
}
/**
* Execute a message dialog in an {@link AsyncTask}.
*
* @param context the {@link Context} to use.
* @param msg the message to show.
* @param okRunnable optional {@link Runnable} to trigger after ok was pressed.
*/
public static void messageDialog( final Context context, final String msg, final Runnable okRunnable ) {
new AsyncTask<String, Void, String>(){
protected String doInBackground( String... params ) {
return ""; //$NON-NLS-1$
}
protected void onPostExecute( String response ) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(msg).setCancelable(false)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener(){
public void onClick( DialogInterface dialog, int id ) {
if (okRunnable != null) {
new Thread(okRunnable).start();
}
}
});
AlertDialog alertDialog = builder.create();
alertDialog.show();
}
}.execute((String) null);
}
/**
* Execute a message dialog in an {@link AsyncTask}.
*
* @param context the {@link Context} to use.
* @param msg the message to show.
* @param yesRunnable optional {@link Runnable} to trigger after yes was pressed.
* @param noRunnable optional {@link Runnable} to trigger after no was pressed.
*/
public static void yesNoMessageDialog( final Context context, final String msg, final Runnable yesRunnable,
final Runnable noRunnable ) {
new AsyncTask<String, Void, String>(){
protected String doInBackground( String... params ) {
return ""; //$NON-NLS-1$
}
protected void onPostExecute( String response ) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(msg).setCancelable(false)
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener(){
public void onClick( DialogInterface dialog, int id ) {
if (yesRunnable != null) {
new Thread(yesRunnable).start();
}
}
}).setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener(){
public void onClick( DialogInterface dialog, int id ) {
if (noRunnable != null) {
new Thread(noRunnable).start();
}
}
});
AlertDialog alertDialog = builder.create();
alertDialog.show();
}
}.execute((String) null);
}
/**
* Execute a message dialog in an {@link AsyncTask}.
*
* @param context the {@link Context} to use.
* @param msgId the id of the message to show.
* @param okRunnable optional {@link Runnable} to trigger after ok was pressed.
*/
public static void messageDialog( final Context context, final int msgId, final Runnable okRunnable ) {
String msg = context.getString(msgId);
messageDialog(context, msg, okRunnable);
}
/**
* Execute a toast in an {@link AsyncTask}.
*
* @param context the {@link Context} to use.
* @param msg the message to show.
* @param okRunnable optional {@link Runnable} to trigger after ok was pressed.
*/
public static void toast( final Context context, final String msg, final int length ) {
new AsyncTask<String, Void, String>(){
protected String doInBackground( String... params ) {
return ""; //$NON-NLS-1$
}
protected void onPostExecute( String response ) {
Toast.makeText(context, msg, length).show();
}
}.execute((String) null);
}
/**
* Execute a toast in an {@link AsyncTask}.
*
* @param context the {@link Context} to use.
* @param msgId the id of the message to show.
* @param okRunnable optional {@link Runnable} to trigger after ok was pressed.
*/
public static void toast( final Context context, final int msgId, final int length ) {
String msg = context.getString(msgId);
toast(context, msg, length);
}
/**
* Execute a message dialog in an {@link AsyncTask}.
*
* @param context the {@link Context} to use.
* @param title a title for the input dialog.
* @param message a message to show.
* @param defaultText a default text to fill in.
* @param textRunnable optional {@link TextRunnable} to trigger after ok was pressed.
*/
public static void inputMessageDialog( final Context context, final String title, final String message,
final String defaultText, final TextRunnable textRunnable ) {
final EditText input = new EditText(context);
input.setText(defaultText);
Builder builder = new AlertDialog.Builder(context).setTitle(title);
builder.setMessage(message);
builder.setView(input);
builder.setIcon(android.R.drawable.ic_dialog_alert)
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener(){
public void onClick( DialogInterface dialog, int whichButton ) {
}
}).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener(){
public void onClick( DialogInterface dialog, int whichButton ) {
Editable value = input.getText();
String newText = value.toString();
if (newText == null || newText.length() < 1) {
newText = defaultText;
}
if (textRunnable != null) {
textRunnable.setText(newText);
new Thread(textRunnable).start();
}
}
}).setCancelable(false).show();
}
public static void ring( Context context ) {
Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
Ringtone r = RingtoneManager.getRingtone(context, notification);
r.play();
}
/**
* Converts TMS tile coordinates to Google Tile coordinates.
*
* <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p>
*
* @param tx the x tile number.
* @param ty the y tile number.
* @param zoom the current zoom level.
* @return the converted values.
*/
public static int[] tmsTile2GoogleTile( int tx, int ty, int zoom ) {
return new int[]{tx, (int) ((Math.pow(2, zoom) - 1) - ty)};
}
/**
* Converts Google tile coordinates to TMS Tile coordinates.
*
* <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p>
*
* @param tx the x tile number.
* @param ty the y tile number.
* @param zoom the current zoom level.
* @return the converted values.
*/
public static int[] googleTile2TmsTile( int tx, int ty, int zoom ) {
return new int[]{tx, (int) ((Math.pow(2, zoom) - 1) - ty)};
}
/**
* Converts TMS tile coordinates to Microsoft QuadTree.
*
* <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p>
*
* @return the quadtree key.
*/
public static String quadTree( int tx, int ty, int zoom ) {
String quadKey = ""; //$NON-NLS-1$
ty = (int) ((Math.pow(2, zoom) - 1) - ty);
for( int i = zoom; i < 0; i-- ) {
int digit = 0;
int mask = 1 << (i - 1);
if ((tx & mask) != 0) {
digit += 1;
}
if ((ty & mask) != 0) {
digit += 2;
}
quadKey += (digit + ""); //$NON-NLS-1$
}
return quadKey;
}
/**
* <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p>
*
* @param tx
* @param ty
* @param zoom
* @param tileSize
* @return [minx, miny, maxx, maxy]
*/
public static double[] tileLatLonBounds( int tx, int ty, int zoom, int tileSize ) {
double[] bounds = tileBounds(tx, ty, zoom, tileSize);
double[] mins = metersToLatLon(bounds[0], bounds[1]);
double[] maxs = metersToLatLon(bounds[2], bounds[3]);
return new double[]{mins[1], maxs[0], maxs[1], mins[0]};
}
/**
* Returns bounds of the given tile in EPSG:900913 coordinates
*
* <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p>
*
* @param tx
* @param ty
* @param zoom
* @return [minx, miny, maxx, maxy]
*/
public static double[] tileBounds( int tx, int ty, int zoom, int tileSize ) {
double[] min = pixelsToMeters(tx * tileSize, ty * tileSize, zoom, tileSize);
double minx = min[0], miny = min[1];
double[] max = pixelsToMeters((tx + 1) * tileSize, (ty + 1) * tileSize, zoom, tileSize);
double maxx = max[0], maxy = max[1];
return new double[]{minx, miny, maxx, maxy};
}
/**
* Converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84
* Datum
*
* <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p>
*
* @return
*/
public static double[] metersToLatLon( double mx, double my ) {
double lon = (mx / originShift) * 180.0;
double lat = (my / originShift) * 180.0;
lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0);
return new double[]{-lat, lon};
}
/**
* Converts pixel coordinates in given zoom level of pyramid to EPSG:900913
*
* <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p>
*
* @return
*/
public static double[] pixelsToMeters( double px, double py, int zoom, int tileSize ) {
double res = getResolution(zoom, tileSize);
double mx = px * res - originShift;
double my = py * res - originShift;
return new double[]{mx, my};
}
/**
* Resolution (meters/pixel) for given zoom level (measured at Equator)
*
* <p>Code copied from: http://code.google.com/p/gmap-tile-generator/</p>
*
* @return
*/
public static double getResolution( int zoom, int tileSize ) {
// return (2 * Math.PI * 6378137) / (this.tileSize * 2**zoom)
double initialResolution = 2 * Math.PI * 6378137 / tileSize;
return initialResolution / Math.pow(2, zoom);
}
@SuppressWarnings("nls")
public static String makeXmlSafe( String string ) {
if (string == null)
return "";
string = string.replaceAll("&", "&");
return string;
}
/**
* A string formatter.
*
* <p>This method exists, because the method to use is not definitive yet.</p>
* <p>
* Currently the format of the substitutes in the message are:
* <ul>
* <li>%1$d = for numeric</li>
* <li>%1$s = for strings</li>
* </ul>
* The %1, %2, etc refer to the number of the args.
* </p>
*
* @param msg the message.
* @param args the args to substitute.
* @return the formatted string.
*/
public static String format( String msg, String... args ) {
String msgFormat = String.format(msg, (Object[]) args);
return msgFormat;
}
/**
* Convert bytes to hex string.
*
* @param b the bytes array to convert.
* @param size the size of the array to consider.
* @return the hex string.
*/
public static String getHexString( byte[] b, int size ) {
if (size < 1) {
size = b.length;
}
StringBuilder sb = new StringBuilder();
for( int i = size - 1; i >= 0; i-- ) {
if (i >= 0)
sb.append(Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1));
}
return sb.toString();
}
/**
* Get the megabytes available in the filesystem at 'file'.
*
* @param file the filesystem's path.
* @return the available space in mb.
*/
public static float getAvailableMegabytes( File file ) {
StatFs stat = new StatFs(file.getPath());
long bytesAvailable = (long) stat.getBlockSize() * (long) stat.getAvailableBlocks();
return bytesAvailable / (1024.f * 1024.f);
}
/**
* Get the size in megabytes of the filesystem at 'file'.
*
* @param file the filesystem's path.
* @return the size in mb.
*/
public static float getFilesystemMegabytes( File file ) {
StatFs stat = new StatFs(file.getPath());
long bytes = (long) stat.getBlockSize() * (long) stat.getBlockCount();
return bytes / (1024.f * 1024.f);
}
}