| /* |
| * 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; |
| } |
| } |
| } |