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