• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
surfaceCreated(SurfaceHolder holder)230     public void surfaceCreated(SurfaceHolder holder) {
231         mHaveSurface = true;
232         startEGL();
233     }
234 
surfaceDestroyed(SurfaceHolder holder)235     public void surfaceDestroyed(SurfaceHolder holder) {
236         mHaveSurface = false;
237         stopEGL();
238     }
239 
surfaceChanged(SurfaceHolder holder, int format, int w, int h)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      */
GTView(Context context)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      */
startEGL()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      */
stopEGL()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 
onPause()339     public void onPause() {
340         mPaused = true;
341         stopAnimating();
342         stopEGL();
343     }
344 
onResume()345     public void onResume() {
346         mPaused = false;
347         startEGL();
348     }
349 
destroy()350     public void destroy() {
351         stopAnimating();
352         stopEGL();
353     }
354 
355     /**
356      * Begin animation.
357      */
startAnimating()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      */
stopAnimating()369     public void stopAnimating() {
370         mHandler.removeMessages(INVALIDATE);
371     }
372 
373     /**
374      * Read a two-byte integer from the input stream.
375      */
readInt16(InputStream is)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      */
getOffset(City c)386     private static float getOffset(City c) {
387         return USE_RAW_OFFSETS ? c.getRawOffset() : c.getOffset();
388     }
389 
cache(InputStream is)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      */
loadAssets(final AssetManager am)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      */
tzEqual(float o1, float o2)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      */
shiftTimeZone(int incr)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      */
atEndOfTimeZone(int incr)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      */
shiftWithinTimeZone(int incr)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      */
nameMatches(City city, String prefix)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      */
hasMatches(String prefix)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      */
shiftByName()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      */
incrementRotationalVelocity(float incr)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      */
clearCityMatches()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      */
enableClock(boolean enabled)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      */
onTouchEvent(MotionEvent event)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 
onKeyDown(int keyCode, KeyEvent event)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      */
init(GL10 gl)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      */
computeSunDirection()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      */
distance(float lat1, float lon1, float lat2, float lon2)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      */
locateCity(boolean useOffset, float offset)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      */
goToCity()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      */
lerp(float a, float b, float lerp)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      */
drawCityLights(GL10 gl, float brightness)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      */
drawAtmosphere(GL10 gl)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      */
drawWorldFlat(GL10 gl)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      */
drawWorldRound(GL10 gl)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      */
drawClock(Canvas canvas, long now, int w, int h, float lerp)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      */
onDraw(Canvas canvas)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      */
drawOpenGLScene()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 
onCreate(Bundle icicle)1452     @Override protected void onCreate(Bundle icicle) {
1453         super.onCreate(icicle);
1454         gtView = new GTView(this);
1455         setContentView(gtView);
1456     }
1457 
onResume()1458     @Override protected void onResume() {
1459         super.onResume();
1460         gtView.onResume();
1461         Looper.myQueue().addIdleHandler(new Idler());
1462     }
1463 
onPause()1464     @Override protected void onPause() {
1465         super.onPause();
1466         gtView.onPause();
1467     }
1468 
onStop()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 {
Idler()1477         public Idler() {
1478             super();
1479         }
1480 
queueIdle()1481         public final boolean queueIdle() {
1482             if (gtView != null) {
1483                 gtView.startAnimating();
1484             }
1485             return false;
1486         }
1487     }
1488 }
1489