/*
* Licensed to GraphHopper GmbH under one or more contributor
* license agreements. See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*
* GraphHopper GmbH licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.graphhopper.util;
import com.carrotsearch.hppc.IntArrayList;
import com.carrotsearch.hppc.IntIndexedContainer;
import com.graphhopper.util.shapes.BBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.charset.Charset;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;
/**
* Several utility classes which are compatible with Java6 on Android.
* <p>
*
* @author Peter Karich
* @see Helper7 for none-Android compatible methods.
*/
public class Helper {
public static final DistanceCalc DIST_EARTH = new DistanceCalcEarth();
public static final DistanceCalc3D DIST_3D = new DistanceCalc3D();
public static final DistancePlaneProjection DIST_PLANE = new DistancePlaneProjection();
public static final AngleCalc ANGLE_CALC = new AngleCalc();
public static final Charset UTF_CS = Charset.forName("UTF-8");
public static final TimeZone UTC = TimeZone.getTimeZone("UTC");
public static final long MB = 1L << 20;
private static final Logger LOGGER = LoggerFactory.getLogger(Helper.class);
// +- 180 and +-90 => let use use 400
private static final float DEGREE_FACTOR = Integer.MAX_VALUE / 400f;
// milli meter is a bit extreme but we have integers
private static final float ELE_FACTOR = 1000f;
private Helper() {
}
public static ArrayList<Integer> intListToArrayList(IntIndexedContainer from) {
int len = from.size();
ArrayList<Integer> list = new ArrayList<Integer>(len);
for (int i = 0; i < len; i++) {
list.add(from.get(i));
}
return list;
}
public static Locale getLocale(String param) {
int pointIndex = param.indexOf('.');
if (pointIndex > 0)
param = param.substring(0, pointIndex);
param = param.replace("-", "_");
int index = param.indexOf("_");
if (index < 0) {
return new Locale(param);
}
return new Locale(param.substring(0, index), param.substring(index + 1));
}
static String packageToPath(Package pkg) {
return pkg.getName().replaceAll("\\.", File.separator);
}
public static int countBitValue(int maxTurnCosts) {
if (maxTurnCosts < 0)
throw new IllegalArgumentException("maxTurnCosts cannot be negative " + maxTurnCosts);
int counter = 0;
while (maxTurnCosts > 0) {
maxTurnCosts >>= 1;
counter++;
}
return counter++;
}
public static void loadProperties(Map<String, String> map, Reader tmpReader) throws IOException {
BufferedReader reader = new BufferedReader(tmpReader);
String line;
try {
while ((line = reader.readLine()) != null) {
if (line.startsWith("//") || line.startsWith("#")) {
continue;
}
if (Helper.isEmpty(line)) {
continue;
}
int index = line.indexOf("=");
if (index < 0) {
LOGGER.warn("Skipping configuration at line:" + line);
continue;
}
String field = line.substring(0, index);
String value = line.substring(index + 1);
map.put(field.trim(), value.trim());
}
} finally {
reader.close();
}
}
public static void saveProperties(Map<String, String> map, Writer tmpWriter) throws IOException {
BufferedWriter writer = new BufferedWriter(tmpWriter);
try {
for (Entry<String, String> e : map.entrySet()) {
writer.append(e.getKey());
writer.append('=');
writer.append(e.getValue());
writer.append('\n');
}
} finally {
writer.close();
}
}
public static List<String> readFile(String file) throws IOException {
return readFile(new InputStreamReader(new FileInputStream(file), UTF_CS));
}
public static List<String> readFile(Reader simpleReader) throws IOException {
BufferedReader reader = new BufferedReader(simpleReader);
try {
List<String> res = new ArrayList<String>();
String line;
while ((line = reader.readLine()) != null) {
res.add(line);
}
return res;
} finally {
reader.close();
}
}
public static String isToString(InputStream inputStream) throws IOException {
int size = 1024 * 8;
String encoding = "UTF-8";
InputStream in = new BufferedInputStream(inputStream, size);
try {
byte[] buffer = new byte[size];
ByteArrayOutputStream output = new ByteArrayOutputStream();
int numRead;
while ((numRead = in.read(buffer)) != -1) {
output.write(buffer, 0, numRead);
}
return output.toString(encoding);
} finally {
in.close();
}
}
public static int idealIntArraySize(int need) {
return idealByteArraySize(need * 4) / 4;
}
public static int idealByteArraySize(int need) {
for (int i = 4; i < 32; i++) {
if (need <= (1 << i) - 12) {
return (1 << i) - 12;
}
}
return need;
}
public static boolean removeDir(File file) {
if (!file.exists()) {
return true;
}
if (file.isDirectory()) {
for (File f : file.listFiles()) {
removeDir(f);
}
}
return file.delete();
}
public static long getTotalMB() {
return Runtime.getRuntime().totalMemory() / MB;
}
public static long getUsedMB() {
return (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / MB;
}
public static String getMemInfo() {
return "totalMB:" + getTotalMB() + ", usedMB:" + getUsedMB();
}
public static int getSizeOfObjectRef(int factor) {
// pointer to class, flags, lock
return factor * (4 + 4 + 4);
}
public static int getSizeOfLongArray(int length, int factor) {
// pointer to class, flags, lock, size
return factor * (4 + 4 + 4 + 4) + 8 * length;
}
public static int getSizeOfObjectArray(int length, int factor) {
// improvements: add 4byte to make a multiple of 8 in some cases plus compressed oop
return factor * (4 + 4 + 4 + 4) + 4 * length;
}
public static void close(Closeable cl) {
try {
if (cl != null)
cl.close();
} catch (IOException ex) {
throw new RuntimeException("Couldn't close resource", ex);
}
}
public static boolean isEmpty(String str) {
return str == null || str.trim().length() == 0;
}
/**
* Determines if the specified ByteBuffer is one which maps to a file!
*/
public static boolean isFileMapped(ByteBuffer bb) {
if (bb instanceof MappedByteBuffer) {
try {
((MappedByteBuffer) bb).isLoaded();
return true;
} catch (UnsupportedOperationException ex) {
}
}
return false;
}
public static int calcIndexSize(BBox graphBounds) {
if (!graphBounds.isValid())
throw new IllegalArgumentException("Bounding box is not valid to calculate index size: " + graphBounds);
double dist = DIST_EARTH.calcDist(graphBounds.maxLat, graphBounds.minLon,
graphBounds.minLat, graphBounds.maxLon);
// convert to km and maximum is 50000km => 1GB
dist = Math.min(dist / 1000, 50000);
return Math.max(2000, (int) (dist * dist));
}
public static String pruneFileEnd(String file) {
int index = file.lastIndexOf(".");
if (index < 0)
return file;
return file.substring(0, index);
}
public static List<Double> createDoubleList(double[] values) {
List<Double> list = new ArrayList<>();
for (double v : values) {
list.add(v);
}
return list;
}
public static IntArrayList createTList(int... list) {
return IntArrayList.from(list);
}
public static PointList createPointList(double... list) {
if (list.length % 2 != 0)
throw new IllegalArgumentException("list should consist of lat,lon pairs!");
int max = list.length / 2;
PointList res = new PointList(max, false);
for (int i = 0; i < max; i++) {
res.add(list[2 * i], list[2 * i + 1], Double.NaN);
}
return res;
}
public static PointList createPointList3D(double... list) {
if (list.length % 3 != 0)
throw new IllegalArgumentException("list should consist of lat,lon,ele tuples!");
int max = list.length / 3;
PointList res = new PointList(max, true);
for (int i = 0; i < max; i++) {
res.add(list[3 * i], list[3 * i + 1], list[3 * i + 2]);
}
return res;
}
/**
* Converts into an integer to be compatible with the still limited DataAccess class (accepts
* only integer values). But this conversion also reduces memory consumption where the precision
* loss is acceptable. As +- 180° and +-90° are assumed as maximum values.
* <p>
*
* @return the integer of the specified degree
*/
public static final int degreeToInt(double deg) {
if (deg >= Double.MAX_VALUE)
return Integer.MAX_VALUE;
if (deg <= -Double.MAX_VALUE)
return -Integer.MAX_VALUE;
return (int) (deg * DEGREE_FACTOR);
}
/**
* Converts back the integer value.
* <p>
*
* @return the degree value of the specified integer
*/
public static final double intToDegree(int storedInt) {
if (storedInt == Integer.MAX_VALUE)
return Double.MAX_VALUE;
if (storedInt == -Integer.MAX_VALUE)
return -Double.MAX_VALUE;
return (double) storedInt / DEGREE_FACTOR;
}
/**
* Converts elevation value (in meters) into integer for storage.
*/
public static final int eleToInt(double ele) {
if (ele >= Integer.MAX_VALUE)
return Integer.MAX_VALUE;
return (int) (ele * ELE_FACTOR);
}
/**
* Converts the integer value retrieved from storage into elevation (in meters). Do not expect
* more precision than meters although it currently is!
*/
public static final double intToEle(int integEle) {
if (integEle == Integer.MAX_VALUE)
return Double.MAX_VALUE;
return integEle / ELE_FACTOR;
}
public static void cleanMappedByteBuffer(final ByteBuffer buffer) {
// TODO avoid reflection on every call
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws Exception {
if (Constants.JAVA_VERSION.equals("9-ea")) {
// >=JDK9 class sun.misc.Unsafe { void invokeCleaner(ByteBuffer buf) }
final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
// we do not need to check for a specific class, we can call the Unsafe method with any buffer class
MethodHandle unmapper = MethodHandles.lookup().findVirtual(unsafeClass, "invokeCleaner",
MethodType.methodType(void.class, ByteBuffer.class));
// fetch the unsafe instance and bind it to the virtual MethodHandle
final Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
final Object theUnsafe = f.get(null);
try {
unmapper.bindTo(theUnsafe).invokeExact(buffer);
return null;
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
if (buffer.getClass().getSimpleName().equals("MappedByteBufferAdapter")) {
if (!Constants.ANDROID)
throw new RuntimeException("MappedByteBufferAdapter only supported for Android at the moment");
// For Android 4.1 call ((MappedByteBufferAdapter)buffer).free() see #914
Class<?> directByteBufferClass = Class.forName("java.nio.MappedByteBufferAdapter");
callBufferFree(buffer, directByteBufferClass);
} else {
// <=JDK8 class DirectByteBuffer { sun.misc.Cleaner cleaner(Buffer buf) }
// then call sun.misc.Cleaner.clean
final Class<?> directByteBufferClass = Class.forName("java.nio.DirectByteBuffer");
try {
final Method dbbCleanerMethod = directByteBufferClass.getMethod("cleaner");
dbbCleanerMethod.setAccessible(true);
// call: cleaner = ((DirectByteBuffer)buffer).cleaner()
final Object cleaner = dbbCleanerMethod.invoke(buffer);
if (cleaner != null) {
final Class<?> cleanerMethodReturnType = dbbCleanerMethod.getReturnType();
final Method cleanMethod = cleanerMethodReturnType.getDeclaredMethod("clean");
cleanMethod.setAccessible(true);
// call: ((sun.misc.Cleaner)cleaner).clean()
cleanMethod.invoke(cleaner);
}
} catch (NoSuchMethodException ex2) {
if (Constants.ANDROID)
// For Android 5.1.1 call ((DirectByteBuffer)buffer).free() see #933
callBufferFree(buffer, directByteBufferClass);
else
// ignore if method cleaner or clean is not available
LOGGER.warn("NoSuchMethodException | " + Constants.JAVA_VERSION, ex2);
}
}
return null;
}
});
} catch (PrivilegedActionException e) {
throw new RuntimeException("Unable to unmap the mapped buffer", e);
}
}
private static void callBufferFree(ByteBuffer buffer, Class<?> directByteBufferClass)
throws InvocationTargetException, IllegalAccessException {
try {
final Method dbbFreeMethod = directByteBufferClass.getMethod("free");
dbbFreeMethod.setAccessible(true);
dbbFreeMethod.invoke(buffer);
} catch (NoSuchMethodException ex2) {
LOGGER.warn("NoSuchMethodException | " + Constants.JAVA_VERSION, ex2);
}
}
/**
* Trying to force the release of the mapped ByteBuffer. See
* http://stackoverflow.com/q/2972986/194609 and use only if you know what you are doing.
*/
public static void cleanHack() {
System.gc();
}
public static String nf(long no) {
// I like french localization the most: 123654 will be 123 654 instead
// of comma vs. point confusion for English/German people.
// NumberFormat is not thread safe => but getInstance looks like it's cached
return NumberFormat.getInstance(Locale.FRANCE).format(no);
}
public static String firstBig(String sayText) {
if (sayText == null || sayText.length() <= 0) {
return sayText;
}
return Character.toUpperCase(sayText.charAt(0)) + sayText.substring(1);
}
/**
* This methods returns the value or min if too small or max if too big.
*/
public static final double keepIn(double value, double min, double max) {
return Math.max(min, Math.min(value, max));
}
/**
* Round the value to the specified exponent
*/
public static double round(double value, int exponent) {
double factor = Math.pow(10, exponent);
return Math.round(value * factor) / factor;
}
public static final double round6(double value) {
return Math.round(value * 1e6) / 1e6;
}
public static final double round4(double value) {
return Math.round(value * 1e4) / 1e4;
}
public static final double round2(double value) {
return Math.round(value * 100) / 100d;
}
/**
* This creates a date formatter for yyyy-MM-dd'T'HH:mm:ss'Z' which is has to be identical to
* buildDate used in pom.xml
*/
public static DateFormat createFormatter() {
return createFormatter("yyyy-MM-dd'T'HH:mm:ss'Z'");
}
/**
* Creates a SimpleDateFormat with the UK locale.
*/
public static DateFormat createFormatter(String str) {
DateFormat df = new SimpleDateFormat(str, Locale.UK);
df.setTimeZone(UTC);
return df;
}
/**
* This method handles the specified (potentially negative) int as unsigned bit representation
* and returns the positive converted long.
*/
public static final long toUnsignedLong(int x) {
return ((long) x) & 0xFFFFffffL;
}
/**
* Converts the specified long back into a signed int (reverse method for toUnsignedLong)
*/
public static final int toSignedInt(long x) {
return (int) x;
}
public static final String camelCaseToUnderScore(String key) {
if (key.isEmpty())
return key;
StringBuilder sb = new StringBuilder(key.length());
for (int i = 0; i < key.length(); i++) {
char c = key.charAt(i);
if (Character.isUpperCase(c))
sb.append("_").append(Character.toLowerCase(c));
else
sb.append(c);
}
return sb.toString();
}
public static final String underScoreToCamelCase(String key) {
if (key.isEmpty())
return key;
StringBuilder sb = new StringBuilder(key.length());
for (int i = 0; i < key.length(); i++) {
char c = key.charAt(i);
if (c == '_') {
i++;
if (i < key.length())
sb.append(Character.toUpperCase(key.charAt(i)));
else
sb.append(c);
} else
sb.append(c);
}
return sb.toString();
}
}