| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed 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 androidx.camera.core; |
| |
| import android.location.Location; |
| import android.util.Log; |
| |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RestrictTo; |
| import androidx.annotation.RestrictTo.Scope; |
| import androidx.exifinterface.media.ExifInterface; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.text.ParseException; |
| import java.text.SimpleDateFormat; |
| import java.util.Date; |
| import java.util.Locale; |
| |
| /** |
| * Utility class for modifying metadata on JPEG files. |
| * |
| * <p>Call {@link #save()} to persist changes to disc. |
| * |
| * @hide |
| */ |
| @RestrictTo(Scope.LIBRARY_GROUP) |
| public final class Exif { |
| |
| /** Timestamp value indicating a timestamp value that is either not set or not valid */ |
| public static final long INVALID_TIMESTAMP = -1; |
| |
| private static final String TAG = Exif.class.getSimpleName(); |
| |
| private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = |
| new ThreadLocal<SimpleDateFormat>() { |
| @Override |
| public SimpleDateFormat initialValue() { |
| return new SimpleDateFormat("yyyy:MM:dd", Locale.US); |
| } |
| }; |
| private static final ThreadLocal<SimpleDateFormat> TIME_FORMAT = |
| new ThreadLocal<SimpleDateFormat>() { |
| @Override |
| public SimpleDateFormat initialValue() { |
| return new SimpleDateFormat("HH:mm:ss", Locale.US); |
| } |
| }; |
| private static final ThreadLocal<SimpleDateFormat> DATETIME_FORMAT = |
| new ThreadLocal<SimpleDateFormat>() { |
| @Override |
| public SimpleDateFormat initialValue() { |
| return new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US); |
| } |
| }; |
| |
| private static final String KILOMETERS_PER_HOUR = "K"; |
| private static final String MILES_PER_HOUR = "M"; |
| private static final String KNOTS = "N"; |
| |
| private final ExifInterface mExifInterface; |
| |
| // When true, avoid saving any time. This is a privacy issue. |
| private boolean mRemoveTimestamp = false; |
| |
| private Exif(ExifInterface exifInterface) { |
| mExifInterface = exifInterface; |
| } |
| |
| /** |
| * Returns an Exif from the exif data contained in the file. |
| * |
| * @param file the file to read exif data from |
| */ |
| public static Exif createFromFile(File file) throws IOException { |
| return createFromFileString(file.toString()); |
| } |
| |
| /** |
| * Returns an Exif from the exif data contained in the file at the filePath |
| * |
| * @param filePath the path to the file to read exif data from |
| */ |
| public static Exif createFromFileString(String filePath) throws IOException { |
| return new Exif(new ExifInterface(filePath)); |
| } |
| |
| /** |
| * Returns an Exif from the exif data contain in the input stream. |
| * @param is the input stream to read exif data from |
| */ |
| public static Exif createFromInputStream(InputStream is) throws IOException { |
| return new Exif(new ExifInterface(is)); |
| } |
| |
| private static String convertToExifDateTime(long timestamp) { |
| return DATETIME_FORMAT.get().format(new Date(timestamp)); |
| } |
| |
| private static Date convertFromExifDateTime(String dateTime) throws ParseException { |
| return DATETIME_FORMAT.get().parse(dateTime); |
| } |
| |
| private static Date convertFromExifDate(String date) throws ParseException { |
| return DATE_FORMAT.get().parse(date); |
| } |
| |
| private static Date convertFromExifTime(String time) throws ParseException { |
| return TIME_FORMAT.get().parse(time); |
| } |
| |
| /** Persists changes to disc. */ |
| public void save() throws IOException { |
| if (!mRemoveTimestamp) { |
| attachLastModifiedTimestamp(); |
| } |
| mExifInterface.saveAttributes(); |
| } |
| |
| @Override |
| public String toString() { |
| return String.format( |
| Locale.ENGLISH, |
| "Exif{width=%s, height=%s, rotation=%d, " |
| + "isFlippedVertically=%s, isFlippedHorizontally=%s, location=%s, " |
| + "timestamp=%s, description=%s}", |
| getWidth(), |
| getHeight(), |
| getRotation(), |
| isFlippedVertically(), |
| isFlippedHorizontally(), |
| getLocation(), |
| getTimestamp(), |
| getDescription()); |
| } |
| |
| private int getOrientation() { |
| return mExifInterface.getAttributeInt( |
| ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); |
| } |
| |
| /** Returns the width of the photo in pixels. */ |
| public int getWidth() { |
| return mExifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0); |
| } |
| |
| /** Returns the height of the photo in pixels. */ |
| public int getHeight() { |
| return mExifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0); |
| } |
| |
| @Nullable |
| public String getDescription() { |
| return mExifInterface.getAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION); |
| } |
| |
| /** Sets the description for the exif. */ |
| public void setDescription(@Nullable String description) { |
| mExifInterface.setAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION, description); |
| } |
| |
| /** @return The degree of rotation (eg. 0, 90, 180, 270). */ |
| public int getRotation() { |
| switch (getOrientation()) { |
| case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: |
| return 0; |
| case ExifInterface.ORIENTATION_ROTATE_180: |
| return 180; |
| case ExifInterface.ORIENTATION_FLIP_VERTICAL: |
| return 180; |
| case ExifInterface.ORIENTATION_TRANSPOSE: |
| return 270; |
| case ExifInterface.ORIENTATION_ROTATE_90: |
| return 90; |
| case ExifInterface.ORIENTATION_TRANSVERSE: |
| return 90; |
| case ExifInterface.ORIENTATION_ROTATE_270: |
| return 270; |
| case ExifInterface.ORIENTATION_NORMAL: |
| // Fall-through |
| case ExifInterface.ORIENTATION_UNDEFINED: |
| // Fall-through |
| default: |
| return 0; |
| } |
| } |
| |
| /** @return True if the image is flipped vertically after rotation. */ |
| public boolean isFlippedVertically() { |
| switch (getOrientation()) { |
| case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: |
| return false; |
| case ExifInterface.ORIENTATION_ROTATE_180: |
| return false; |
| case ExifInterface.ORIENTATION_FLIP_VERTICAL: |
| return true; |
| case ExifInterface.ORIENTATION_TRANSPOSE: |
| return true; |
| case ExifInterface.ORIENTATION_ROTATE_90: |
| return false; |
| case ExifInterface.ORIENTATION_TRANSVERSE: |
| return true; |
| case ExifInterface.ORIENTATION_ROTATE_270: |
| return false; |
| case ExifInterface.ORIENTATION_NORMAL: |
| // Fall-through |
| case ExifInterface.ORIENTATION_UNDEFINED: |
| // Fall-through |
| default: |
| return false; |
| } |
| } |
| |
| /** @return True if the image is flipped horizontally after rotation. */ |
| public boolean isFlippedHorizontally() { |
| switch (getOrientation()) { |
| case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: |
| return true; |
| case ExifInterface.ORIENTATION_ROTATE_180: |
| return false; |
| case ExifInterface.ORIENTATION_FLIP_VERTICAL: |
| return false; |
| case ExifInterface.ORIENTATION_TRANSPOSE: |
| return false; |
| case ExifInterface.ORIENTATION_ROTATE_90: |
| return false; |
| case ExifInterface.ORIENTATION_TRANSVERSE: |
| return false; |
| case ExifInterface.ORIENTATION_ROTATE_270: |
| return false; |
| case ExifInterface.ORIENTATION_NORMAL: |
| // Fall-through |
| case ExifInterface.ORIENTATION_UNDEFINED: |
| // Fall-through |
| default: |
| return false; |
| } |
| } |
| |
| private void attachLastModifiedTimestamp() { |
| long now = System.currentTimeMillis(); |
| String datetime = convertToExifDateTime(now); |
| |
| mExifInterface.setAttribute(ExifInterface.TAG_DATETIME, datetime); |
| |
| try { |
| String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime()); |
| mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, subsec); |
| } catch (ParseException e) { |
| } |
| } |
| |
| /** |
| * @return The timestamp (in millis) that this picture was modified, or {@link |
| * #INVALID_TIMESTAMP} if no time is available. |
| */ |
| public long getLastModifiedTimestamp() { |
| long timestamp = parseTimestamp(mExifInterface.getAttribute(ExifInterface.TAG_DATETIME)); |
| if (timestamp == INVALID_TIMESTAMP) { |
| return INVALID_TIMESTAMP; |
| } |
| |
| String subSecs = mExifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME); |
| if (subSecs != null) { |
| try { |
| long sub = Long.parseLong(subSecs); |
| while (sub > 1000) { |
| sub /= 10; |
| } |
| timestamp += sub; |
| } catch (NumberFormatException e) { |
| // Ignored |
| } |
| } |
| |
| return timestamp; |
| } |
| |
| /** |
| * @return The timestamp (in millis) that this picture was taken, or {@link #INVALID_TIMESTAMP} |
| * if no time is available. |
| */ |
| public long getTimestamp() { |
| long timestamp = |
| parseTimestamp(mExifInterface.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL)); |
| if (timestamp == INVALID_TIMESTAMP) { |
| return INVALID_TIMESTAMP; |
| } |
| |
| String subSecs = mExifInterface.getAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL); |
| if (subSecs != null) { |
| try { |
| long sub = Long.parseLong(subSecs); |
| while (sub > 1000) { |
| sub /= 10; |
| } |
| timestamp += sub; |
| } catch (NumberFormatException e) { |
| // Ignored |
| } |
| } |
| |
| return timestamp; |
| } |
| |
| /** @return The location this picture was taken, or null if no location is available. */ |
| @Nullable |
| public Location getLocation() { |
| String provider = mExifInterface.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD); |
| double[] latlng = mExifInterface.getLatLong(); |
| double altitude = mExifInterface.getAltitude(0); |
| double speed = mExifInterface.getAttributeDouble(ExifInterface.TAG_GPS_SPEED, 0); |
| String speedRef = mExifInterface.getAttribute(ExifInterface.TAG_GPS_SPEED_REF); |
| speedRef = speedRef == null ? KILOMETERS_PER_HOUR : speedRef; // Ensure speedRef is not null |
| long timestamp = |
| parseTimestamp( |
| mExifInterface.getAttribute(ExifInterface.TAG_GPS_DATESTAMP), |
| mExifInterface.getAttribute(ExifInterface.TAG_GPS_TIMESTAMP)); |
| if (latlng == null) { |
| return null; |
| } |
| if (provider == null) { |
| provider = TAG; |
| } |
| |
| Location location = new Location(provider); |
| location.setLatitude(latlng[0]); |
| location.setLongitude(latlng[1]); |
| if (altitude != 0) { |
| location.setAltitude(altitude); |
| } |
| if (speed != 0) { |
| switch (speedRef) { |
| case MILES_PER_HOUR: |
| speed = Speed.fromMilesPerHour(speed).toMetersPerSecond(); |
| break; |
| case KNOTS: |
| speed = Speed.fromKnots(speed).toMetersPerSecond(); |
| break; |
| case KILOMETERS_PER_HOUR: |
| // fall through |
| default: |
| speed = Speed.fromKilometersPerHour(speed).toMetersPerSecond(); |
| break; |
| } |
| |
| location.setSpeed((float) speed); |
| } |
| if (timestamp != INVALID_TIMESTAMP) { |
| location.setTime(timestamp); |
| } |
| return location; |
| } |
| |
| /** |
| * Rotates the image by the given degrees. Can only rotate by right angles (eg. 90, 180, -90). |
| * Other increments will set the orientation to undefined. |
| */ |
| public void rotate(int degrees) { |
| if (degrees % 90 != 0) { |
| Log.w( |
| TAG, |
| String.format( |
| "Can only rotate in right angles (eg. 0, 90, 180, 270). %d is " |
| + "unsupported.", |
| degrees)); |
| mExifInterface.setAttribute( |
| ExifInterface.TAG_ORIENTATION, |
| String.valueOf(ExifInterface.ORIENTATION_UNDEFINED)); |
| return; |
| } |
| |
| degrees %= 360; |
| |
| int orientation = getOrientation(); |
| while (degrees < 0) { |
| degrees += 90; |
| |
| switch (orientation) { |
| case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: |
| orientation = ExifInterface.ORIENTATION_TRANSPOSE; |
| break; |
| case ExifInterface.ORIENTATION_ROTATE_180: |
| orientation = ExifInterface.ORIENTATION_ROTATE_90; |
| break; |
| case ExifInterface.ORIENTATION_FLIP_VERTICAL: |
| orientation = ExifInterface.ORIENTATION_TRANSVERSE; |
| break; |
| case ExifInterface.ORIENTATION_TRANSPOSE: |
| orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL; |
| break; |
| case ExifInterface.ORIENTATION_ROTATE_90: |
| orientation = ExifInterface.ORIENTATION_NORMAL; |
| break; |
| case ExifInterface.ORIENTATION_TRANSVERSE: |
| orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL; |
| break; |
| case ExifInterface.ORIENTATION_ROTATE_270: |
| orientation = ExifInterface.ORIENTATION_ROTATE_90; |
| break; |
| case ExifInterface.ORIENTATION_NORMAL: |
| // Fall-through |
| case ExifInterface.ORIENTATION_UNDEFINED: |
| // Fall-through |
| default: |
| orientation = ExifInterface.ORIENTATION_ROTATE_270; |
| break; |
| } |
| } |
| while (degrees > 0) { |
| degrees -= 90; |
| |
| switch (orientation) { |
| case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: |
| orientation = ExifInterface.ORIENTATION_TRANSVERSE; |
| break; |
| case ExifInterface.ORIENTATION_ROTATE_180: |
| orientation = ExifInterface.ORIENTATION_ROTATE_270; |
| break; |
| case ExifInterface.ORIENTATION_FLIP_VERTICAL: |
| orientation = ExifInterface.ORIENTATION_TRANSPOSE; |
| break; |
| case ExifInterface.ORIENTATION_TRANSPOSE: |
| orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL; |
| break; |
| case ExifInterface.ORIENTATION_ROTATE_90: |
| orientation = ExifInterface.ORIENTATION_ROTATE_180; |
| break; |
| case ExifInterface.ORIENTATION_TRANSVERSE: |
| orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL; |
| break; |
| case ExifInterface.ORIENTATION_ROTATE_270: |
| orientation = ExifInterface.ORIENTATION_NORMAL; |
| break; |
| case ExifInterface.ORIENTATION_NORMAL: |
| // Fall-through |
| case ExifInterface.ORIENTATION_UNDEFINED: |
| // Fall-through |
| default: |
| orientation = ExifInterface.ORIENTATION_ROTATE_90; |
| break; |
| } |
| } |
| mExifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation)); |
| } |
| |
| /** |
| * Sets attributes to represent a flip of the image over the horizon so that the top and bottom |
| * are reversed. |
| */ |
| public void flipVertically() { |
| int orientation; |
| switch (getOrientation()) { |
| case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: |
| orientation = ExifInterface.ORIENTATION_ROTATE_180; |
| break; |
| case ExifInterface.ORIENTATION_ROTATE_180: |
| orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL; |
| break; |
| case ExifInterface.ORIENTATION_FLIP_VERTICAL: |
| orientation = ExifInterface.ORIENTATION_NORMAL; |
| break; |
| case ExifInterface.ORIENTATION_TRANSPOSE: |
| orientation = ExifInterface.ORIENTATION_ROTATE_270; |
| break; |
| case ExifInterface.ORIENTATION_ROTATE_90: |
| orientation = ExifInterface.ORIENTATION_TRANSVERSE; |
| break; |
| case ExifInterface.ORIENTATION_TRANSVERSE: |
| orientation = ExifInterface.ORIENTATION_ROTATE_90; |
| break; |
| case ExifInterface.ORIENTATION_ROTATE_270: |
| orientation = ExifInterface.ORIENTATION_TRANSPOSE; |
| break; |
| case ExifInterface.ORIENTATION_NORMAL: |
| // Fall-through |
| case ExifInterface.ORIENTATION_UNDEFINED: |
| // Fall-through |
| default: |
| orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL; |
| break; |
| } |
| mExifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation)); |
| } |
| |
| /** |
| * Sets attributes to represent a flip of the image over the vertical so that the left and right |
| * are reversed. |
| */ |
| public void flipHorizontally() { |
| int orientation; |
| switch (getOrientation()) { |
| case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: |
| orientation = ExifInterface.ORIENTATION_NORMAL; |
| break; |
| case ExifInterface.ORIENTATION_ROTATE_180: |
| orientation = ExifInterface.ORIENTATION_FLIP_VERTICAL; |
| break; |
| case ExifInterface.ORIENTATION_FLIP_VERTICAL: |
| orientation = ExifInterface.ORIENTATION_ROTATE_180; |
| break; |
| case ExifInterface.ORIENTATION_TRANSPOSE: |
| orientation = ExifInterface.ORIENTATION_ROTATE_90; |
| break; |
| case ExifInterface.ORIENTATION_ROTATE_90: |
| orientation = ExifInterface.ORIENTATION_TRANSPOSE; |
| break; |
| case ExifInterface.ORIENTATION_TRANSVERSE: |
| orientation = ExifInterface.ORIENTATION_ROTATE_270; |
| break; |
| case ExifInterface.ORIENTATION_ROTATE_270: |
| orientation = ExifInterface.ORIENTATION_TRANSVERSE; |
| break; |
| case ExifInterface.ORIENTATION_NORMAL: |
| // Fall-through |
| case ExifInterface.ORIENTATION_UNDEFINED: |
| // Fall-through |
| default: |
| orientation = ExifInterface.ORIENTATION_FLIP_HORIZONTAL; |
| break; |
| } |
| mExifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation)); |
| } |
| |
| /** Attaches the current timestamp to the file. */ |
| public void attachTimestamp() { |
| long now = System.currentTimeMillis(); |
| String datetime = convertToExifDateTime(now); |
| |
| mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, datetime); |
| mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, datetime); |
| |
| try { |
| String subsec = Long.toString(now - convertFromExifDateTime(datetime).getTime()); |
| mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, subsec); |
| mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, subsec); |
| } catch (ParseException e) { |
| } |
| |
| mRemoveTimestamp = false; |
| } |
| |
| /** Removes the timestamp from the file. */ |
| public void removeTimestamp() { |
| mExifInterface.setAttribute(ExifInterface.TAG_DATETIME, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, null); |
| mRemoveTimestamp = true; |
| } |
| |
| /** Attaches the given location to the file. */ |
| public void attachLocation(Location location) { |
| mExifInterface.setGpsInfo(location); |
| } |
| |
| /** Removes the location from the file. */ |
| public void removeLocation() { |
| mExifInterface.setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_GPS_ALTITUDE_REF, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_GPS_SPEED_REF, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, null); |
| mExifInterface.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, null); |
| } |
| |
| /** @return The timestamp (in millis), or {@link #INVALID_TIMESTAMP} if no time is available. */ |
| private long parseTimestamp(@Nullable String date, @Nullable String time) { |
| if (date == null && time == null) { |
| return INVALID_TIMESTAMP; |
| } |
| if (time == null) { |
| try { |
| return convertFromExifDate(date).getTime(); |
| } catch (ParseException e) { |
| return INVALID_TIMESTAMP; |
| } |
| } |
| if (date == null) { |
| try { |
| return convertFromExifTime(time).getTime(); |
| } catch (ParseException e) { |
| return INVALID_TIMESTAMP; |
| } |
| } |
| return parseTimestamp(date + " " + time); |
| } |
| |
| /** @return The timestamp (in millis), or {@link #INVALID_TIMESTAMP} if no time is available. */ |
| private long parseTimestamp(@Nullable String datetime) { |
| if (datetime == null) { |
| return INVALID_TIMESTAMP; |
| } |
| try { |
| return convertFromExifDateTime(datetime).getTime(); |
| } catch (ParseException e) { |
| return INVALID_TIMESTAMP; |
| } |
| } |
| |
| private static final class Speed { |
| static Converter fromKilometersPerHour(double kph) { |
| return new Converter(kph * 0.621371); |
| } |
| |
| static Converter fromMetersPerSecond(double mps) { |
| return new Converter(mps * 2.23694); |
| } |
| |
| static Converter fromMilesPerHour(double mph) { |
| return new Converter(mph); |
| } |
| |
| static Converter fromKnots(double knots) { |
| return new Converter(knots * 1.15078); |
| } |
| |
| static final class Converter { |
| final double mMph; |
| |
| Converter(double mph) { |
| mMph = mph; |
| } |
| |
| double toKilometersPerHour() { |
| return mMph / 0.621371; |
| } |
| |
| double toMilesPerHour() { |
| return mMph; |
| } |
| |
| double toKnots() { |
| return mMph / 1.15078; |
| } |
| |
| double toMetersPerSecond() { |
| return mMph / 2.23694; |
| } |
| } |
| } |
| } |