alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Android example source code file (GlobalTime.java)

This example Android source code file (GlobalTime.java) is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Android by Example" TM.

Java - Android tags/keywords

android, city, egl10, eglconfig, gtview, idler, inputstream, io, ioexception, motion_none, motion_x, os, override, perform_depth_test, rotation_factor, seconds_per_day, string, ui, util, view

The GlobalTime.java Android example source code

/*
 * Copyright (C) 2007 Google Inc.
 *
 * 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();

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

    // 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;

    public void surfaceCreated(SurfaceHolder holder) {
        EGL10 egl = (EGL10)EGLContext.getEGL();
        mEGLSurface = egl.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, this, null);
        egl.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        // nothing to do
    }

    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);

        AssetManager am = context.getAssets();
        startTime = System.currentTimeMillis();

        EGL10 egl = (EGL10)EGLContext.getEGL();
        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;

        mClock = new Clock();

        setFocusable(true);
        setFocusableInTouchMode(true);
        requestFocus();

        try {
            loadAssets(am);
        } catch (IOException ioe) {
            ioe.printStackTrace();
            throw new RuntimeException(ioe);
        } catch (ArrayIndexOutOfBoundsException aioobe) {
            aioobe.printStackTrace();
            throw new RuntimeException(aioobe);
        }
    }

    /**
     * Destroy the view.
     */
    public void destroy() {
        EGL10 egl = (EGL10)EGLContext.getEGL();
        egl.eglMakeCurrent(mEGLDisplay, 
                egl.EGL_NO_SURFACE, egl.EGL_NO_SURFACE, egl.EGL_NO_CONTEXT);
        egl.eglDestroyContext(mEGLDisplay, mEGLContext);
        egl.eglDestroySurface(mEGLDisplay, mEGLSurface);
        egl.eglTerminate(mEGLDisplay);
        mEGLContext = null;
    }

    /**
     * Begin animation.
     */
    public void startAnimating() {
        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;

    private void setGTView() {
        if (gtView == null) {
            gtView = new GTView(this);
            setContentView(gtView);
        }
    }

    @Override protected void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setGTView();
    }

    @Override protected void onResume() {
        super.onResume();
        setGTView();
        Looper.myQueue().addIdleHandler(new Idler());
    }

    @Override protected void onPause() {
        super.onPause();
        gtView.stopAnimating();
    }

    @Override protected void onStop() {
        super.onStop();
        gtView.stopAnimating();
        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;
        }
    }
}

Other Android examples (source code examples)

Here is a short list of links related to this Android GlobalTime.java source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.