Home | History | Annotate | Download | only in globaltime
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.globaltime;
     18 
     19 import java.io.ByteArrayInputStream;
     20 import java.io.FileNotFoundException;
     21 import java.io.IOException;
     22 import java.io.InputStream;
     23 import java.util.ArrayList;
     24 import java.util.Calendar;
     25 import java.util.List;
     26 import java.util.Locale;
     27 import java.util.TimeZone;
     28 
     29 import javax.microedition.khronos.egl.*;
     30 import javax.microedition.khronos.opengles.*;
     31 
     32 import android.app.Activity;
     33 import android.content.Context;
     34 import android.content.res.AssetManager;
     35 import android.graphics.Canvas;
     36 import android.opengl.Object3D;
     37 import android.os.Bundle;
     38 import android.os.Handler;
     39 import android.os.Looper;
     40 import android.os.Message;
     41 import android.os.MessageQueue;
     42 import android.util.Log;
     43 import android.view.KeyEvent;
     44 import android.view.MotionEvent;
     45 import android.view.SurfaceHolder;
     46 import android.view.SurfaceView;
     47 import android.view.animation.AccelerateDecelerateInterpolator;
     48 import android.view.animation.DecelerateInterpolator;
     49 import android.view.animation.Interpolator;
     50 
     51 /**
     52  * The main View of the GlobalTime Activity.
     53  */
     54 class GTView extends SurfaceView implements SurfaceHolder.Callback {
     55 
     56     /**
     57      * A TimeZone object used to compute the current UTC time.
     58      */
     59     private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("utc");
     60 
     61     /**
     62      * The Sun's color is close to that of a 5780K blackbody.
     63      */
     64     private static final float[] SUNLIGHT_COLOR = {
     65         1.0f, 0.9375f, 0.91015625f, 1.0f
     66     };
     67 
     68     /**
     69      * The inclination of the earth relative to the plane of the ecliptic
     70      * is 23.45 degrees.
     71      */
     72     private static final float EARTH_INCLINATION = 23.45f * Shape.PI / 180.0f;
     73 
     74     /** Seconds in a day */
     75     private static final int SECONDS_PER_DAY = 24 * 60 * 60;
     76 
     77     /** Flag for the depth test */
     78     private static final boolean PERFORM_DEPTH_TEST= false;
     79 
     80     /** Use raw time zone offsets, disregarding "summer time."  If false,
     81      * current offsets will be used, which requires a much longer startup time
     82      * in order to sort the city database.
     83      */
     84     private static final boolean USE_RAW_OFFSETS = true;
     85 
     86     /**
     87      * The earth's atmosphere.
     88      */
     89     private static final Annulus ATMOSPHERE =
     90         new Annulus(0.0f, 0.0f, 1.75f, 0.9f, 1.08f, 0.4f, 0.4f, 0.8f, 0.0f,
     91             0.0f, 0.0f, 0.0f, 1.0f, 50);
     92 
     93     /**
     94      * The tesselation of the earth by latitude.
     95      */
     96     private static final int SPHERE_LATITUDES = 25;
     97 
     98     /**
     99      * The tesselation of the earth by longitude.
    100      */
    101     private static int SPHERE_LONGITUDES = 25;
    102 
    103     /**
    104      * A flattened version of the earth.  The normals are computed identically
    105      * to those of the round earth, allowing the day/night lighting to be
    106      * applied to the flattened surface.
    107      */
    108     private static Sphere worldFlat = new LatLongSphere(0.0f, 0.0f, 0.0f, 1.0f,
    109         SPHERE_LATITUDES, SPHERE_LONGITUDES,
    110         0.0f, 360.0f, true, true, false, true);
    111 
    112     /**
    113      * The earth.
    114      */
    115     private Object3D mWorld;
    116 
    117     /**
    118      * Geometry of the city lights
    119      */
    120     private PointCloud mLights;
    121 
    122     /**
    123      * True if the activiy has been initialized.
    124      */
    125     boolean mInitialized = false;
    126 
    127     /**
    128      * True if we're in alphabetic entry mode.
    129      */
    130     private boolean mAlphaKeySet = false;
    131 
    132     private EGLContext mEGLContext;
    133     private EGLSurface mEGLSurface;
    134     private EGLDisplay mEGLDisplay;
    135     private EGLConfig  mEGLConfig;
    136     GLView  mGLView;
    137 
    138     // Rotation and tilt of the Earth
    139     private float mRotAngle = 0.0f;
    140     private float mTiltAngle = 0.0f;
    141 
    142     // Rotational velocity of the orbiting viewer
    143     private float mRotVelocity = 1.0f;
    144 
    145     // Rotation of the flat view
    146     private float mWrapX =  0.0f;
    147     private float  mWrapVelocity =  0.0f;
    148     private float mWrapVelocityFactor =  0.01f;
    149 
    150     // Toggle switches
    151     private boolean mDisplayAtmosphere = true;
    152     private boolean mDisplayClock = false;
    153     private boolean mClockShowing = false;
    154     private boolean mDisplayLights = false;
    155     private boolean mDisplayWorld = true;
    156     private boolean mDisplayWorldFlat = false;
    157     private boolean mSmoothShading = true;
    158 
    159     // City search string
    160     private String mCityName = "";
    161 
    162     // List of all cities
    163     private List<City> mClockCities;
    164 
    165     // List of cities matching a user-supplied prefix
    166     private List<City> mCityNameMatches = new ArrayList<City>();
    167 
    168     private List<City> mCities;
    169 
    170     // Start time for clock fade animation
    171     private long mClockFadeTime;
    172 
    173     // Interpolator for clock fade animation
    174     private Interpolator mClockSizeInterpolator =
    175         new DecelerateInterpolator(1.0f);
    176 
    177     // Index of current clock
    178     private int mCityIndex;
    179 
    180     // Current clock
    181     private Clock mClock;
    182 
    183     // City-to-city flight animation parameters
    184     private boolean mFlyToCity = false;
    185     private long mCityFlyStartTime;
    186     private float mCityFlightTime;
    187     private float mRotAngleStart, mRotAngleDest;
    188     private float mTiltAngleStart, mTiltAngleDest;
    189 
    190     // Interpolator for flight motion animation
    191     private Interpolator mFlyToCityInterpolator =
    192         new AccelerateDecelerateInterpolator();
    193 
    194     private static int sNumLights;
    195     private static int[] sLightCoords;
    196 
    197     //     static Map<Float,int[]> cityCoords = new HashMap<Float,int[]>();
    198 
    199     // Arrays for GL calls
    200     private float[] mClipPlaneEquation = new float[4];
    201     private float[] mLightDir = new float[4];
    202 
    203     // Calendar for computing the Sun's position
    204     Calendar mSunCal = Calendar.getInstance(UTC_TIME_ZONE);
    205 
    206     // Triangles drawn per frame
    207     private int mNumTriangles;
    208 
    209     private long startTime;
    210 
    211     private static final int MOTION_NONE = 0;
    212     private static final int MOTION_X = 1;
    213     private static final int MOTION_Y = 2;
    214 
    215     private static final int MIN_MANHATTAN_DISTANCE = 20;
    216     private static final float ROTATION_FACTOR = 1.0f / 30.0f;
    217     private static final float TILT_FACTOR = 0.35f;
    218 
    219     // Touchscreen support
    220     private float mMotionStartX;
    221     private float mMotionStartY;
    222     private float mMotionStartRotVelocity;
    223     private float mMotionStartTiltAngle;
    224     private int mMotionDirection;
    225 
    226     private boolean mPaused = true;
    227     private boolean mHaveSurface = false;
    228     private boolean mStartAnimating = false;
    229 
    230     public void surfaceCreated(SurfaceHolder holder) {
    231         mHaveSurface = true;
    232         startEGL();
    233     }
    234 
    235     public void surfaceDestroyed(SurfaceHolder holder) {
    236         mHaveSurface = false;
    237         stopEGL();
    238     }
    239 
    240     public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
    241         // nothing to do
    242     }
    243 
    244     /**
    245      * Set up the view.
    246      *
    247      * @param context the Context
    248      * @param am an AssetManager to retrieve the city database from
    249      */
    250     public GTView(Context context) {
    251         super(context);
    252 
    253         getHolder().addCallback(this);
    254         getHolder().setType(SurfaceHolder.SURFACE_TYPE_GPU);
    255 
    256         startTime = System.currentTimeMillis();
    257 
    258         mClock = new Clock();
    259 
    260         startEGL();
    261 
    262         setFocusable(true);
    263         setFocusableInTouchMode(true);
    264         requestFocus();
    265     }
    266 
    267     /**
    268      * Creates an egl context. If the state of the activity is right, also
    269      * creates the egl surface. Otherwise the surface will be created in a
    270      * future call to createEGLSurface().
    271      */
    272     private void startEGL() {
    273         EGL10 egl = (EGL10)EGLContext.getEGL();
    274 
    275         if (mEGLContext == null) {
    276             EGLDisplay dpy = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
    277             int[] version = new int[2];
    278             egl.eglInitialize(dpy, version);
    279             int[] configSpec = {
    280                     EGL10.EGL_DEPTH_SIZE,   16,
    281                     EGL10.EGL_NONE
    282             };
    283             EGLConfig[] configs = new EGLConfig[1];
    284             int[] num_config = new int[1];
    285             egl.eglChooseConfig(dpy, configSpec, configs, 1, num_config);
    286             mEGLConfig = configs[0];
    287 
    288             mEGLContext = egl.eglCreateContext(dpy, mEGLConfig,
    289                     EGL10.EGL_NO_CONTEXT, null);
    290             mEGLDisplay = dpy;
    291 
    292             AssetManager am = mContext.getAssets();
    293             try {
    294                 loadAssets(am);
    295             } catch (IOException ioe) {
    296                 ioe.printStackTrace();
    297                 throw new RuntimeException(ioe);
    298             } catch (ArrayIndexOutOfBoundsException aioobe) {
    299                 aioobe.printStackTrace();
    300                 throw new RuntimeException(aioobe);
    301             }
    302         }
    303 
    304         if (mEGLSurface == null && !mPaused && mHaveSurface) {
    305             mEGLSurface = egl.eglCreateWindowSurface(mEGLDisplay, mEGLConfig,
    306                     this, null);
    307             egl.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface,
    308                     mEGLContext);
    309             mInitialized = false;
    310             if (mStartAnimating) {
    311                 startAnimating();
    312                 mStartAnimating = false;
    313             }
    314         }
    315     }
    316 
    317     /**
    318      * Destroys the egl context. If an egl surface has been created, it is
    319      * destroyed as well.
    320      */
    321     private void stopEGL() {
    322         EGL10 egl = (EGL10)EGLContext.getEGL();
    323         if (mEGLSurface != null) {
    324             egl.eglMakeCurrent(mEGLDisplay,
    325                     egl.EGL_NO_SURFACE, egl.EGL_NO_SURFACE, egl.EGL_NO_CONTEXT);
    326             egl.eglDestroySurface(mEGLDisplay, mEGLSurface);
    327             mEGLSurface = null;
    328         }
    329 
    330         if (mEGLContext != null) {
    331             egl.eglDestroyContext(mEGLDisplay, mEGLContext);
    332             egl.eglTerminate(mEGLDisplay);
    333             mEGLContext = null;
    334             mEGLDisplay = null;
    335             mEGLConfig = null;
    336         }
    337     }
    338 
    339     public void onPause() {
    340         mPaused = true;
    341         stopAnimating();
    342         stopEGL();
    343     }
    344 
    345     public void onResume() {
    346         mPaused = false;
    347         startEGL();
    348     }
    349 
    350     public void destroy() {
    351         stopAnimating();
    352         stopEGL();
    353     }
    354 
    355     /**
    356      * Begin animation.
    357      */
    358     public void startAnimating() {
    359         if (mEGLSurface == null) {
    360             mStartAnimating = true; // will start when egl surface is created
    361         } else {
    362             mHandler.sendEmptyMessage(INVALIDATE);
    363         }
    364     }
    365 
    366     /**
    367      * Quit animation.
    368      */
    369     public void stopAnimating() {
    370         mHandler.removeMessages(INVALIDATE);
    371     }
    372 
    373     /**
    374      * Read a two-byte integer from the input stream.
    375      */
    376     private int readInt16(InputStream is) throws IOException {
    377         int lo = is.read();
    378         int hi = is.read();
    379         return (hi << 8) | lo;
    380     }
    381 
    382     /**
    383      * Returns the offset from UTC for the given city.  If USE_RAW_OFFSETS
    384      * is true, summer/daylight savings is ignored.
    385      */
    386     private static float getOffset(City c) {
    387         return USE_RAW_OFFSETS ? c.getRawOffset() : c.getOffset();
    388     }
    389 
    390     private InputStream cache(InputStream is) throws IOException {
    391         int nbytes = is.available();
    392         byte[] data = new byte[nbytes];
    393         int nread = 0;
    394         while (nread < nbytes) {
    395             nread += is.read(data, nread, nbytes - nread);
    396         }
    397         return new ByteArrayInputStream(data);
    398     }
    399 
    400     /**
    401      * Load the city and lights databases.
    402      *
    403      * @param am the AssetManager to load from.
    404      */
    405     private void loadAssets(final AssetManager am) throws IOException {
    406         Locale locale = Locale.getDefault();
    407         String language = locale.getLanguage();
    408         String country = locale.getCountry();
    409 
    410         InputStream cis = null;
    411         try {
    412             // Look for (e.g.) cities_fr_FR.dat or cities_fr_CA.dat
    413             cis = am.open("cities_" + language + "_" + country + ".dat");
    414         } catch (FileNotFoundException e1) {
    415             try {
    416                 // Look for (e.g.) cities_fr.dat or cities_fr.dat
    417                 cis = am.open("cities_" + language + ".dat");
    418             } catch (FileNotFoundException e2) {
    419                 try {
    420                     // Use English city names by default
    421                     cis = am.open("cities_en.dat");
    422                 } catch (FileNotFoundException e3) {
    423                     throw e3;
    424                 }
    425             }
    426         }
    427 
    428         cis = cache(cis);
    429         City.loadCities(cis);
    430         City[] cities;
    431         if (USE_RAW_OFFSETS) {
    432             cities = City.getCitiesByRawOffset();
    433         } else {
    434             cities = City.getCitiesByOffset();
    435         }
    436 
    437         mClockCities = new ArrayList<City>(cities.length);
    438         for (int i = 0; i < cities.length; i++) {
    439             mClockCities.add(cities[i]);
    440         }
    441         mCities = mClockCities;
    442         mCityIndex = 0;
    443 
    444         this.mWorld = new Object3D() {
    445                 @Override
    446                 public InputStream readFile(String filename)
    447                     throws IOException {
    448                     return cache(am.open(filename));
    449                 }
    450             };
    451 
    452         mWorld.load("world.gles");
    453 
    454         // lights.dat has the following format.  All integers
    455         // are 16 bits, low byte first.
    456         //
    457         // width
    458         // height
    459         // N [# of lights]
    460         // light 0 X [in the range 0 to (width - 1)]
    461         // light 0 Y ]in the range 0 to (height - 1)]
    462         // light 1 X [in the range 0 to (width - 1)]
    463         // light 1 Y ]in the range 0 to (height - 1)]
    464         // ...
    465         // light (N - 1) X [in the range 0 to (width - 1)]
    466         // light (N - 1) Y ]in the range 0 to (height - 1)]
    467         //
    468         // For a larger number of lights, it could make more
    469         // sense to store the light positions in a bitmap
    470         // and extract them manually
    471         InputStream lis = am.open("lights.dat");
    472         lis = cache(lis);
    473 
    474         int lightWidth = readInt16(lis);
    475         int lightHeight = readInt16(lis);
    476         sNumLights = readInt16(lis);
    477         sLightCoords = new int[3 * sNumLights];
    478 
    479         int lidx = 0;
    480         float lightRadius = 1.009f;
    481         float lightScale = 65536.0f * lightRadius;
    482 
    483         float[] cosTheta = new float[lightWidth];
    484         float[] sinTheta = new float[lightWidth];
    485         float twoPi = (float) (2.0 * Math.PI);
    486         float scaleW = twoPi / lightWidth;
    487         for (int i = 0; i < lightWidth; i++) {
    488             float theta = twoPi - i * scaleW;
    489             cosTheta[i] = (float)Math.cos(theta);
    490             sinTheta[i] = (float)Math.sin(theta);
    491         }
    492 
    493         float[] cosPhi = new float[lightHeight];
    494         float[] sinPhi = new float[lightHeight];
    495         float scaleH = (float) (Math.PI / lightHeight);
    496         for (int j = 0; j < lightHeight; j++) {
    497             float phi = j * scaleH;
    498             cosPhi[j] = (float)Math.cos(phi);
    499             sinPhi[j] = (float)Math.sin(phi);
    500         }
    501 
    502         int nbytes = 4 * sNumLights;
    503         byte[] ilights = new byte[nbytes];
    504         int nread = 0;
    505         while (nread < nbytes) {
    506             nread += lis.read(ilights, nread, nbytes - nread);
    507         }
    508 
    509         int idx = 0;
    510         for (int i = 0; i < sNumLights; i++) {
    511             int lx = (((ilights[idx + 1] & 0xff) << 8) |
    512                        (ilights[idx    ] & 0xff));
    513             int ly = (((ilights[idx + 3] & 0xff) << 8) |
    514                        (ilights[idx + 2] & 0xff));
    515             idx += 4;
    516 
    517             float sin = sinPhi[ly];
    518             float x = cosTheta[lx]*sin;
    519             float y = cosPhi[ly];
    520             float z = sinTheta[lx]*sin;
    521 
    522             sLightCoords[lidx++] = (int) (x * lightScale);
    523             sLightCoords[lidx++] = (int) (y * lightScale);
    524             sLightCoords[lidx++] = (int) (z * lightScale);
    525         }
    526         mLights = new PointCloud(sLightCoords);
    527     }
    528 
    529     /**
    530      * Returns true if two time zone offsets are equal.  We assume distinct
    531      * time zone offsets will differ by at least a few minutes.
    532      */
    533     private boolean tzEqual(float o1, float o2) {
    534         return Math.abs(o1 - o2) < 0.001;
    535     }
    536 
    537     /**
    538      * Move to a different time zone.
    539      *
    540      * @param incr The increment between the current and future time zones.
    541      */
    542     private void shiftTimeZone(int incr) {
    543         // If only 1 city in the current set, there's nowhere to go
    544         if (mCities.size() <= 1) {
    545             return;
    546         }
    547 
    548         float offset = getOffset(mCities.get(mCityIndex));
    549         do {
    550             mCityIndex = (mCityIndex + mCities.size() + incr) % mCities.size();
    551         } while (tzEqual(getOffset(mCities.get(mCityIndex)), offset));
    552 
    553         offset = getOffset(mCities.get(mCityIndex));
    554         locateCity(true, offset);
    555         goToCity();
    556     }
    557 
    558     /**
    559      * Returns true if there is another city within the current time zone
    560      * that is the given increment away from the current city.
    561      *
    562      * @param incr the increment, +1 or -1
    563      * @return
    564      */
    565     private boolean atEndOfTimeZone(int incr) {
    566         if (mCities.size() <= 1) {
    567             return true;
    568         }
    569 
    570         float offset = getOffset(mCities.get(mCityIndex));
    571         int nindex = (mCityIndex + mCities.size() + incr) % mCities.size();
    572         if (tzEqual(getOffset(mCities.get(nindex)), offset)) {
    573             return false;
    574         }
    575         return true;
    576     }
    577 
    578     /**
    579      * Shifts cities within the current time zone.
    580      *
    581      * @param incr the increment, +1 or -1
    582      */
    583     private void shiftWithinTimeZone(int incr) {
    584         float offset = getOffset(mCities.get(mCityIndex));
    585         int nindex = (mCityIndex + mCities.size() + incr) % mCities.size();
    586         if (tzEqual(getOffset(mCities.get(nindex)), offset)) {
    587             mCityIndex = nindex;
    588             goToCity();
    589         }
    590     }
    591 
    592     /**
    593      * Returns true if the city name matches the given prefix, ignoring spaces.
    594      */
    595     private boolean nameMatches(City city, String prefix) {
    596         String cityName = city.getName().replaceAll("[ ]", "");
    597         return prefix.regionMatches(true, 0,
    598                                     cityName, 0,
    599                                     prefix.length());
    600     }
    601 
    602     /**
    603      * Returns true if there are cities matching the given name prefix.
    604      */
    605     private boolean hasMatches(String prefix) {
    606         for (int i = 0; i < mClockCities.size(); i++) {
    607             City city = mClockCities.get(i);
    608             if (nameMatches(city, prefix)) {
    609                 return true;
    610             }
    611         }
    612 
    613         return false;
    614     }
    615 
    616     /**
    617      * Shifts to the nearest city that matches the new prefix.
    618      */
    619     private void shiftByName() {
    620         // Attempt to keep current city if it matches
    621         City finalCity = null;
    622         City currCity = mCities.get(mCityIndex);
    623         if (nameMatches(currCity, mCityName)) {
    624             finalCity = currCity;
    625         }
    626 
    627         mCityNameMatches.clear();
    628         for (int i = 0; i < mClockCities.size(); i++) {
    629             City city = mClockCities.get(i);
    630             if (nameMatches(city, mCityName)) {
    631                 mCityNameMatches.add(city);
    632             }
    633         }
    634 
    635         mCities = mCityNameMatches;
    636 
    637         if (finalCity != null) {
    638             for (int i = 0; i < mCityNameMatches.size(); i++) {
    639                 if (mCityNameMatches.get(i) == finalCity) {
    640                     mCityIndex = i;
    641                     break;
    642                 }
    643             }
    644         } else {
    645             // Find the closest matching city
    646             locateCity(false, 0.0f);
    647         }
    648         goToCity();
    649     }
    650 
    651     /**
    652      * Increases or decreases the rotational speed of the earth.
    653      */
    654     private void incrementRotationalVelocity(float incr) {
    655         if (mDisplayWorldFlat) {
    656             mWrapVelocity -= incr;
    657         } else {
    658             mRotVelocity -= incr;
    659         }
    660     }
    661 
    662     /**
    663      * Clears the current matching prefix, while keeping the focus on
    664      * the current city.
    665      */
    666     private void clearCityMatches() {
    667         // Determine the global city index that matches the current city
    668         if (mCityNameMatches.size() > 0) {
    669             City city = mCityNameMatches.get(mCityIndex);
    670             for (int i = 0; i < mClockCities.size(); i++) {
    671                 City ncity = mClockCities.get(i);
    672                 if (city.equals(ncity)) {
    673                     mCityIndex = i;
    674                     break;
    675                 }
    676             }
    677         }
    678 
    679         mCityName = "";
    680         mCityNameMatches.clear();
    681         mCities = mClockCities;
    682         goToCity();
    683     }
    684 
    685     /**
    686      * Fade the clock in or out.
    687      */
    688     private void enableClock(boolean enabled) {
    689         mClockFadeTime = System.currentTimeMillis();
    690         mDisplayClock = enabled;
    691         mClockShowing = true;
    692         mAlphaKeySet = enabled;
    693         if (enabled) {
    694             // Find the closest matching city
    695             locateCity(false, 0.0f);
    696         }
    697         clearCityMatches();
    698     }
    699 
    700     /**
    701      * Use the touchscreen to alter the rotational velocity or the
    702      * tilt of the earth.
    703      */
    704     @Override public boolean onTouchEvent(MotionEvent event) {
    705         switch (event.getAction()) {
    706             case MotionEvent.ACTION_DOWN:
    707                 mMotionStartX = event.getX();
    708                 mMotionStartY = event.getY();
    709                 mMotionStartRotVelocity = mDisplayWorldFlat ?
    710                     mWrapVelocity : mRotVelocity;
    711                 mMotionStartTiltAngle = mTiltAngle;
    712 
    713                 // Stop the rotation
    714                 if (mDisplayWorldFlat) {
    715                     mWrapVelocity = 0.0f;
    716                 } else {
    717                     mRotVelocity = 0.0f;
    718                 }
    719                 mMotionDirection = MOTION_NONE;
    720                 break;
    721 
    722             case MotionEvent.ACTION_MOVE:
    723                 // Disregard motion events when the clock is displayed
    724                 float dx = event.getX() - mMotionStartX;
    725                 float dy = event.getY() - mMotionStartY;
    726                 float delx = Math.abs(dx);
    727                 float dely = Math.abs(dy);
    728 
    729                 // Determine the direction of motion (major axis)
    730                 // Once if has been determined, it's locked in until
    731                 // we receive ACTION_UP or ACTION_CANCEL
    732                 if ((mMotionDirection == MOTION_NONE) &&
    733                     (delx + dely > MIN_MANHATTAN_DISTANCE)) {
    734                     if (delx > dely) {
    735                         mMotionDirection = MOTION_X;
    736                     } else {
    737                         mMotionDirection = MOTION_Y;
    738                     }
    739                 }
    740 
    741                 // If the clock is displayed, don't actually rotate or tilt;
    742                 // just use mMotionDirection to record whether motion occurred
    743                 if (!mDisplayClock) {
    744                     if (mMotionDirection == MOTION_X) {
    745                         if (mDisplayWorldFlat) {
    746                             mWrapVelocity = mMotionStartRotVelocity +
    747                                 dx * ROTATION_FACTOR;
    748                         } else {
    749                             mRotVelocity = mMotionStartRotVelocity +
    750                                 dx * ROTATION_FACTOR;
    751                         }
    752                         mClock.setCity(null);
    753                     } else if (mMotionDirection == MOTION_Y &&
    754                         !mDisplayWorldFlat) {
    755                         mTiltAngle = mMotionStartTiltAngle + dy * TILT_FACTOR;
    756                         if (mTiltAngle < -90.0f) {
    757                             mTiltAngle = -90.0f;
    758                         }
    759                         if (mTiltAngle > 90.0f) {
    760                             mTiltAngle = 90.0f;
    761                         }
    762                         mClock.setCity(null);
    763                     }
    764                 }
    765                 break;
    766 
    767             case MotionEvent.ACTION_UP:
    768                 mMotionDirection = MOTION_NONE;
    769                 break;
    770 
    771             case MotionEvent.ACTION_CANCEL:
    772                 mTiltAngle = mMotionStartTiltAngle;
    773                 if (mDisplayWorldFlat) {
    774                     mWrapVelocity = mMotionStartRotVelocity;
    775                 } else {
    776                     mRotVelocity = mMotionStartRotVelocity;
    777                 }
    778                 mMotionDirection = MOTION_NONE;
    779                 break;
    780         }
    781         return true;
    782     }
    783 
    784     @Override public boolean onKeyDown(int keyCode, KeyEvent event) {
    785         if (mInitialized && mGLView.processKey(keyCode)) {
    786             boolean drawing = (mClockShowing || mGLView.hasMessages());
    787             this.setWillNotDraw(!drawing);
    788             return true;
    789         }
    790 
    791         boolean handled = false;
    792 
    793         // If we're not in alphabetical entry mode, convert letters
    794         // to their digit equivalents
    795         if (!mAlphaKeySet) {
    796             char numChar = event.getNumber();
    797             if (numChar >= '0' && numChar <= '9') {
    798                 keyCode = KeyEvent.KEYCODE_0 + (numChar - '0');
    799             }
    800         }
    801 
    802         switch (keyCode) {
    803         // The 'space' key toggles the clock
    804         case KeyEvent.KEYCODE_SPACE:
    805             mAlphaKeySet = !mAlphaKeySet;
    806             enableClock(mAlphaKeySet);
    807             handled = true;
    808             break;
    809 
    810         // The 'left' and 'right' buttons shift time zones if the clock is
    811         // displayed, otherwise they alters the rotational speed of the earthh
    812         case KeyEvent.KEYCODE_DPAD_LEFT:
    813             if (mDisplayClock) {
    814                 shiftTimeZone(-1);
    815             } else {
    816                 mClock.setCity(null);
    817                 incrementRotationalVelocity(1.0f);
    818             }
    819             handled = true;
    820             break;
    821 
    822         case KeyEvent.KEYCODE_DPAD_RIGHT:
    823             if (mDisplayClock) {
    824                 shiftTimeZone(1);
    825             } else {
    826                 mClock.setCity(null);
    827                 incrementRotationalVelocity(-1.0f);
    828             }
    829             handled = true;
    830             break;
    831 
    832         // The 'up' and 'down' buttons shift cities within a time zone if the
    833         // clock is displayed, otherwise they tilt the earth
    834         case KeyEvent.KEYCODE_DPAD_UP:
    835             if (mDisplayClock) {
    836                 shiftWithinTimeZone(-1);
    837             } else {
    838                 mClock.setCity(null);
    839                 if (!mDisplayWorldFlat) {
    840                     mTiltAngle += 360.0f / 48.0f;
    841                 }
    842             }
    843             handled = true;
    844             break;
    845 
    846         case KeyEvent.KEYCODE_DPAD_DOWN:
    847             if (mDisplayClock) {
    848                 shiftWithinTimeZone(1);
    849             } else {
    850                 mClock.setCity(null);
    851                 if (!mDisplayWorldFlat) {
    852                     mTiltAngle -= 360.0f / 48.0f;
    853                 }
    854             }
    855             handled = true;
    856             break;
    857 
    858         // The center key stops the earth's rotation, then toggles between the
    859         // round and flat views of the earth
    860         case KeyEvent.KEYCODE_DPAD_CENTER:
    861             if ((!mDisplayWorldFlat && mRotVelocity == 0.0f) ||
    862                 (mDisplayWorldFlat && mWrapVelocity == 0.0f)) {
    863                 mDisplayWorldFlat = !mDisplayWorldFlat;
    864             } else {
    865                 if (mDisplayWorldFlat) {
    866                     mWrapVelocity = 0.0f;
    867                 } else {
    868                     mRotVelocity = 0.0f;
    869                 }
    870             }
    871             handled = true;
    872             break;
    873 
    874         // The 'L' key toggles the city lights
    875         case KeyEvent.KEYCODE_L:
    876             if (!mAlphaKeySet && !mDisplayWorldFlat) {
    877                 mDisplayLights = !mDisplayLights;
    878                 handled = true;
    879             }
    880             break;
    881 
    882 
    883         // The 'W' key toggles the earth (just for fun)
    884         case KeyEvent.KEYCODE_W:
    885             if (!mAlphaKeySet && !mDisplayWorldFlat) {
    886                 mDisplayWorld = !mDisplayWorld;
    887                 handled = true;
    888             }
    889             break;
    890 
    891         // The 'A' key toggles the atmosphere
    892         case KeyEvent.KEYCODE_A:
    893             if (!mAlphaKeySet && !mDisplayWorldFlat) {
    894                 mDisplayAtmosphere = !mDisplayAtmosphere;
    895                 handled = true;
    896             }
    897             break;
    898 
    899         // The '2' key zooms out
    900         case KeyEvent.KEYCODE_2:
    901             if (!mAlphaKeySet && !mDisplayWorldFlat) {
    902                 mGLView.zoom(-2);
    903                 handled = true;
    904             }
    905             break;
    906 
    907         // The '8' key zooms in
    908         case KeyEvent.KEYCODE_8:
    909             if (!mAlphaKeySet && !mDisplayWorldFlat) {
    910                 mGLView.zoom(2);
    911                 handled = true;
    912             }
    913             break;
    914         }
    915 
    916         // Handle letters in city names
    917         if (!handled && mAlphaKeySet) {
    918             switch (keyCode) {
    919             // Add a letter to the city name prefix
    920             case KeyEvent.KEYCODE_A:
    921             case KeyEvent.KEYCODE_B:
    922             case KeyEvent.KEYCODE_C:
    923             case KeyEvent.KEYCODE_D:
    924             case KeyEvent.KEYCODE_E:
    925             case KeyEvent.KEYCODE_F:
    926             case KeyEvent.KEYCODE_G:
    927             case KeyEvent.KEYCODE_H:
    928             case KeyEvent.KEYCODE_I:
    929             case KeyEvent.KEYCODE_J:
    930             case KeyEvent.KEYCODE_K:
    931             case KeyEvent.KEYCODE_L:
    932             case KeyEvent.KEYCODE_M:
    933             case KeyEvent.KEYCODE_N:
    934             case KeyEvent.KEYCODE_O:
    935             case KeyEvent.KEYCODE_P:
    936             case KeyEvent.KEYCODE_Q:
    937             case KeyEvent.KEYCODE_R:
    938             case KeyEvent.KEYCODE_S:
    939             case KeyEvent.KEYCODE_T:
    940             case KeyEvent.KEYCODE_U:
    941             case KeyEvent.KEYCODE_V:
    942             case KeyEvent.KEYCODE_W:
    943             case KeyEvent.KEYCODE_X:
    944             case KeyEvent.KEYCODE_Y:
    945             case KeyEvent.KEYCODE_Z:
    946                 char c = (char)(keyCode - KeyEvent.KEYCODE_A + 'A');
    947                 if (hasMatches(mCityName + c)) {
    948                     mCityName += c;
    949                     shiftByName();
    950                 }
    951                 handled = true;
    952                 break;
    953 
    954             // Remove a letter from the city name prefix
    955             case KeyEvent.KEYCODE_DEL:
    956                 if (mCityName.length() > 0) {
    957                     mCityName = mCityName.substring(0, mCityName.length() - 1);
    958                     shiftByName();
    959                 } else {
    960                     clearCityMatches();
    961                 }
    962                 handled = true;
    963                 break;
    964 
    965             // Clear the city name prefix
    966             case KeyEvent.KEYCODE_ENTER:
    967                 clearCityMatches();
    968                 handled = true;
    969                 break;
    970             }
    971         }
    972 
    973         boolean drawing = (mClockShowing ||
    974             ((mGLView != null) && (mGLView.hasMessages())));
    975         this.setWillNotDraw(!drawing);
    976 
    977         // Let the system handle other keypresses
    978         if (!handled) {
    979             return super.onKeyDown(keyCode, event);
    980         }
    981         return true;
    982     }
    983 
    984     /**
    985      * Initialize OpenGL ES drawing.
    986      */
    987     private synchronized void init(GL10 gl) {
    988         mGLView = new GLView();
    989         mGLView.setNearFrustum(5.0f);
    990         mGLView.setFarFrustum(50.0f);
    991         mGLView.setLightModelAmbientIntensity(0.225f);
    992         mGLView.setAmbientIntensity(0.0f);
    993         mGLView.setDiffuseIntensity(1.5f);
    994         mGLView.setDiffuseColor(SUNLIGHT_COLOR);
    995         mGLView.setSpecularIntensity(0.0f);
    996         mGLView.setSpecularColor(SUNLIGHT_COLOR);
    997 
    998         if (PERFORM_DEPTH_TEST) {
    999             gl.glEnable(GL10.GL_DEPTH_TEST);
   1000         }
   1001         gl.glDisable(GL10.GL_SCISSOR_TEST);
   1002         gl.glClearColor(0, 0, 0, 1);
   1003         gl.glHint(GL10.GL_POINT_SMOOTH_HINT, GL10.GL_NICEST);
   1004 
   1005         mInitialized = true;
   1006     }
   1007 
   1008     /**
   1009      * Computes the vector from the center of the earth to the sun for a
   1010      * particular moment in time.
   1011      */
   1012     private void computeSunDirection() {
   1013         mSunCal.setTimeInMillis(System.currentTimeMillis());
   1014         int day = mSunCal.get(Calendar.DAY_OF_YEAR);
   1015         int seconds = 3600 * mSunCal.get(Calendar.HOUR_OF_DAY) +
   1016             60 * mSunCal.get(Calendar.MINUTE) + mSunCal.get(Calendar.SECOND);
   1017         day += (float) seconds / SECONDS_PER_DAY;
   1018 
   1019         // Approximate declination of the sun, changes sinusoidally
   1020         // during the year.  The winter solstice occurs 10 days before
   1021         // the start of the year.
   1022         float decl = (float) (EARTH_INCLINATION *
   1023             Math.cos(Shape.TWO_PI * (day + 10) / 365.0));
   1024 
   1025         // Subsolar latitude, convert from (-PI/2, PI/2) -> (0, PI) form
   1026         float phi = decl + Shape.PI_OVER_TWO;
   1027         // Subsolar longitude
   1028         float theta = Shape.TWO_PI * seconds / SECONDS_PER_DAY;
   1029 
   1030         float sinPhi = (float) Math.sin(phi);
   1031         float cosPhi = (float) Math.cos(phi);
   1032         float sinTheta = (float) Math.sin(theta);
   1033         float cosTheta = (float) Math.cos(theta);
   1034 
   1035         // Convert from polar to rectangular coordinates
   1036         float x = cosTheta * sinPhi;
   1037         float y = cosPhi;
   1038         float z = sinTheta * sinPhi;
   1039 
   1040         // Directional light -> w == 0
   1041         mLightDir[0] = x;
   1042         mLightDir[1] = y;
   1043         mLightDir[2] = z;
   1044         mLightDir[3] = 0.0f;
   1045     }
   1046 
   1047     /**
   1048      * Computes the approximate spherical distance between two
   1049      * (latitude, longitude) coordinates.
   1050      */
   1051     private float distance(float lat1, float lon1,
   1052                            float lat2, float lon2) {
   1053         lat1 *= Shape.DEGREES_TO_RADIANS;
   1054         lat2 *= Shape.DEGREES_TO_RADIANS;
   1055         lon1 *= Shape.DEGREES_TO_RADIANS;
   1056         lon2 *= Shape.DEGREES_TO_RADIANS;
   1057 
   1058         float r = 6371.0f; // Earth's radius in km
   1059         float dlat = lat2 - lat1;
   1060         float dlon = lon2 - lon1;
   1061         double sinlat2 = Math.sin(dlat / 2.0f);
   1062         sinlat2 *= sinlat2;
   1063         double sinlon2 = Math.sin(dlon / 2.0f);
   1064         sinlon2 *= sinlon2;
   1065 
   1066         double a = sinlat2 + Math.cos(lat1) * Math.cos(lat2) * sinlon2;
   1067         double c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
   1068         return (float) (r * c);
   1069     }
   1070 
   1071     /**
   1072      * Locates the closest city to the currently displayed center point,
   1073      * optionally restricting the search to cities within a given time zone.
   1074      */
   1075     private void locateCity(boolean useOffset, float offset) {
   1076         float mindist = Float.MAX_VALUE;
   1077         int minidx = -1;
   1078         for (int i = 0; i < mCities.size(); i++) {
   1079             City city = mCities.get(i);
   1080             if (useOffset && !tzEqual(getOffset(city), offset)) {
   1081                 continue;
   1082             }
   1083             float dist = distance(city.getLatitude(), city.getLongitude(),
   1084                 mTiltAngle, mRotAngle - 90.0f);
   1085             if (dist < mindist) {
   1086                 mindist = dist;
   1087                 minidx = i;
   1088             }
   1089         }
   1090 
   1091         mCityIndex = minidx;
   1092     }
   1093 
   1094     /**
   1095      * Animates the earth to be centered at the current city.
   1096      */
   1097     private void goToCity() {
   1098         City city = mCities.get(mCityIndex);
   1099         float dist = distance(city.getLatitude(), city.getLongitude(),
   1100             mTiltAngle, mRotAngle - 90.0f);
   1101 
   1102         mFlyToCity = true;
   1103         mCityFlyStartTime = System.currentTimeMillis();
   1104         mCityFlightTime = dist / 5.0f; // 5000 km/sec
   1105         mRotAngleStart = mRotAngle;
   1106         mRotAngleDest = city.getLongitude() + 90;
   1107 
   1108         if (mRotAngleDest - mRotAngleStart > 180.0f) {
   1109             mRotAngleDest -= 360.0f;
   1110         } else if (mRotAngleStart - mRotAngleDest > 180.0f) {
   1111             mRotAngleDest += 360.0f;
   1112         }
   1113 
   1114         mTiltAngleStart = mTiltAngle;
   1115         mTiltAngleDest = city.getLatitude();
   1116         mRotVelocity = 0.0f;
   1117     }
   1118 
   1119     /**
   1120      * Returns a linearly interpolated value between two values.
   1121      */
   1122     private float lerp(float a, float b, float lerp) {
   1123         return a + (b - a)*lerp;
   1124     }
   1125 
   1126     /**
   1127      * Draws the city lights, using a clip plane to restrict the lights
   1128      * to the night side of the earth.
   1129      */
   1130     private void drawCityLights(GL10 gl, float brightness) {
   1131         gl.glEnable(GL10.GL_POINT_SMOOTH);
   1132         gl.glDisable(GL10.GL_DEPTH_TEST);
   1133         gl.glDisable(GL10.GL_LIGHTING);
   1134         gl.glDisable(GL10.GL_DITHER);
   1135         gl.glShadeModel(GL10.GL_FLAT);
   1136         gl.glEnable(GL10.GL_BLEND);
   1137         gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
   1138         gl.glPointSize(1.0f);
   1139 
   1140         float ls = lerp(0.8f, 0.3f, brightness);
   1141         gl.glColor4f(ls * 1.0f, ls * 1.0f, ls * 0.8f, 1.0f);
   1142 
   1143         if (mDisplayWorld) {
   1144             mClipPlaneEquation[0] = -mLightDir[0];
   1145             mClipPlaneEquation[1] = -mLightDir[1];
   1146             mClipPlaneEquation[2] = -mLightDir[2];
   1147             mClipPlaneEquation[3] = 0.0f;
   1148             // Assume we have glClipPlanef() from OpenGL ES 1.1
   1149             ((GL11) gl).glClipPlanef(GL11.GL_CLIP_PLANE0,
   1150                 mClipPlaneEquation, 0);
   1151             gl.glEnable(GL11.GL_CLIP_PLANE0);
   1152         }
   1153         mLights.draw(gl);
   1154         if (mDisplayWorld) {
   1155             gl.glDisable(GL11.GL_CLIP_PLANE0);
   1156         }
   1157 
   1158         mNumTriangles += mLights.getNumTriangles()*2;
   1159     }
   1160 
   1161     /**
   1162      * Draws the atmosphere.
   1163      */
   1164     private void drawAtmosphere(GL10 gl) {
   1165         gl.glDisable(GL10.GL_LIGHTING);
   1166         gl.glDisable(GL10.GL_CULL_FACE);
   1167         gl.glDisable(GL10.GL_DITHER);
   1168         gl.glDisable(GL10.GL_DEPTH_TEST);
   1169         gl.glShadeModel(mSmoothShading ? GL10.GL_SMOOTH : GL10.GL_FLAT);
   1170 
   1171         // Draw the atmospheric layer
   1172         float tx = mGLView.getTranslateX();
   1173         float ty = mGLView.getTranslateY();
   1174         float tz = mGLView.getTranslateZ();
   1175 
   1176         gl.glMatrixMode(GL10.GL_MODELVIEW);
   1177         gl.glLoadIdentity();
   1178         gl.glTranslatef(tx, ty, tz);
   1179 
   1180         // Blend in the atmosphere a bit
   1181         gl.glEnable(GL10.GL_BLEND);
   1182         gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
   1183         ATMOSPHERE.draw(gl);
   1184 
   1185         mNumTriangles += ATMOSPHERE.getNumTriangles();
   1186     }
   1187 
   1188     /**
   1189      * Draws the world in a 2D map view.
   1190      */
   1191     private void drawWorldFlat(GL10 gl) {
   1192         gl.glDisable(GL10.GL_BLEND);
   1193         gl.glEnable(GL10.GL_DITHER);
   1194         gl.glShadeModel(mSmoothShading ? GL10.GL_SMOOTH : GL10.GL_FLAT);
   1195 
   1196         gl.glTranslatef(mWrapX - 2, 0.0f, 0.0f);
   1197         worldFlat.draw(gl);
   1198         gl.glTranslatef(2.0f, 0.0f, 0.0f);
   1199         worldFlat.draw(gl);
   1200         mNumTriangles += worldFlat.getNumTriangles() * 2;
   1201 
   1202         mWrapX += mWrapVelocity * mWrapVelocityFactor;
   1203         while (mWrapX < 0.0f) {
   1204             mWrapX += 2.0f;
   1205         }
   1206         while (mWrapX > 2.0f) {
   1207             mWrapX -= 2.0f;
   1208         }
   1209     }
   1210 
   1211     /**
   1212      * Draws the world in a 2D round view.
   1213      */
   1214     private void drawWorldRound(GL10 gl) {
   1215         gl.glDisable(GL10.GL_BLEND);
   1216         gl.glEnable(GL10.GL_DITHER);
   1217         gl.glShadeModel(mSmoothShading ? GL10.GL_SMOOTH : GL10.GL_FLAT);
   1218 
   1219         mWorld.draw(gl);
   1220         mNumTriangles += mWorld.getNumTriangles();
   1221     }
   1222 
   1223     /**
   1224      * Draws the clock.
   1225      *
   1226      * @param canvas the Canvas to draw to
   1227      * @param now the current time
   1228      * @param w the width of the screen
   1229      * @param h the height of the screen
   1230      * @param lerp controls the animation, between 0.0 and 1.0
   1231      */
   1232     private void drawClock(Canvas canvas,
   1233                            long now,
   1234                            int w, int h,
   1235                            float lerp) {
   1236         float clockAlpha = lerp(0.0f, 0.8f, lerp);
   1237         mClockShowing = clockAlpha > 0.0f;
   1238         if (clockAlpha > 0.0f) {
   1239             City city = mCities.get(mCityIndex);
   1240             mClock.setCity(city);
   1241             mClock.setTime(now);
   1242 
   1243             float cx = w / 2.0f;
   1244             float cy = h / 2.0f;
   1245             float smallRadius = 18.0f;
   1246             float bigRadius = 0.75f * 0.5f * Math.min(w, h);
   1247             float radius = lerp(smallRadius, bigRadius, lerp);
   1248 
   1249             // Only display left/right arrows if we are in a name search
   1250             boolean scrollingByName =
   1251                 (mCityName.length() > 0) && (mCities.size() > 1);
   1252             mClock.drawClock(canvas, cx, cy, radius,
   1253                              clockAlpha,
   1254                              1.0f,
   1255                              lerp == 1.0f, lerp == 1.0f,
   1256                              !atEndOfTimeZone(-1),
   1257                              !atEndOfTimeZone(1),
   1258                              scrollingByName,
   1259                              mCityName.length());
   1260         }
   1261     }
   1262 
   1263     /**
   1264      * Draws the 2D layer.
   1265      */
   1266     @Override protected void onDraw(Canvas canvas) {
   1267         long now = System.currentTimeMillis();
   1268         if (startTime != -1) {
   1269             startTime = -1;
   1270         }
   1271 
   1272         int w = getWidth();
   1273         int h = getHeight();
   1274 
   1275         // Interpolator for clock size, clock alpha, night lights intensity
   1276         float lerp = Math.min((now - mClockFadeTime)/1000.0f, 1.0f);
   1277         if (!mDisplayClock) {
   1278             // Clock is receding
   1279             lerp = 1.0f - lerp;
   1280         }
   1281         lerp = mClockSizeInterpolator.getInterpolation(lerp);
   1282 
   1283         // we don't need to make sure OpenGL rendering is done because
   1284         // we're drawing in to a different surface
   1285 
   1286         drawClock(canvas, now, w, h, lerp);
   1287 
   1288         mGLView.showMessages(canvas);
   1289         mGLView.showStatistics(canvas, w);
   1290     }
   1291 
   1292     /**
   1293      * Draws the 3D layer.
   1294      */
   1295     protected void drawOpenGLScene() {
   1296         long now = System.currentTimeMillis();
   1297         mNumTriangles = 0;
   1298 
   1299         EGL10 egl = (EGL10)EGLContext.getEGL();
   1300         GL10 gl = (GL10)mEGLContext.getGL();
   1301 
   1302         if (!mInitialized) {
   1303             init(gl);
   1304         }
   1305 
   1306         int w = getWidth();
   1307         int h = getHeight();
   1308         gl.glViewport(0, 0, w, h);
   1309 
   1310         gl.glEnable(GL10.GL_LIGHTING);
   1311         gl.glEnable(GL10.GL_LIGHT0);
   1312         gl.glEnable(GL10.GL_CULL_FACE);
   1313         gl.glFrontFace(GL10.GL_CCW);
   1314 
   1315         float ratio = (float) w / h;
   1316         mGLView.setAspectRatio(ratio);
   1317 
   1318         mGLView.setTextureParameters(gl);
   1319 
   1320         if (PERFORM_DEPTH_TEST) {
   1321             gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
   1322         } else {
   1323             gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
   1324         }
   1325 
   1326         if (mDisplayWorldFlat) {
   1327             gl.glMatrixMode(GL10.GL_PROJECTION);
   1328             gl.glLoadIdentity();
   1329             gl.glFrustumf(-1.0f, 1.0f, -1.0f / ratio, 1.0f / ratio, 1.0f, 2.0f);
   1330             gl.glMatrixMode(GL10.GL_MODELVIEW);
   1331             gl.glLoadIdentity();
   1332             gl.glTranslatef(0.0f, 0.0f, -1.0f);
   1333         } else {
   1334             mGLView.setProjection(gl);
   1335             mGLView.setView(gl);
   1336         }
   1337 
   1338         if (!mDisplayWorldFlat) {
   1339             if (mFlyToCity) {
   1340                 float lerp = (now - mCityFlyStartTime)/mCityFlightTime;
   1341                 if (lerp >= 1.0f) {
   1342                     mFlyToCity = false;
   1343                 }
   1344                 lerp = Math.min(lerp, 1.0f);
   1345                 lerp = mFlyToCityInterpolator.getInterpolation(lerp);
   1346                 mRotAngle = lerp(mRotAngleStart, mRotAngleDest, lerp);
   1347                 mTiltAngle = lerp(mTiltAngleStart, mTiltAngleDest, lerp);
   1348             }
   1349 
   1350             // Rotate the viewpoint around the earth
   1351             gl.glMatrixMode(GL10.GL_MODELVIEW);
   1352             gl.glRotatef(mTiltAngle, 1, 0, 0);
   1353             gl.glRotatef(mRotAngle, 0, 1, 0);
   1354 
   1355             // Increment the rotation angle
   1356             mRotAngle += mRotVelocity;
   1357             if (mRotAngle < 0.0f) {
   1358                 mRotAngle += 360.0f;
   1359             }
   1360             if (mRotAngle > 360.0f) {
   1361                 mRotAngle -= 360.0f;
   1362             }
   1363         }
   1364 
   1365         // Draw the world with lighting
   1366         gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, mLightDir, 0);
   1367         mGLView.setLights(gl, GL10.GL_LIGHT0);
   1368 
   1369         if (mDisplayWorldFlat) {
   1370             drawWorldFlat(gl);
   1371         } else if (mDisplayWorld) {
   1372             drawWorldRound(gl);
   1373         }
   1374 
   1375         if (mDisplayLights && !mDisplayWorldFlat) {
   1376             // Interpolator for clock size, clock alpha, night lights intensity
   1377             float lerp = Math.min((now - mClockFadeTime)/1000.0f, 1.0f);
   1378             if (!mDisplayClock) {
   1379                 // Clock is receding
   1380                 lerp = 1.0f - lerp;
   1381             }
   1382             lerp = mClockSizeInterpolator.getInterpolation(lerp);
   1383             drawCityLights(gl, lerp);
   1384         }
   1385 
   1386         if (mDisplayAtmosphere && !mDisplayWorldFlat) {
   1387             drawAtmosphere(gl);
   1388         }
   1389         mGLView.setNumTriangles(mNumTriangles);
   1390         egl.eglSwapBuffers(mEGLDisplay, mEGLSurface);
   1391 
   1392         if (egl.eglGetError() == EGL11.EGL_CONTEXT_LOST) {
   1393             // we lost the gpu, quit immediately
   1394             Context c = getContext();
   1395             if (c instanceof Activity) {
   1396                 ((Activity)c).finish();
   1397             }
   1398         }
   1399     }
   1400 
   1401 
   1402     private static final int INVALIDATE = 1;
   1403     private static final int ONE_MINUTE = 60000;
   1404 
   1405     /**
   1406      * Controls the animation using the message queue.  Every time we receive
   1407      * an INVALIDATE message, we redraw and place another message in the queue.
   1408      */
   1409     private final Handler mHandler = new Handler() {
   1410         private long mLastSunPositionTime = 0;
   1411 
   1412         @Override public void handleMessage(Message msg) {
   1413             if (msg.what == INVALIDATE) {
   1414 
   1415                 // Use the message's time, it's good enough and
   1416                 // allows us to avoid a system call.
   1417                 if ((msg.getWhen() - mLastSunPositionTime) >= ONE_MINUTE) {
   1418                     // Recompute the sun's position once per minute
   1419                     // Place the light at the Sun's direction
   1420                     computeSunDirection();
   1421                     mLastSunPositionTime = msg.getWhen();
   1422                 }
   1423 
   1424                 // Draw the GL scene
   1425                 drawOpenGLScene();
   1426 
   1427                 // Send an update for the 2D overlay if needed
   1428                 if (mInitialized &&
   1429                                 (mClockShowing || mGLView.hasMessages())) {
   1430                     invalidate();
   1431                 }
   1432 
   1433                 // Just send another message immediately. This works because
   1434                 // drawOpenGLScene() does the timing for us -- it will
   1435                 // block until the last frame has been processed.
   1436                 // The invalidate message we're posting here will be
   1437                 // interleaved properly with motion/key events which
   1438                 // guarantee a prompt reaction to the user input.
   1439                 sendEmptyMessage(INVALIDATE);
   1440             }
   1441         }
   1442     };
   1443 }
   1444 
   1445 /**
   1446  * The main activity class for GlobalTime.
   1447  */
   1448 public class GlobalTime extends Activity {
   1449 
   1450     GTView gtView = null;
   1451 
   1452     @Override protected void onCreate(Bundle icicle) {
   1453         super.onCreate(icicle);
   1454         gtView = new GTView(this);
   1455         setContentView(gtView);
   1456     }
   1457 
   1458     @Override protected void onResume() {
   1459         super.onResume();
   1460         gtView.onResume();
   1461         Looper.myQueue().addIdleHandler(new Idler());
   1462     }
   1463 
   1464     @Override protected void onPause() {
   1465         super.onPause();
   1466         gtView.onPause();
   1467     }
   1468 
   1469     @Override protected void onStop() {
   1470         super.onStop();
   1471         gtView.destroy();
   1472         gtView = null;
   1473     }
   1474 
   1475     // Allow the activity to go idle before its animation starts
   1476     class Idler implements MessageQueue.IdleHandler {
   1477         public Idler() {
   1478             super();
   1479         }
   1480 
   1481         public final boolean queueIdle() {
   1482             if (gtView != null) {
   1483                 gtView.startAnimating();
   1484             }
   1485             return false;
   1486         }
   1487     }
   1488 }
   1489