1489 lines
46 KiB
Java
1489 lines
46 KiB
Java
/*
|
|
* Copyright (C) 2007 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 com.android.globaltime;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.util.ArrayList;
|
|
import java.util.Calendar;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.TimeZone;
|
|
|
|
import javax.microedition.khronos.egl.*;
|
|
import javax.microedition.khronos.opengles.*;
|
|
|
|
import android.app.Activity;
|
|
import android.content.Context;
|
|
import android.content.res.AssetManager;
|
|
import android.graphics.Canvas;
|
|
import android.opengl.Object3D;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.Looper;
|
|
import android.os.Message;
|
|
import android.os.MessageQueue;
|
|
import android.util.Log;
|
|
import android.view.KeyEvent;
|
|
import android.view.MotionEvent;
|
|
import android.view.SurfaceHolder;
|
|
import android.view.SurfaceView;
|
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
|
import android.view.animation.DecelerateInterpolator;
|
|
import android.view.animation.Interpolator;
|
|
|
|
/**
|
|
* The main View of the GlobalTime Activity.
|
|
*/
|
|
class GTView extends SurfaceView implements SurfaceHolder.Callback {
|
|
|
|
/**
|
|
* A TimeZone object used to compute the current UTC time.
|
|
*/
|
|
private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("utc");
|
|
|
|
/**
|
|
* The Sun's color is close to that of a 5780K blackbody.
|
|
*/
|
|
private static final float[] SUNLIGHT_COLOR = {
|
|
1.0f, 0.9375f, 0.91015625f, 1.0f
|
|
};
|
|
|
|
/**
|
|
* The inclination of the earth relative to the plane of the ecliptic
|
|
* is 23.45 degrees.
|
|
*/
|
|
private static final float EARTH_INCLINATION = 23.45f * Shape.PI / 180.0f;
|
|
|
|
/** Seconds in a day */
|
|
private static final int SECONDS_PER_DAY = 24 * 60 * 60;
|
|
|
|
/** Flag for the depth test */
|
|
private static final boolean PERFORM_DEPTH_TEST= false;
|
|
|
|
/** Use raw time zone offsets, disregarding "summer time." If false,
|
|
* current offsets will be used, which requires a much longer startup time
|
|
* in order to sort the city database.
|
|
*/
|
|
private static final boolean USE_RAW_OFFSETS = true;
|
|
|
|
/**
|
|
* The earth's atmosphere.
|
|
*/
|
|
private static final Annulus ATMOSPHERE =
|
|
new Annulus(0.0f, 0.0f, 1.75f, 0.9f, 1.08f, 0.4f, 0.4f, 0.8f, 0.0f,
|
|
0.0f, 0.0f, 0.0f, 1.0f, 50);
|
|
|
|
/**
|
|
* The tesselation of the earth by latitude.
|
|
*/
|
|
private static final int SPHERE_LATITUDES = 25;
|
|
|
|
/**
|
|
* The tesselation of the earth by longitude.
|
|
*/
|
|
private static int SPHERE_LONGITUDES = 25;
|
|
|
|
/**
|
|
* A flattened version of the earth. The normals are computed identically
|
|
* to those of the round earth, allowing the day/night lighting to be
|
|
* applied to the flattened surface.
|
|
*/
|
|
private static Sphere worldFlat = new LatLongSphere(0.0f, 0.0f, 0.0f, 1.0f,
|
|
SPHERE_LATITUDES, SPHERE_LONGITUDES,
|
|
0.0f, 360.0f, true, true, false, true);
|
|
|
|
/**
|
|
* The earth.
|
|
*/
|
|
private Object3D mWorld;
|
|
|
|
/**
|
|
* Geometry of the city lights
|
|
*/
|
|
private PointCloud mLights;
|
|
|
|
/**
|
|
* True if the activiy has been initialized.
|
|
*/
|
|
boolean mInitialized = false;
|
|
|
|
/**
|
|
* True if we're in alphabetic entry mode.
|
|
*/
|
|
private boolean mAlphaKeySet = false;
|
|
|
|
private EGLContext mEGLContext;
|
|
private EGLSurface mEGLSurface;
|
|
private EGLDisplay mEGLDisplay;
|
|
private EGLConfig mEGLConfig;
|
|
GLView mGLView;
|
|
|
|
// Rotation and tilt of the Earth
|
|
private float mRotAngle = 0.0f;
|
|
private float mTiltAngle = 0.0f;
|
|
|
|
// Rotational velocity of the orbiting viewer
|
|
private float mRotVelocity = 1.0f;
|
|
|
|
// Rotation of the flat view
|
|
private float mWrapX = 0.0f;
|
|
private float mWrapVelocity = 0.0f;
|
|
private float mWrapVelocityFactor = 0.01f;
|
|
|
|
// Toggle switches
|
|
private boolean mDisplayAtmosphere = true;
|
|
private boolean mDisplayClock = false;
|
|
private boolean mClockShowing = false;
|
|
private boolean mDisplayLights = false;
|
|
private boolean mDisplayWorld = true;
|
|
private boolean mDisplayWorldFlat = false;
|
|
private boolean mSmoothShading = true;
|
|
|
|
// City search string
|
|
private String mCityName = "";
|
|
|
|
// List of all cities
|
|
private List<City> mClockCities;
|
|
|
|
// List of cities matching a user-supplied prefix
|
|
private List<City> mCityNameMatches = new ArrayList<City>();
|
|
|
|
private List<City> mCities;
|
|
|
|
// Start time for clock fade animation
|
|
private long mClockFadeTime;
|
|
|
|
// Interpolator for clock fade animation
|
|
private Interpolator mClockSizeInterpolator =
|
|
new DecelerateInterpolator(1.0f);
|
|
|
|
// Index of current clock
|
|
private int mCityIndex;
|
|
|
|
// Current clock
|
|
private Clock mClock;
|
|
|
|
// City-to-city flight animation parameters
|
|
private boolean mFlyToCity = false;
|
|
private long mCityFlyStartTime;
|
|
private float mCityFlightTime;
|
|
private float mRotAngleStart, mRotAngleDest;
|
|
private float mTiltAngleStart, mTiltAngleDest;
|
|
|
|
// Interpolator for flight motion animation
|
|
private Interpolator mFlyToCityInterpolator =
|
|
new AccelerateDecelerateInterpolator();
|
|
|
|
private static int sNumLights;
|
|
private static int[] sLightCoords;
|
|
|
|
// static Map<Float,int[]> cityCoords = new HashMap<Float,int[]>();
|
|
|
|
// Arrays for GL calls
|
|
private float[] mClipPlaneEquation = new float[4];
|
|
private float[] mLightDir = new float[4];
|
|
|
|
// Calendar for computing the Sun's position
|
|
Calendar mSunCal = Calendar.getInstance(UTC_TIME_ZONE);
|
|
|
|
// Triangles drawn per frame
|
|
private int mNumTriangles;
|
|
|
|
private long startTime;
|
|
|
|
private static final int MOTION_NONE = 0;
|
|
private static final int MOTION_X = 1;
|
|
private static final int MOTION_Y = 2;
|
|
|
|
private static final int MIN_MANHATTAN_DISTANCE = 20;
|
|
private static final float ROTATION_FACTOR = 1.0f / 30.0f;
|
|
private static final float TILT_FACTOR = 0.35f;
|
|
|
|
// Touchscreen support
|
|
private float mMotionStartX;
|
|
private float mMotionStartY;
|
|
private float mMotionStartRotVelocity;
|
|
private float mMotionStartTiltAngle;
|
|
private int mMotionDirection;
|
|
|
|
private boolean mPaused = true;
|
|
private boolean mHaveSurface = false;
|
|
private boolean mStartAnimating = false;
|
|
|
|
public void surfaceCreated(SurfaceHolder holder) {
|
|
mHaveSurface = true;
|
|
startEGL();
|
|
}
|
|
|
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
|
mHaveSurface = false;
|
|
stopEGL();
|
|
}
|
|
|
|
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
|
|
// nothing to do
|
|
}
|
|
|
|
/**
|
|
* Set up the view.
|
|
*
|
|
* @param context the Context
|
|
* @param am an AssetManager to retrieve the city database from
|
|
*/
|
|
public GTView(Context context) {
|
|
super(context);
|
|
|
|
getHolder().addCallback(this);
|
|
getHolder().setType(SurfaceHolder.SURFACE_TYPE_GPU);
|
|
|
|
startTime = System.currentTimeMillis();
|
|
|
|
mClock = new Clock();
|
|
|
|
startEGL();
|
|
|
|
setFocusable(true);
|
|
setFocusableInTouchMode(true);
|
|
requestFocus();
|
|
}
|
|
|
|
/**
|
|
* Creates an egl context. If the state of the activity is right, also
|
|
* creates the egl surface. Otherwise the surface will be created in a
|
|
* future call to createEGLSurface().
|
|
*/
|
|
private void startEGL() {
|
|
EGL10 egl = (EGL10)EGLContext.getEGL();
|
|
|
|
if (mEGLContext == null) {
|
|
EGLDisplay dpy = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
|
|
int[] version = new int[2];
|
|
egl.eglInitialize(dpy, version);
|
|
int[] configSpec = {
|
|
EGL10.EGL_DEPTH_SIZE, 16,
|
|
EGL10.EGL_NONE
|
|
};
|
|
EGLConfig[] configs = new EGLConfig[1];
|
|
int[] num_config = new int[1];
|
|
egl.eglChooseConfig(dpy, configSpec, configs, 1, num_config);
|
|
mEGLConfig = configs[0];
|
|
|
|
mEGLContext = egl.eglCreateContext(dpy, mEGLConfig,
|
|
EGL10.EGL_NO_CONTEXT, null);
|
|
mEGLDisplay = dpy;
|
|
|
|
AssetManager am = mContext.getAssets();
|
|
try {
|
|
loadAssets(am);
|
|
} catch (IOException ioe) {
|
|
ioe.printStackTrace();
|
|
throw new RuntimeException(ioe);
|
|
} catch (ArrayIndexOutOfBoundsException aioobe) {
|
|
aioobe.printStackTrace();
|
|
throw new RuntimeException(aioobe);
|
|
}
|
|
}
|
|
|
|
if (mEGLSurface == null && !mPaused && mHaveSurface) {
|
|
mEGLSurface = egl.eglCreateWindowSurface(mEGLDisplay, mEGLConfig,
|
|
this, null);
|
|
egl.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface,
|
|
mEGLContext);
|
|
mInitialized = false;
|
|
if (mStartAnimating) {
|
|
startAnimating();
|
|
mStartAnimating = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroys the egl context. If an egl surface has been created, it is
|
|
* destroyed as well.
|
|
*/
|
|
private void stopEGL() {
|
|
EGL10 egl = (EGL10)EGLContext.getEGL();
|
|
if (mEGLSurface != null) {
|
|
egl.eglMakeCurrent(mEGLDisplay,
|
|
egl.EGL_NO_SURFACE, egl.EGL_NO_SURFACE, egl.EGL_NO_CONTEXT);
|
|
egl.eglDestroySurface(mEGLDisplay, mEGLSurface);
|
|
mEGLSurface = null;
|
|
}
|
|
|
|
if (mEGLContext != null) {
|
|
egl.eglDestroyContext(mEGLDisplay, mEGLContext);
|
|
egl.eglTerminate(mEGLDisplay);
|
|
mEGLContext = null;
|
|
mEGLDisplay = null;
|
|
mEGLConfig = null;
|
|
}
|
|
}
|
|
|
|
public void onPause() {
|
|
mPaused = true;
|
|
stopAnimating();
|
|
stopEGL();
|
|
}
|
|
|
|
public void onResume() {
|
|
mPaused = false;
|
|
startEGL();
|
|
}
|
|
|
|
public void destroy() {
|
|
stopAnimating();
|
|
stopEGL();
|
|
}
|
|
|
|
/**
|
|
* Begin animation.
|
|
*/
|
|
public void startAnimating() {
|
|
if (mEGLSurface == null) {
|
|
mStartAnimating = true; // will start when egl surface is created
|
|
} else {
|
|
mHandler.sendEmptyMessage(INVALIDATE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Quit animation.
|
|
*/
|
|
public void stopAnimating() {
|
|
mHandler.removeMessages(INVALIDATE);
|
|
}
|
|
|
|
/**
|
|
* Read a two-byte integer from the input stream.
|
|
*/
|
|
private int readInt16(InputStream is) throws IOException {
|
|
int lo = is.read();
|
|
int hi = is.read();
|
|
return (hi << 8) | lo;
|
|
}
|
|
|
|
/**
|
|
* Returns the offset from UTC for the given city. If USE_RAW_OFFSETS
|
|
* is true, summer/daylight savings is ignored.
|
|
*/
|
|
private static float getOffset(City c) {
|
|
return USE_RAW_OFFSETS ? c.getRawOffset() : c.getOffset();
|
|
}
|
|
|
|
private InputStream cache(InputStream is) throws IOException {
|
|
int nbytes = is.available();
|
|
byte[] data = new byte[nbytes];
|
|
int nread = 0;
|
|
while (nread < nbytes) {
|
|
nread += is.read(data, nread, nbytes - nread);
|
|
}
|
|
return new ByteArrayInputStream(data);
|
|
}
|
|
|
|
/**
|
|
* Load the city and lights databases.
|
|
*
|
|
* @param am the AssetManager to load from.
|
|
*/
|
|
private void loadAssets(final AssetManager am) throws IOException {
|
|
Locale locale = Locale.getDefault();
|
|
String language = locale.getLanguage();
|
|
String country = locale.getCountry();
|
|
|
|
InputStream cis = null;
|
|
try {
|
|
// Look for (e.g.) cities_fr_FR.dat or cities_fr_CA.dat
|
|
cis = am.open("cities_" + language + "_" + country + ".dat");
|
|
} catch (FileNotFoundException e1) {
|
|
try {
|
|
// Look for (e.g.) cities_fr.dat or cities_fr.dat
|
|
cis = am.open("cities_" + language + ".dat");
|
|
} catch (FileNotFoundException e2) {
|
|
try {
|
|
// Use English city names by default
|
|
cis = am.open("cities_en.dat");
|
|
} catch (FileNotFoundException e3) {
|
|
throw e3;
|
|
}
|
|
}
|
|
}
|
|
|
|
cis = cache(cis);
|
|
City.loadCities(cis);
|
|
City[] cities;
|
|
if (USE_RAW_OFFSETS) {
|
|
cities = City.getCitiesByRawOffset();
|
|
} else {
|
|
cities = City.getCitiesByOffset();
|
|
}
|
|
|
|
mClockCities = new ArrayList<City>(cities.length);
|
|
for (int i = 0; i < cities.length; i++) {
|
|
mClockCities.add(cities[i]);
|
|
}
|
|
mCities = mClockCities;
|
|
mCityIndex = 0;
|
|
|
|
this.mWorld = new Object3D() {
|
|
@Override
|
|
public InputStream readFile(String filename)
|
|
throws IOException {
|
|
return cache(am.open(filename));
|
|
}
|
|
};
|
|
|
|
mWorld.load("world.gles");
|
|
|
|
// lights.dat has the following format. All integers
|
|
// are 16 bits, low byte first.
|
|
//
|
|
// width
|
|
// height
|
|
// N [# of lights]
|
|
// light 0 X [in the range 0 to (width - 1)]
|
|
// light 0 Y ]in the range 0 to (height - 1)]
|
|
// light 1 X [in the range 0 to (width - 1)]
|
|
// light 1 Y ]in the range 0 to (height - 1)]
|
|
// ...
|
|
// light (N - 1) X [in the range 0 to (width - 1)]
|
|
// light (N - 1) Y ]in the range 0 to (height - 1)]
|
|
//
|
|
// For a larger number of lights, it could make more
|
|
// sense to store the light positions in a bitmap
|
|
// and extract them manually
|
|
InputStream lis = am.open("lights.dat");
|
|
lis = cache(lis);
|
|
|
|
int lightWidth = readInt16(lis);
|
|
int lightHeight = readInt16(lis);
|
|
sNumLights = readInt16(lis);
|
|
sLightCoords = new int[3 * sNumLights];
|
|
|
|
int lidx = 0;
|
|
float lightRadius = 1.009f;
|
|
float lightScale = 65536.0f * lightRadius;
|
|
|
|
float[] cosTheta = new float[lightWidth];
|
|
float[] sinTheta = new float[lightWidth];
|
|
float twoPi = (float) (2.0 * Math.PI);
|
|
float scaleW = twoPi / lightWidth;
|
|
for (int i = 0; i < lightWidth; i++) {
|
|
float theta = twoPi - i * scaleW;
|
|
cosTheta[i] = (float)Math.cos(theta);
|
|
sinTheta[i] = (float)Math.sin(theta);
|
|
}
|
|
|
|
float[] cosPhi = new float[lightHeight];
|
|
float[] sinPhi = new float[lightHeight];
|
|
float scaleH = (float) (Math.PI / lightHeight);
|
|
for (int j = 0; j < lightHeight; j++) {
|
|
float phi = j * scaleH;
|
|
cosPhi[j] = (float)Math.cos(phi);
|
|
sinPhi[j] = (float)Math.sin(phi);
|
|
}
|
|
|
|
int nbytes = 4 * sNumLights;
|
|
byte[] ilights = new byte[nbytes];
|
|
int nread = 0;
|
|
while (nread < nbytes) {
|
|
nread += lis.read(ilights, nread, nbytes - nread);
|
|
}
|
|
|
|
int idx = 0;
|
|
for (int i = 0; i < sNumLights; i++) {
|
|
int lx = (((ilights[idx + 1] & 0xff) << 8) |
|
|
(ilights[idx ] & 0xff));
|
|
int ly = (((ilights[idx + 3] & 0xff) << 8) |
|
|
(ilights[idx + 2] & 0xff));
|
|
idx += 4;
|
|
|
|
float sin = sinPhi[ly];
|
|
float x = cosTheta[lx]*sin;
|
|
float y = cosPhi[ly];
|
|
float z = sinTheta[lx]*sin;
|
|
|
|
sLightCoords[lidx++] = (int) (x * lightScale);
|
|
sLightCoords[lidx++] = (int) (y * lightScale);
|
|
sLightCoords[lidx++] = (int) (z * lightScale);
|
|
}
|
|
mLights = new PointCloud(sLightCoords);
|
|
}
|
|
|
|
/**
|
|
* Returns true if two time zone offsets are equal. We assume distinct
|
|
* time zone offsets will differ by at least a few minutes.
|
|
*/
|
|
private boolean tzEqual(float o1, float o2) {
|
|
return Math.abs(o1 - o2) < 0.001;
|
|
}
|
|
|
|
/**
|
|
* Move to a different time zone.
|
|
*
|
|
* @param incr The increment between the current and future time zones.
|
|
*/
|
|
private void shiftTimeZone(int incr) {
|
|
// If only 1 city in the current set, there's nowhere to go
|
|
if (mCities.size() <= 1) {
|
|
return;
|
|
}
|
|
|
|
float offset = getOffset(mCities.get(mCityIndex));
|
|
do {
|
|
mCityIndex = (mCityIndex + mCities.size() + incr) % mCities.size();
|
|
} while (tzEqual(getOffset(mCities.get(mCityIndex)), offset));
|
|
|
|
offset = getOffset(mCities.get(mCityIndex));
|
|
locateCity(true, offset);
|
|
goToCity();
|
|
}
|
|
|
|
/**
|
|
* Returns true if there is another city within the current time zone
|
|
* that is the given increment away from the current city.
|
|
*
|
|
* @param incr the increment, +1 or -1
|
|
* @return
|
|
*/
|
|
private boolean atEndOfTimeZone(int incr) {
|
|
if (mCities.size() <= 1) {
|
|
return true;
|
|
}
|
|
|
|
float offset = getOffset(mCities.get(mCityIndex));
|
|
int nindex = (mCityIndex + mCities.size() + incr) % mCities.size();
|
|
if (tzEqual(getOffset(mCities.get(nindex)), offset)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Shifts cities within the current time zone.
|
|
*
|
|
* @param incr the increment, +1 or -1
|
|
*/
|
|
private void shiftWithinTimeZone(int incr) {
|
|
float offset = getOffset(mCities.get(mCityIndex));
|
|
int nindex = (mCityIndex + mCities.size() + incr) % mCities.size();
|
|
if (tzEqual(getOffset(mCities.get(nindex)), offset)) {
|
|
mCityIndex = nindex;
|
|
goToCity();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if the city name matches the given prefix, ignoring spaces.
|
|
*/
|
|
private boolean nameMatches(City city, String prefix) {
|
|
String cityName = city.getName().replaceAll("[ ]", "");
|
|
return prefix.regionMatches(true, 0,
|
|
cityName, 0,
|
|
prefix.length());
|
|
}
|
|
|
|
/**
|
|
* Returns true if there are cities matching the given name prefix.
|
|
*/
|
|
private boolean hasMatches(String prefix) {
|
|
for (int i = 0; i < mClockCities.size(); i++) {
|
|
City city = mClockCities.get(i);
|
|
if (nameMatches(city, prefix)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Shifts to the nearest city that matches the new prefix.
|
|
*/
|
|
private void shiftByName() {
|
|
// Attempt to keep current city if it matches
|
|
City finalCity = null;
|
|
City currCity = mCities.get(mCityIndex);
|
|
if (nameMatches(currCity, mCityName)) {
|
|
finalCity = currCity;
|
|
}
|
|
|
|
mCityNameMatches.clear();
|
|
for (int i = 0; i < mClockCities.size(); i++) {
|
|
City city = mClockCities.get(i);
|
|
if (nameMatches(city, mCityName)) {
|
|
mCityNameMatches.add(city);
|
|
}
|
|
}
|
|
|
|
mCities = mCityNameMatches;
|
|
|
|
if (finalCity != null) {
|
|
for (int i = 0; i < mCityNameMatches.size(); i++) {
|
|
if (mCityNameMatches.get(i) == finalCity) {
|
|
mCityIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// Find the closest matching city
|
|
locateCity(false, 0.0f);
|
|
}
|
|
goToCity();
|
|
}
|
|
|
|
/**
|
|
* Increases or decreases the rotational speed of the earth.
|
|
*/
|
|
private void incrementRotationalVelocity(float incr) {
|
|
if (mDisplayWorldFlat) {
|
|
mWrapVelocity -= incr;
|
|
} else {
|
|
mRotVelocity -= incr;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears the current matching prefix, while keeping the focus on
|
|
* the current city.
|
|
*/
|
|
private void clearCityMatches() {
|
|
// Determine the global city index that matches the current city
|
|
if (mCityNameMatches.size() > 0) {
|
|
City city = mCityNameMatches.get(mCityIndex);
|
|
for (int i = 0; i < mClockCities.size(); i++) {
|
|
City ncity = mClockCities.get(i);
|
|
if (city.equals(ncity)) {
|
|
mCityIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
mCityName = "";
|
|
mCityNameMatches.clear();
|
|
mCities = mClockCities;
|
|
goToCity();
|
|
}
|
|
|
|
/**
|
|
* Fade the clock in or out.
|
|
*/
|
|
private void enableClock(boolean enabled) {
|
|
mClockFadeTime = System.currentTimeMillis();
|
|
mDisplayClock = enabled;
|
|
mClockShowing = true;
|
|
mAlphaKeySet = enabled;
|
|
if (enabled) {
|
|
// Find the closest matching city
|
|
locateCity(false, 0.0f);
|
|
}
|
|
clearCityMatches();
|
|
}
|
|
|
|
/**
|
|
* Use the touchscreen to alter the rotational velocity or the
|
|
* tilt of the earth.
|
|
*/
|
|
@Override public boolean onTouchEvent(MotionEvent event) {
|
|
switch (event.getAction()) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
mMotionStartX = event.getX();
|
|
mMotionStartY = event.getY();
|
|
mMotionStartRotVelocity = mDisplayWorldFlat ?
|
|
mWrapVelocity : mRotVelocity;
|
|
mMotionStartTiltAngle = mTiltAngle;
|
|
|
|
// Stop the rotation
|
|
if (mDisplayWorldFlat) {
|
|
mWrapVelocity = 0.0f;
|
|
} else {
|
|
mRotVelocity = 0.0f;
|
|
}
|
|
mMotionDirection = MOTION_NONE;
|
|
break;
|
|
|
|
case MotionEvent.ACTION_MOVE:
|
|
// Disregard motion events when the clock is displayed
|
|
float dx = event.getX() - mMotionStartX;
|
|
float dy = event.getY() - mMotionStartY;
|
|
float delx = Math.abs(dx);
|
|
float dely = Math.abs(dy);
|
|
|
|
// Determine the direction of motion (major axis)
|
|
// Once if has been determined, it's locked in until
|
|
// we receive ACTION_UP or ACTION_CANCEL
|
|
if ((mMotionDirection == MOTION_NONE) &&
|
|
(delx + dely > MIN_MANHATTAN_DISTANCE)) {
|
|
if (delx > dely) {
|
|
mMotionDirection = MOTION_X;
|
|
} else {
|
|
mMotionDirection = MOTION_Y;
|
|
}
|
|
}
|
|
|
|
// If the clock is displayed, don't actually rotate or tilt;
|
|
// just use mMotionDirection to record whether motion occurred
|
|
if (!mDisplayClock) {
|
|
if (mMotionDirection == MOTION_X) {
|
|
if (mDisplayWorldFlat) {
|
|
mWrapVelocity = mMotionStartRotVelocity +
|
|
dx * ROTATION_FACTOR;
|
|
} else {
|
|
mRotVelocity = mMotionStartRotVelocity +
|
|
dx * ROTATION_FACTOR;
|
|
}
|
|
mClock.setCity(null);
|
|
} else if (mMotionDirection == MOTION_Y &&
|
|
!mDisplayWorldFlat) {
|
|
mTiltAngle = mMotionStartTiltAngle + dy * TILT_FACTOR;
|
|
if (mTiltAngle < -90.0f) {
|
|
mTiltAngle = -90.0f;
|
|
}
|
|
if (mTiltAngle > 90.0f) {
|
|
mTiltAngle = 90.0f;
|
|
}
|
|
mClock.setCity(null);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MotionEvent.ACTION_UP:
|
|
mMotionDirection = MOTION_NONE;
|
|
break;
|
|
|
|
case MotionEvent.ACTION_CANCEL:
|
|
mTiltAngle = mMotionStartTiltAngle;
|
|
if (mDisplayWorldFlat) {
|
|
mWrapVelocity = mMotionStartRotVelocity;
|
|
} else {
|
|
mRotVelocity = mMotionStartRotVelocity;
|
|
}
|
|
mMotionDirection = MOTION_NONE;
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
if (mInitialized && mGLView.processKey(keyCode)) {
|
|
boolean drawing = (mClockShowing || mGLView.hasMessages());
|
|
this.setWillNotDraw(!drawing);
|
|
return true;
|
|
}
|
|
|
|
boolean handled = false;
|
|
|
|
// If we're not in alphabetical entry mode, convert letters
|
|
// to their digit equivalents
|
|
if (!mAlphaKeySet) {
|
|
char numChar = event.getNumber();
|
|
if (numChar >= '0' && numChar <= '9') {
|
|
keyCode = KeyEvent.KEYCODE_0 + (numChar - '0');
|
|
}
|
|
}
|
|
|
|
switch (keyCode) {
|
|
// The 'space' key toggles the clock
|
|
case KeyEvent.KEYCODE_SPACE:
|
|
mAlphaKeySet = !mAlphaKeySet;
|
|
enableClock(mAlphaKeySet);
|
|
handled = true;
|
|
break;
|
|
|
|
// The 'left' and 'right' buttons shift time zones if the clock is
|
|
// displayed, otherwise they alters the rotational speed of the earthh
|
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
|
if (mDisplayClock) {
|
|
shiftTimeZone(-1);
|
|
} else {
|
|
mClock.setCity(null);
|
|
incrementRotationalVelocity(1.0f);
|
|
}
|
|
handled = true;
|
|
break;
|
|
|
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
|
if (mDisplayClock) {
|
|
shiftTimeZone(1);
|
|
} else {
|
|
mClock.setCity(null);
|
|
incrementRotationalVelocity(-1.0f);
|
|
}
|
|
handled = true;
|
|
break;
|
|
|
|
// The 'up' and 'down' buttons shift cities within a time zone if the
|
|
// clock is displayed, otherwise they tilt the earth
|
|
case KeyEvent.KEYCODE_DPAD_UP:
|
|
if (mDisplayClock) {
|
|
shiftWithinTimeZone(-1);
|
|
} else {
|
|
mClock.setCity(null);
|
|
if (!mDisplayWorldFlat) {
|
|
mTiltAngle += 360.0f / 48.0f;
|
|
}
|
|
}
|
|
handled = true;
|
|
break;
|
|
|
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
|
if (mDisplayClock) {
|
|
shiftWithinTimeZone(1);
|
|
} else {
|
|
mClock.setCity(null);
|
|
if (!mDisplayWorldFlat) {
|
|
mTiltAngle -= 360.0f / 48.0f;
|
|
}
|
|
}
|
|
handled = true;
|
|
break;
|
|
|
|
// The center key stops the earth's rotation, then toggles between the
|
|
// round and flat views of the earth
|
|
case KeyEvent.KEYCODE_DPAD_CENTER:
|
|
if ((!mDisplayWorldFlat && mRotVelocity == 0.0f) ||
|
|
(mDisplayWorldFlat && mWrapVelocity == 0.0f)) {
|
|
mDisplayWorldFlat = !mDisplayWorldFlat;
|
|
} else {
|
|
if (mDisplayWorldFlat) {
|
|
mWrapVelocity = 0.0f;
|
|
} else {
|
|
mRotVelocity = 0.0f;
|
|
}
|
|
}
|
|
handled = true;
|
|
break;
|
|
|
|
// The 'L' key toggles the city lights
|
|
case KeyEvent.KEYCODE_L:
|
|
if (!mAlphaKeySet && !mDisplayWorldFlat) {
|
|
mDisplayLights = !mDisplayLights;
|
|
handled = true;
|
|
}
|
|
break;
|
|
|
|
|
|
// The 'W' key toggles the earth (just for fun)
|
|
case KeyEvent.KEYCODE_W:
|
|
if (!mAlphaKeySet && !mDisplayWorldFlat) {
|
|
mDisplayWorld = !mDisplayWorld;
|
|
handled = true;
|
|
}
|
|
break;
|
|
|
|
// The 'A' key toggles the atmosphere
|
|
case KeyEvent.KEYCODE_A:
|
|
if (!mAlphaKeySet && !mDisplayWorldFlat) {
|
|
mDisplayAtmosphere = !mDisplayAtmosphere;
|
|
handled = true;
|
|
}
|
|
break;
|
|
|
|
// The '2' key zooms out
|
|
case KeyEvent.KEYCODE_2:
|
|
if (!mAlphaKeySet && !mDisplayWorldFlat) {
|
|
mGLView.zoom(-2);
|
|
handled = true;
|
|
}
|
|
break;
|
|
|
|
// The '8' key zooms in
|
|
case KeyEvent.KEYCODE_8:
|
|
if (!mAlphaKeySet && !mDisplayWorldFlat) {
|
|
mGLView.zoom(2);
|
|
handled = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Handle letters in city names
|
|
if (!handled && mAlphaKeySet) {
|
|
switch (keyCode) {
|
|
// Add a letter to the city name prefix
|
|
case KeyEvent.KEYCODE_A:
|
|
case KeyEvent.KEYCODE_B:
|
|
case KeyEvent.KEYCODE_C:
|
|
case KeyEvent.KEYCODE_D:
|
|
case KeyEvent.KEYCODE_E:
|
|
case KeyEvent.KEYCODE_F:
|
|
case KeyEvent.KEYCODE_G:
|
|
case KeyEvent.KEYCODE_H:
|
|
case KeyEvent.KEYCODE_I:
|
|
case KeyEvent.KEYCODE_J:
|
|
case KeyEvent.KEYCODE_K:
|
|
case KeyEvent.KEYCODE_L:
|
|
case KeyEvent.KEYCODE_M:
|
|
case KeyEvent.KEYCODE_N:
|
|
case KeyEvent.KEYCODE_O:
|
|
case KeyEvent.KEYCODE_P:
|
|
case KeyEvent.KEYCODE_Q:
|
|
case KeyEvent.KEYCODE_R:
|
|
case KeyEvent.KEYCODE_S:
|
|
case KeyEvent.KEYCODE_T:
|
|
case KeyEvent.KEYCODE_U:
|
|
case KeyEvent.KEYCODE_V:
|
|
case KeyEvent.KEYCODE_W:
|
|
case KeyEvent.KEYCODE_X:
|
|
case KeyEvent.KEYCODE_Y:
|
|
case KeyEvent.KEYCODE_Z:
|
|
char c = (char)(keyCode - KeyEvent.KEYCODE_A + 'A');
|
|
if (hasMatches(mCityName + c)) {
|
|
mCityName += c;
|
|
shiftByName();
|
|
}
|
|
handled = true;
|
|
break;
|
|
|
|
// Remove a letter from the city name prefix
|
|
case KeyEvent.KEYCODE_DEL:
|
|
if (mCityName.length() > 0) {
|
|
mCityName = mCityName.substring(0, mCityName.length() - 1);
|
|
shiftByName();
|
|
} else {
|
|
clearCityMatches();
|
|
}
|
|
handled = true;
|
|
break;
|
|
|
|
// Clear the city name prefix
|
|
case KeyEvent.KEYCODE_ENTER:
|
|
clearCityMatches();
|
|
handled = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
boolean drawing = (mClockShowing ||
|
|
((mGLView != null) && (mGLView.hasMessages())));
|
|
this.setWillNotDraw(!drawing);
|
|
|
|
// Let the system handle other keypresses
|
|
if (!handled) {
|
|
return super.onKeyDown(keyCode, event);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Initialize OpenGL ES drawing.
|
|
*/
|
|
private synchronized void init(GL10 gl) {
|
|
mGLView = new GLView();
|
|
mGLView.setNearFrustum(5.0f);
|
|
mGLView.setFarFrustum(50.0f);
|
|
mGLView.setLightModelAmbientIntensity(0.225f);
|
|
mGLView.setAmbientIntensity(0.0f);
|
|
mGLView.setDiffuseIntensity(1.5f);
|
|
mGLView.setDiffuseColor(SUNLIGHT_COLOR);
|
|
mGLView.setSpecularIntensity(0.0f);
|
|
mGLView.setSpecularColor(SUNLIGHT_COLOR);
|
|
|
|
if (PERFORM_DEPTH_TEST) {
|
|
gl.glEnable(GL10.GL_DEPTH_TEST);
|
|
}
|
|
gl.glDisable(GL10.GL_SCISSOR_TEST);
|
|
gl.glClearColor(0, 0, 0, 1);
|
|
gl.glHint(GL10.GL_POINT_SMOOTH_HINT, GL10.GL_NICEST);
|
|
|
|
mInitialized = true;
|
|
}
|
|
|
|
/**
|
|
* Computes the vector from the center of the earth to the sun for a
|
|
* particular moment in time.
|
|
*/
|
|
private void computeSunDirection() {
|
|
mSunCal.setTimeInMillis(System.currentTimeMillis());
|
|
int day = mSunCal.get(Calendar.DAY_OF_YEAR);
|
|
int seconds = 3600 * mSunCal.get(Calendar.HOUR_OF_DAY) +
|
|
60 * mSunCal.get(Calendar.MINUTE) + mSunCal.get(Calendar.SECOND);
|
|
day += (float) seconds / SECONDS_PER_DAY;
|
|
|
|
// Approximate declination of the sun, changes sinusoidally
|
|
// during the year. The winter solstice occurs 10 days before
|
|
// the start of the year.
|
|
float decl = (float) (EARTH_INCLINATION *
|
|
Math.cos(Shape.TWO_PI * (day + 10) / 365.0));
|
|
|
|
// Subsolar latitude, convert from (-PI/2, PI/2) -> (0, PI) form
|
|
float phi = decl + Shape.PI_OVER_TWO;
|
|
// Subsolar longitude
|
|
float theta = Shape.TWO_PI * seconds / SECONDS_PER_DAY;
|
|
|
|
float sinPhi = (float) Math.sin(phi);
|
|
float cosPhi = (float) Math.cos(phi);
|
|
float sinTheta = (float) Math.sin(theta);
|
|
float cosTheta = (float) Math.cos(theta);
|
|
|
|
// Convert from polar to rectangular coordinates
|
|
float x = cosTheta * sinPhi;
|
|
float y = cosPhi;
|
|
float z = sinTheta * sinPhi;
|
|
|
|
// Directional light -> w == 0
|
|
mLightDir[0] = x;
|
|
mLightDir[1] = y;
|
|
mLightDir[2] = z;
|
|
mLightDir[3] = 0.0f;
|
|
}
|
|
|
|
/**
|
|
* Computes the approximate spherical distance between two
|
|
* (latitude, longitude) coordinates.
|
|
*/
|
|
private float distance(float lat1, float lon1,
|
|
float lat2, float lon2) {
|
|
lat1 *= Shape.DEGREES_TO_RADIANS;
|
|
lat2 *= Shape.DEGREES_TO_RADIANS;
|
|
lon1 *= Shape.DEGREES_TO_RADIANS;
|
|
lon2 *= Shape.DEGREES_TO_RADIANS;
|
|
|
|
float r = 6371.0f; // Earth's radius in km
|
|
float dlat = lat2 - lat1;
|
|
float dlon = lon2 - lon1;
|
|
double sinlat2 = Math.sin(dlat / 2.0f);
|
|
sinlat2 *= sinlat2;
|
|
double sinlon2 = Math.sin(dlon / 2.0f);
|
|
sinlon2 *= sinlon2;
|
|
|
|
double a = sinlat2 + Math.cos(lat1) * Math.cos(lat2) * sinlon2;
|
|
double c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
return (float) (r * c);
|
|
}
|
|
|
|
/**
|
|
* Locates the closest city to the currently displayed center point,
|
|
* optionally restricting the search to cities within a given time zone.
|
|
*/
|
|
private void locateCity(boolean useOffset, float offset) {
|
|
float mindist = Float.MAX_VALUE;
|
|
int minidx = -1;
|
|
for (int i = 0; i < mCities.size(); i++) {
|
|
City city = mCities.get(i);
|
|
if (useOffset && !tzEqual(getOffset(city), offset)) {
|
|
continue;
|
|
}
|
|
float dist = distance(city.getLatitude(), city.getLongitude(),
|
|
mTiltAngle, mRotAngle - 90.0f);
|
|
if (dist < mindist) {
|
|
mindist = dist;
|
|
minidx = i;
|
|
}
|
|
}
|
|
|
|
mCityIndex = minidx;
|
|
}
|
|
|
|
/**
|
|
* Animates the earth to be centered at the current city.
|
|
*/
|
|
private void goToCity() {
|
|
City city = mCities.get(mCityIndex);
|
|
float dist = distance(city.getLatitude(), city.getLongitude(),
|
|
mTiltAngle, mRotAngle - 90.0f);
|
|
|
|
mFlyToCity = true;
|
|
mCityFlyStartTime = System.currentTimeMillis();
|
|
mCityFlightTime = dist / 5.0f; // 5000 km/sec
|
|
mRotAngleStart = mRotAngle;
|
|
mRotAngleDest = city.getLongitude() + 90;
|
|
|
|
if (mRotAngleDest - mRotAngleStart > 180.0f) {
|
|
mRotAngleDest -= 360.0f;
|
|
} else if (mRotAngleStart - mRotAngleDest > 180.0f) {
|
|
mRotAngleDest += 360.0f;
|
|
}
|
|
|
|
mTiltAngleStart = mTiltAngle;
|
|
mTiltAngleDest = city.getLatitude();
|
|
mRotVelocity = 0.0f;
|
|
}
|
|
|
|
/**
|
|
* Returns a linearly interpolated value between two values.
|
|
*/
|
|
private float lerp(float a, float b, float lerp) {
|
|
return a + (b - a)*lerp;
|
|
}
|
|
|
|
/**
|
|
* Draws the city lights, using a clip plane to restrict the lights
|
|
* to the night side of the earth.
|
|
*/
|
|
private void drawCityLights(GL10 gl, float brightness) {
|
|
gl.glEnable(GL10.GL_POINT_SMOOTH);
|
|
gl.glDisable(GL10.GL_DEPTH_TEST);
|
|
gl.glDisable(GL10.GL_LIGHTING);
|
|
gl.glDisable(GL10.GL_DITHER);
|
|
gl.glShadeModel(GL10.GL_FLAT);
|
|
gl.glEnable(GL10.GL_BLEND);
|
|
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
|
|
gl.glPointSize(1.0f);
|
|
|
|
float ls = lerp(0.8f, 0.3f, brightness);
|
|
gl.glColor4f(ls * 1.0f, ls * 1.0f, ls * 0.8f, 1.0f);
|
|
|
|
if (mDisplayWorld) {
|
|
mClipPlaneEquation[0] = -mLightDir[0];
|
|
mClipPlaneEquation[1] = -mLightDir[1];
|
|
mClipPlaneEquation[2] = -mLightDir[2];
|
|
mClipPlaneEquation[3] = 0.0f;
|
|
// Assume we have glClipPlanef() from OpenGL ES 1.1
|
|
((GL11) gl).glClipPlanef(GL11.GL_CLIP_PLANE0,
|
|
mClipPlaneEquation, 0);
|
|
gl.glEnable(GL11.GL_CLIP_PLANE0);
|
|
}
|
|
mLights.draw(gl);
|
|
if (mDisplayWorld) {
|
|
gl.glDisable(GL11.GL_CLIP_PLANE0);
|
|
}
|
|
|
|
mNumTriangles += mLights.getNumTriangles()*2;
|
|
}
|
|
|
|
/**
|
|
* Draws the atmosphere.
|
|
*/
|
|
private void drawAtmosphere(GL10 gl) {
|
|
gl.glDisable(GL10.GL_LIGHTING);
|
|
gl.glDisable(GL10.GL_CULL_FACE);
|
|
gl.glDisable(GL10.GL_DITHER);
|
|
gl.glDisable(GL10.GL_DEPTH_TEST);
|
|
gl.glShadeModel(mSmoothShading ? GL10.GL_SMOOTH : GL10.GL_FLAT);
|
|
|
|
// Draw the atmospheric layer
|
|
float tx = mGLView.getTranslateX();
|
|
float ty = mGLView.getTranslateY();
|
|
float tz = mGLView.getTranslateZ();
|
|
|
|
gl.glMatrixMode(GL10.GL_MODELVIEW);
|
|
gl.glLoadIdentity();
|
|
gl.glTranslatef(tx, ty, tz);
|
|
|
|
// Blend in the atmosphere a bit
|
|
gl.glEnable(GL10.GL_BLEND);
|
|
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
|
|
ATMOSPHERE.draw(gl);
|
|
|
|
mNumTriangles += ATMOSPHERE.getNumTriangles();
|
|
}
|
|
|
|
/**
|
|
* Draws the world in a 2D map view.
|
|
*/
|
|
private void drawWorldFlat(GL10 gl) {
|
|
gl.glDisable(GL10.GL_BLEND);
|
|
gl.glEnable(GL10.GL_DITHER);
|
|
gl.glShadeModel(mSmoothShading ? GL10.GL_SMOOTH : GL10.GL_FLAT);
|
|
|
|
gl.glTranslatef(mWrapX - 2, 0.0f, 0.0f);
|
|
worldFlat.draw(gl);
|
|
gl.glTranslatef(2.0f, 0.0f, 0.0f);
|
|
worldFlat.draw(gl);
|
|
mNumTriangles += worldFlat.getNumTriangles() * 2;
|
|
|
|
mWrapX += mWrapVelocity * mWrapVelocityFactor;
|
|
while (mWrapX < 0.0f) {
|
|
mWrapX += 2.0f;
|
|
}
|
|
while (mWrapX > 2.0f) {
|
|
mWrapX -= 2.0f;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws the world in a 2D round view.
|
|
*/
|
|
private void drawWorldRound(GL10 gl) {
|
|
gl.glDisable(GL10.GL_BLEND);
|
|
gl.glEnable(GL10.GL_DITHER);
|
|
gl.glShadeModel(mSmoothShading ? GL10.GL_SMOOTH : GL10.GL_FLAT);
|
|
|
|
mWorld.draw(gl);
|
|
mNumTriangles += mWorld.getNumTriangles();
|
|
}
|
|
|
|
/**
|
|
* Draws the clock.
|
|
*
|
|
* @param canvas the Canvas to draw to
|
|
* @param now the current time
|
|
* @param w the width of the screen
|
|
* @param h the height of the screen
|
|
* @param lerp controls the animation, between 0.0 and 1.0
|
|
*/
|
|
private void drawClock(Canvas canvas,
|
|
long now,
|
|
int w, int h,
|
|
float lerp) {
|
|
float clockAlpha = lerp(0.0f, 0.8f, lerp);
|
|
mClockShowing = clockAlpha > 0.0f;
|
|
if (clockAlpha > 0.0f) {
|
|
City city = mCities.get(mCityIndex);
|
|
mClock.setCity(city);
|
|
mClock.setTime(now);
|
|
|
|
float cx = w / 2.0f;
|
|
float cy = h / 2.0f;
|
|
float smallRadius = 18.0f;
|
|
float bigRadius = 0.75f * 0.5f * Math.min(w, h);
|
|
float radius = lerp(smallRadius, bigRadius, lerp);
|
|
|
|
// Only display left/right arrows if we are in a name search
|
|
boolean scrollingByName =
|
|
(mCityName.length() > 0) && (mCities.size() > 1);
|
|
mClock.drawClock(canvas, cx, cy, radius,
|
|
clockAlpha,
|
|
1.0f,
|
|
lerp == 1.0f, lerp == 1.0f,
|
|
!atEndOfTimeZone(-1),
|
|
!atEndOfTimeZone(1),
|
|
scrollingByName,
|
|
mCityName.length());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Draws the 2D layer.
|
|
*/
|
|
@Override protected void onDraw(Canvas canvas) {
|
|
long now = System.currentTimeMillis();
|
|
if (startTime != -1) {
|
|
startTime = -1;
|
|
}
|
|
|
|
int w = getWidth();
|
|
int h = getHeight();
|
|
|
|
// Interpolator for clock size, clock alpha, night lights intensity
|
|
float lerp = Math.min((now - mClockFadeTime)/1000.0f, 1.0f);
|
|
if (!mDisplayClock) {
|
|
// Clock is receding
|
|
lerp = 1.0f - lerp;
|
|
}
|
|
lerp = mClockSizeInterpolator.getInterpolation(lerp);
|
|
|
|
// we don't need to make sure OpenGL rendering is done because
|
|
// we're drawing in to a different surface
|
|
|
|
drawClock(canvas, now, w, h, lerp);
|
|
|
|
mGLView.showMessages(canvas);
|
|
mGLView.showStatistics(canvas, w);
|
|
}
|
|
|
|
/**
|
|
* Draws the 3D layer.
|
|
*/
|
|
protected void drawOpenGLScene() {
|
|
long now = System.currentTimeMillis();
|
|
mNumTriangles = 0;
|
|
|
|
EGL10 egl = (EGL10)EGLContext.getEGL();
|
|
GL10 gl = (GL10)mEGLContext.getGL();
|
|
|
|
if (!mInitialized) {
|
|
init(gl);
|
|
}
|
|
|
|
int w = getWidth();
|
|
int h = getHeight();
|
|
gl.glViewport(0, 0, w, h);
|
|
|
|
gl.glEnable(GL10.GL_LIGHTING);
|
|
gl.glEnable(GL10.GL_LIGHT0);
|
|
gl.glEnable(GL10.GL_CULL_FACE);
|
|
gl.glFrontFace(GL10.GL_CCW);
|
|
|
|
float ratio = (float) w / h;
|
|
mGLView.setAspectRatio(ratio);
|
|
|
|
mGLView.setTextureParameters(gl);
|
|
|
|
if (PERFORM_DEPTH_TEST) {
|
|
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
|
|
} else {
|
|
gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
|
|
}
|
|
|
|
if (mDisplayWorldFlat) {
|
|
gl.glMatrixMode(GL10.GL_PROJECTION);
|
|
gl.glLoadIdentity();
|
|
gl.glFrustumf(-1.0f, 1.0f, -1.0f / ratio, 1.0f / ratio, 1.0f, 2.0f);
|
|
gl.glMatrixMode(GL10.GL_MODELVIEW);
|
|
gl.glLoadIdentity();
|
|
gl.glTranslatef(0.0f, 0.0f, -1.0f);
|
|
} else {
|
|
mGLView.setProjection(gl);
|
|
mGLView.setView(gl);
|
|
}
|
|
|
|
if (!mDisplayWorldFlat) {
|
|
if (mFlyToCity) {
|
|
float lerp = (now - mCityFlyStartTime)/mCityFlightTime;
|
|
if (lerp >= 1.0f) {
|
|
mFlyToCity = false;
|
|
}
|
|
lerp = Math.min(lerp, 1.0f);
|
|
lerp = mFlyToCityInterpolator.getInterpolation(lerp);
|
|
mRotAngle = lerp(mRotAngleStart, mRotAngleDest, lerp);
|
|
mTiltAngle = lerp(mTiltAngleStart, mTiltAngleDest, lerp);
|
|
}
|
|
|
|
// Rotate the viewpoint around the earth
|
|
gl.glMatrixMode(GL10.GL_MODELVIEW);
|
|
gl.glRotatef(mTiltAngle, 1, 0, 0);
|
|
gl.glRotatef(mRotAngle, 0, 1, 0);
|
|
|
|
// Increment the rotation angle
|
|
mRotAngle += mRotVelocity;
|
|
if (mRotAngle < 0.0f) {
|
|
mRotAngle += 360.0f;
|
|
}
|
|
if (mRotAngle > 360.0f) {
|
|
mRotAngle -= 360.0f;
|
|
}
|
|
}
|
|
|
|
// Draw the world with lighting
|
|
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, mLightDir, 0);
|
|
mGLView.setLights(gl, GL10.GL_LIGHT0);
|
|
|
|
if (mDisplayWorldFlat) {
|
|
drawWorldFlat(gl);
|
|
} else if (mDisplayWorld) {
|
|
drawWorldRound(gl);
|
|
}
|
|
|
|
if (mDisplayLights && !mDisplayWorldFlat) {
|
|
// Interpolator for clock size, clock alpha, night lights intensity
|
|
float lerp = Math.min((now - mClockFadeTime)/1000.0f, 1.0f);
|
|
if (!mDisplayClock) {
|
|
// Clock is receding
|
|
lerp = 1.0f - lerp;
|
|
}
|
|
lerp = mClockSizeInterpolator.getInterpolation(lerp);
|
|
drawCityLights(gl, lerp);
|
|
}
|
|
|
|
if (mDisplayAtmosphere && !mDisplayWorldFlat) {
|
|
drawAtmosphere(gl);
|
|
}
|
|
mGLView.setNumTriangles(mNumTriangles);
|
|
egl.eglSwapBuffers(mEGLDisplay, mEGLSurface);
|
|
|
|
if (egl.eglGetError() == EGL11.EGL_CONTEXT_LOST) {
|
|
// we lost the gpu, quit immediately
|
|
Context c = getContext();
|
|
if (c instanceof Activity) {
|
|
((Activity)c).finish();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private static final int INVALIDATE = 1;
|
|
private static final int ONE_MINUTE = 60000;
|
|
|
|
/**
|
|
* Controls the animation using the message queue. Every time we receive
|
|
* an INVALIDATE message, we redraw and place another message in the queue.
|
|
*/
|
|
private final Handler mHandler = new Handler() {
|
|
private long mLastSunPositionTime = 0;
|
|
|
|
@Override public void handleMessage(Message msg) {
|
|
if (msg.what == INVALIDATE) {
|
|
|
|
// Use the message's time, it's good enough and
|
|
// allows us to avoid a system call.
|
|
if ((msg.getWhen() - mLastSunPositionTime) >= ONE_MINUTE) {
|
|
// Recompute the sun's position once per minute
|
|
// Place the light at the Sun's direction
|
|
computeSunDirection();
|
|
mLastSunPositionTime = msg.getWhen();
|
|
}
|
|
|
|
// Draw the GL scene
|
|
drawOpenGLScene();
|
|
|
|
// Send an update for the 2D overlay if needed
|
|
if (mInitialized &&
|
|
(mClockShowing || mGLView.hasMessages())) {
|
|
invalidate();
|
|
}
|
|
|
|
// Just send another message immediately. This works because
|
|
// drawOpenGLScene() does the timing for us -- it will
|
|
// block until the last frame has been processed.
|
|
// The invalidate message we're posting here will be
|
|
// interleaved properly with motion/key events which
|
|
// guarantee a prompt reaction to the user input.
|
|
sendEmptyMessage(INVALIDATE);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The main activity class for GlobalTime.
|
|
*/
|
|
public class GlobalTime extends Activity {
|
|
|
|
GTView gtView = null;
|
|
|
|
@Override protected void onCreate(Bundle icicle) {
|
|
super.onCreate(icicle);
|
|
gtView = new GTView(this);
|
|
setContentView(gtView);
|
|
}
|
|
|
|
@Override protected void onResume() {
|
|
super.onResume();
|
|
gtView.onResume();
|
|
Looper.myQueue().addIdleHandler(new Idler());
|
|
}
|
|
|
|
@Override protected void onPause() {
|
|
super.onPause();
|
|
gtView.onPause();
|
|
}
|
|
|
|
@Override protected void onStop() {
|
|
super.onStop();
|
|
gtView.destroy();
|
|
gtView = null;
|
|
}
|
|
|
|
// Allow the activity to go idle before its animation starts
|
|
class Idler implements MessageQueue.IdleHandler {
|
|
public Idler() {
|
|
super();
|
|
}
|
|
|
|
public final boolean queueIdle() {
|
|
if (gtView != null) {
|
|
gtView.startAnimating();
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|