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