FileDocCategorySizeDatePackage
GlobalTime.javaAPI DocAndroid 1.5 API47651Wed May 06 22:41:08 BST 2009com.android.globaltime

GTView

public class GTView extends android.view.SurfaceView implements SurfaceHolder.Callback
The main View of the GlobalTime Activity.

Fields Summary
private static final TimeZone
UTC_TIME_ZONE
A TimeZone object used to compute the current UTC time.
private static final float[]
SUNLIGHT_COLOR
The Sun's color is close to that of a 5780K blackbody.
private static final float
EARTH_INCLINATION
The inclination of the earth relative to the plane of the ecliptic is 23.45 degrees.
private static final int
SECONDS_PER_DAY
Seconds in a day
private static final boolean
PERFORM_DEPTH_TEST
Flag for the depth test
private static final boolean
USE_RAW_OFFSETS
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 Annulus
ATMOSPHERE
The earth's atmosphere.
private static final int
SPHERE_LATITUDES
The tesselation of the earth by latitude.
private static int
SPHERE_LONGITUDES
The tesselation of the earth by longitude.
private static Sphere
worldFlat
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 android.opengl.Object3D
mWorld
The earth.
private PointCloud
mLights
Geometry of the city lights
boolean
mInitialized
True if the activiy has been initialized.
private boolean
mAlphaKeySet
True if we're in alphabetic entry mode.
private EGLContext
mEGLContext
private EGLSurface
mEGLSurface
private EGLDisplay
mEGLDisplay
private EGLConfig
mEGLConfig
GLView
mGLView
private float
mRotAngle
private float
mTiltAngle
private float
mRotVelocity
private float
mWrapX
private float
mWrapVelocity
private float
mWrapVelocityFactor
private boolean
mDisplayAtmosphere
private boolean
mDisplayClock
private boolean
mClockShowing
private boolean
mDisplayLights
private boolean
mDisplayWorld
private boolean
mDisplayWorldFlat
private boolean
mSmoothShading
private String
mCityName
private List
mClockCities
private List
mCityNameMatches
private List
mCities
private long
mClockFadeTime
private android.view.animation.Interpolator
mClockSizeInterpolator
private int
mCityIndex
private Clock
mClock
private boolean
mFlyToCity
private long
mCityFlyStartTime
private float
mCityFlightTime
private float
mRotAngleStart
private float
mRotAngleDest
private float
mTiltAngleStart
private float
mTiltAngleDest
private android.view.animation.Interpolator
mFlyToCityInterpolator
private static int
sNumLights
private static int[]
sLightCoords
private float[]
mClipPlaneEquation
private float[]
mLightDir
Calendar
mSunCal
private int
mNumTriangles
private long
startTime
private static final int
MOTION_NONE
private static final int
MOTION_X
private static final int
MOTION_Y
private static final int
MIN_MANHATTAN_DISTANCE
private static final float
ROTATION_FACTOR
private static final float
TILT_FACTOR
private float
mMotionStartX
private float
mMotionStartY
private float
mMotionStartRotVelocity
private float
mMotionStartTiltAngle
private int
mMotionDirection
private boolean
mPaused
private boolean
mHaveSurface
private boolean
mStartAnimating
private static final int
INVALIDATE
private static final int
ONE_MINUTE
private final android.os.Handler
mHandler
Controls the animation using the message queue. Every time we receive an INVALIDATE message, we redraw and place another message in the queue.
Constructors Summary
public GTView(android.content.Context context)
Set up the view.

param
context the Context
param
am an AssetManager to retrieve the city database from

        super(context);

        getHolder().addCallback(this);
        getHolder().setType(SurfaceHolder.SURFACE_TYPE_GPU);

        startTime = System.currentTimeMillis();

        mClock = new Clock();

        startEGL();
        
        setFocusable(true);
        setFocusableInTouchMode(true);
        requestFocus();
    
Methods Summary
private booleanatEndOfTimeZone(int incr)
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

        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;
    
private java.io.InputStreamcache(java.io.InputStream is)

        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);
    
private voidclearCityMatches()
Clears the current matching prefix, while keeping the focus on the current city.

        // 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();
    
private voidcomputeSunDirection()
Computes the vector from the center of the earth to the sun for a particular moment in time.

        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;
    
public voiddestroy()

        stopAnimating();
        stopEGL();
    
private floatdistance(float lat1, float lon1, float lat2, float lon2)
Computes the approximate spherical distance between two (latitude, longitude) coordinates.

        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);
    
private voiddrawAtmosphere(GL10 gl)
Draws the atmosphere.

        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();
    
private voiddrawCityLights(GL10 gl, float brightness)
Draws the city lights, using a clip plane to restrict the lights to the night side of the earth.

        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;
    
private voiddrawClock(android.graphics.Canvas canvas, long now, int w, int h, float lerp)
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

        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());
        }
    
protected voiddrawOpenGLScene()
Draws the 3D layer.

        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 voiddrawWorldFlat(GL10 gl)
Draws the world in a 2D map view.

        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;
        }
    
private voiddrawWorldRound(GL10 gl)
Draws the world in a 2D round view.

        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();
    
private voidenableClock(boolean enabled)
Fade the clock in or out.

        mClockFadeTime = System.currentTimeMillis();
        mDisplayClock = enabled;
        mClockShowing = true;
        mAlphaKeySet = enabled;
        if (enabled) {
            // Find the closest matching city
            locateCity(false, 0.0f);
        }
        clearCityMatches();
    
private static floatgetOffset(City c)
Returns the offset from UTC for the given city. If USE_RAW_OFFSETS is true, summer/daylight savings is ignored.

        return USE_RAW_OFFSETS ? c.getRawOffset() : c.getOffset();
    
private voidgoToCity()
Animates the earth to be centered at the current city.

        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;
    
private booleanhasMatches(java.lang.String prefix)
Returns true if there are cities matching the given name prefix.

        for (int i = 0; i < mClockCities.size(); i++) {
            City city = mClockCities.get(i);
            if (nameMatches(city, prefix)) {
                return true;
            }
        }

        return false;
    
private voidincrementRotationalVelocity(float incr)
Increases or decreases the rotational speed of the earth.

        if (mDisplayWorldFlat) {
            mWrapVelocity -= incr;
        } else {
            mRotVelocity -= incr;
        }
    
private synchronized voidinit(GL10 gl)
Initialize OpenGL ES drawing.

        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;
    
private floatlerp(float a, float b, float lerp)
Returns a linearly interpolated value between two values.

        return a + (b - a)*lerp;
    
private voidloadAssets(android.content.res.AssetManager am)
Load the city and lights databases.

param
am the AssetManager to load from.

        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);
    
private voidlocateCity(boolean useOffset, float offset)
Locates the closest city to the currently displayed center point, optionally restricting the search to cities within a given time zone.

        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;
    
private booleannameMatches(City city, java.lang.String prefix)
Returns true if the city name matches the given prefix, ignoring spaces.

        String cityName = city.getName().replaceAll("[ ]", "");
        return prefix.regionMatches(true, 0,
                                    cityName, 0,
                                    prefix.length());
    
protected voidonDraw(android.graphics.Canvas canvas)
Draws the 2D layer.

        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);
    
public booleanonKeyDown(int keyCode, android.view.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;
    
public voidonPause()

        mPaused = true;
        stopAnimating();
        stopEGL();
    
public voidonResume()

        mPaused = false;
        startEGL();
    
public booleanonTouchEvent(android.view.MotionEvent event)
Use the touchscreen to alter the rotational velocity or the tilt of the earth.

        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;
    
private intreadInt16(java.io.InputStream is)
Read a two-byte integer from the input stream.

        int lo = is.read();
        int hi = is.read();
        return (hi << 8) | lo;
    
private voidshiftByName()
Shifts to the nearest city that matches the new prefix.

        // 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();
    
private voidshiftTimeZone(int incr)
Move to a different time zone.

param
incr The increment between the current and future time zones.

        // 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();
    
private voidshiftWithinTimeZone(int incr)
Shifts cities within the current time zone.

param
incr the increment, +1 or -1

        float offset = getOffset(mCities.get(mCityIndex));
        int nindex = (mCityIndex + mCities.size() + incr) % mCities.size();
        if (tzEqual(getOffset(mCities.get(nindex)), offset)) {
            mCityIndex = nindex;
            goToCity();
        }
    
public voidstartAnimating()
Begin animation.

        if (mEGLSurface == null) {
            mStartAnimating = true; // will start when egl surface is created
        } else {
            mHandler.sendEmptyMessage(INVALIDATE);
        }
    
private voidstartEGL()
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().

        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;
            }
        }
    
public voidstopAnimating()
Quit animation.

        mHandler.removeMessages(INVALIDATE);
    
private voidstopEGL()
Destroys the egl context. If an egl surface has been created, it is destroyed as well.

        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 voidsurfaceChanged(android.view.SurfaceHolder holder, int format, int w, int h)

        // nothing to do
    
public voidsurfaceCreated(android.view.SurfaceHolder holder)

    
        
        mHaveSurface = true;
        startEGL();
    
public voidsurfaceDestroyed(android.view.SurfaceHolder holder)

        mHaveSurface = false;
        stopEGL();
    
private booleantzEqual(float o1, float o2)
Returns true if two time zone offsets are equal. We assume distinct time zone offsets will differ by at least a few minutes.

        return Math.abs(o1 - o2) < 0.001;