/** * SunSphereData provides the actual image for SunSphereCanvas.<p> * * Copyright &copy; 1997 Martin Minow. All Rights Reserved.<p> * * Permission to use, copy, modify, and redistribute this software and its * documentation for personal, non-commercial use is hereby granted provided that * this copyright notice and appropriate documentation appears in all copies. This * software may not be distributed for fee or as part of commercial, "shareware," * and/or not-for-profit endevors including, but not limited to, CD-ROM collections, * online databases, and subscription services without specific license.<p> * * @author <a href="mailto:minow@pobox.com">Martin Minow</a> * @version 1.0.2 * @see Snyder, John P., Map Projections -- A Working Manual, *		U.S. Geological Survey Professional Paper 1395, *		United States Government Printing Office, Washington * Set tabs every 8 characters. */package org.minow.applets.sunsphere;import java.awt.*;import java.awt.image.*;import java.util.*;import java.beans.*;public class SunSphereData extends Object{    protected SunSphere		sunSphereApplet;    protected SunrisePixels	sunrisePixels;	/* Image (transformed)		*/    /**     * spherePixels contains the final image, after all transformations and     * repositioning.     */    protected int[]		spherePixels	= null; /* sphereDiameter^2		*/    /**     * mapLatitude contains the original image latitude (expressed as a pixel     * index from 0 to imageHeight, increasing to the South) for every pixel     * in the spherical image.     */    protected int[]		mapLatitude;    /**     * latitudeFactor contains the conversion factor to be applied to the     * longitude due to latitude for every pixel in the spherical image. It is a     * value from -180.0 to +180.0 in degrees of longitude.     */    protected float[]		latitudeFactor;    /**     * sphereImage is the image that will be constructed. It is the image     * resulting from constructing a MemoryImageSource from spherePixels.     * The image is always dimensioned [sphereDiameter, sphereDiameter].     */    protected Image	sphereImage	= null; /* Image that will be drawn	*/    protected int	sphereDiameter	= 0;	/* Math.min(width, height)	*/    protected int	sphereRadius	= 0;	/* sphereDiameter / 2		*/    protected int	imageWidth	= 0;	/* Source image width		*/    protected int	imageHeight	= 0;	/* Source image height		*/    protected MemoryImageSource sphereImageSource;    /**     * when this.latitude != current image latitude, the transformation vectors,     * mapLatitude and latitudeFactor are recomputed.     */    protected double	latitude	= Double.NaN;	/* Force initial setup	*/    protected Date	date		= new Date();        public static final double DegRad	= (Math.PI / 180.0);        /**     * Create the SunSphere image.     * @param	imagePixels	The original world map image. This is an unrolled     *				pixel vector where each pixel defines a specific     *				latitude and longitude. The world map is imaged     *				in the "Plate-Coree" or orthographic projection,     *				which is linear in both latitude and longitude.     * @param	imageWidth	The width of the image in pixels.     * @param	imageHeight	The height of the image in pixels     */    public SunSphereData(    	    SunSphere		sunSphereApplet,	    int[]		imagePixels,	    int			imageWidth,	    int			imageHeight	)    {    	this.sunSphereApplet	= sunSphereApplet;	this.imageWidth		= imageWidth;	this.imageHeight	= imageHeight;	/*	 * sunrisePixels will contain the original image with the	 * sunrise/sunset terminator superimposed.	 */	sunrisePixels		= new SunrisePixels(imagePixels, imageWidth, imageHeight);    }        /**     * Called whenever the time changes.     */    public void setCurrentTime(	    long		currentTime	)    {	sunrisePixels.setCurrentTime(currentTime);    }    /**     * Set the current latitude. This may cause the image to be reconstructed.     */    public synchronized boolean setLatitude(    	    double		latitude    	)    {    	boolean result		= this.latitude != latitude;    	if (result) {	    this.latitude	= latitude; 	    createTransformationVectors(); 	} 	return (result);     }    /**     * Return the spherical image. This is called with the display size     * (which will cause the image to be reconstructed when it changes).     * @param sphereDiameter	Math.min(displayWidth, displayHeight)     */    public Image getSphereImage(    	    int			sphereDiameter,    	    Color		background    	)    {	boolean needImageFlush	= false;    	if (sphereImage == null || sphereDiameter != this.sphereDiameter) { 	    this.sphereDiameter	= sphereDiameter; 	    this.sphereRadius	= sphereDiameter / 2; 	    this.latitude	= Double.NaN;	/* Force setup		*/ 	    createSphereImage();	    needImageFlush	= true; 	}	// long startMsec	= System.currentTimeMillis();	needImageFlush = setLatitude(sunSphereApplet.getLatitude());	transformImage(background);	// System.out.println(System.currentTimeMillis() - startMsec);	if (needImageFlush) {	    sphereImage.flush();	}	return (sphereImage);    }    /**     * Create a sphereImage that will receive the transformed map.     */    protected void createSphereImage()    {    	int imageSize		= sphereDiameter * sphereDiameter;	spherePixels		= new int[imageSize];	mapLatitude		= new int[imageSize];	latitudeFactor		= new float[imageSize];	try {	    sphereImageSource	= new MemoryImageSource(		    sphereDiameter,		    sphereDiameter,		    spherePixels,		    0,		    sphereDiameter		);    	}	catch (Exception e) {	    System.out.println("new MemoryImageSource failed: " + e);	    System.out.println("sphereDiameter: " + sphereDiameter);	    throw new RuntimeException ("Failure in MemoryImageSource" + e);	}	sphereImageSource.setAnimated(true);	sphereImage	    =		Toolkit.getDefaultToolkit().createImage(sphereImageSource);	this.latitude		= Double.NaN;	/* Force rebuild	*/    }    /**     * Transform the source image, in mapPixels, to the destination     * image, in spherePixels, using the pre-computed transformation lens.     * This is critical-path for display animation and updates.     */	     protected void transformImage(    	    Color		background    	)    {	int	length		= spherePixels.length;	int	backgroundRGB	= background.getRGB();	int[]	mapPixels	= sunrisePixels.getSunrisePixels();	double	longitude	= sunSphereApplet.getLongitude();	for (int i = 0; i < length; i++) {	    if (mapLatitude[i] < 0) {		spherePixels[i]	= backgroundRGB;	/* Transparent	    */	    }	    else {	    	double	mapLongitude	= Double.NaN;	    	double	xRatio		= Double.NaN;	    	int	xImage		= 0;	    	int	mapIndex	= 0;		try {		    mapLongitude	= longitude + latitudeFactor[i];		    xRatio		= (mapLongitude + 180.0) / 360.0;		    xImage		= (int) (xRatio * (double) imageWidth);		    /*		     * There is a potential round-off error in the above		     * that can make xRatio exceed imageWidth. Here's a		     * cheap fix.		     */		    xImage		%= imageWidth;		    mapIndex		= (mapLatitude[i] * imageWidth) + xImage;		    spherePixels[i]	= mapPixels[mapIndex];		}				catch (Exception e) {		    if (SunSphere.logErrors) {			System.out.println("Transform out of bounds:");			Utility.log("length",		length);			Utility.log("imageWidth",	imageWidth);			Utility.log("longitude",	longitude);			Utility.log("latitudeFactor[i]",  latitudeFactor[i]);			Utility.log("i",		i);			Utility.log("row",		(i / imageWidth));			Utility.log("col",		(i % imageWidth));			Utility.log("mapLongitude",	mapLongitude);			Utility.log("xRatio",		xRatio);			Utility.log("mapLatitude[i]",	mapLatitude[i]);			Utility.log("mapIndex",		mapIndex);			Utility.log("mapPixels.length",	mapPixels.length);		    }		    spherePixels[i] = Color.red.getRGB();		}	    }	}	sphereImageSource.newPixels();    }    /**     * Construct the transformation vectors for a particular input     * and output image with the central latitude at a particular location.     * This uses the following variables:     * imageWidth	The width of the source image     * imageHeigh	The height of the source image     * sphereDiameter	The height and width of the destination imaage     * latitude		The latitude of the center-point of the resulting iamge     *     * This sets mapLatitude[] to the source image pixel row that will be copied     * to the destination. If mapLatitude[i] == -1, this pixel is outside of the     * sphere, and will always be transparent.     *     * latitudeFactor is set to the number of degrees of latitude to offset     * the center-point latitude by for this location. Referring to the     * mapToWorld method below, it is the "+ Math.atan(t3, t4) / DegRad"     * factor.     *     * The vectors are dimensioned <code>sphereDiameter * sphereDiameter</code>     * and are written in row-major order (this is the same order that     * <code>PixelGrabber.grabPixels()</code> uses). This method is called when     * the display is constructed, its size changes, or the observer latitude     * changes. It is not called when only the longitude changes.     */    protected void createTransformationVectors()    {	double	radiusSq	= (double) (sphereRadius * sphereRadius);	double	latitude	= sunSphereApplet.getLatitude();	double	sinLat		= Math.sin(latitude * DegRad);	double	cosLat		= Math.cos(latitude * DegRad);	/*	 * This is the orthographic projection from Snyder, John P.,	 * "Map Projections -- A Working Manual". U.S. Government	 * Printing Office. Page 148-150.	 */	int 	i		= 0;	for (int row = 0; row < sphereDiameter; row++) {	    double y		= sphereRadius - row;	/* y increases to North	*/	    double dY2		= y * y;	    for (int col = 0; col < sphereDiameter; col++) {		double x	= col - sphereRadius;		double dX2	= x * x;		/* x increases to East	*/		double rhoSq	= dX2 + dY2;		if (rhoSq > radiusSq) {		    /*		     * It's invisible		     */		    mapLatitude[i]	= -1;		}		else {		    double rho		= Math.sqrt(rhoSq);		    double thisLatitude	= latitude;		    double sinC		= 0.0;		    double cosC		= 0.0;		    double t1		= 0.0;		    double t2		= 0.0;		    double t3		= 0.0;		    double t4		= 0.0;		    if (rho > 0.0) {			sinC		= rho / ((double) sphereRadius);			cosC		= Math.sqrt(1.0 - (sinC * sinC)); // Math.cos(Math.asin(sinC));			t1		= cosC * sinLat;			t2		= (y * sinC * cosLat) / rho;			thisLatitude	= Math.asin(t1 + t2) / DegRad;			t3		= x * sinC;			t4		= (rho * cosLat * cosC - y * sinLat * sinC);			latitudeFactor[i] = (float) (Math.atan2(t3, t4) / DegRad);		    }		    double yRatio	= (90.0 - thisLatitude) / 180.0;		    mapLatitude[i]	= (int) (yRatio * (double) imageHeight);		    /*		     * There is a potential round-off error in the above sequence,		     * where mapLatitude[i] can (potentially) fall outside of the		     * actual image. We choose a cheap fix.		     */		    mapLatitude[i]	= Math.min(mapLatitude[i], imageHeight - 1);		/*		    if (col == sphereRadius) {			System.out.println(				Utility.format(row, 4)				+ ", " + Utility.format(thisLatitude)			 	+ ", " + Utility.format(mapLatitude[i])			 );		    }		    if (SunSphere.logErrors) {			Utility.log("sphereDiameter",	sphereDiameter);			Utility.log("sphereRadius",	sphereRadius);			Utility.log("latitude",		latitude);			Utility.log("sinLat",		sinLat);			Utility.log("cosLat",		cosLat);			Utility.log("i",		i);			Utility.log("row",		row);			Utility.log("y",		y);			Utility.log("col",		col);			Utility.log("x",		x);			Utility.log("rhoSq",		rhoSq);			Utility.log("rho",		rho);			Utility.log("sinC",		sinC);			Utility.log("cosC",		cosC);			Utility.log("t1",		t1);			Utility.log("t2",		t2);			Utility.log("t3",		t3);			Utility.log("t4",		t4);			Utility.log("thisLatitude",	thisLatitude);			Utility.log("latitudeFactor[i]", latitudeFactor[i]);			Utility.log("yRatio",		yRatio);			Utility.log("mapLatitude[i]",	mapLatitude[i]);		    }		*/		} /* If this point is inside the sphere */		i++;	    } /* For all pixels in this row */	} /* For all rows in the sphere */    }    	    /**     * Convert a pixel-based location, presumably inside the image, to     * the latitude and longitude of the corresponding location on the     * underlying earth. The algorithm is identical to the one used by     * constructLens, but it is organized for hit-testing and other interaction.     *     * @param	pixel		The location of interest (in sphere coordinates)     *				pixel.x, pixel.y ranges from zero to sphereDiameter     * @param	sphereDiameter	The diameter of the sphere (origin is [0, 0]).     * @throws	IllegalArgumentException if the pixel is not within the actual     *				sphere.     * @return	result[0]	Actual latitude (+90 .. -90)     *		result[1]	Actual longitude (-180 .. +180)     */     public double[] mapToWorld(    	    int			pixelX,    	    int			pixelY	)    {    	/*    	 * x advances from left to right (West to East)    	 * y advances from bottom to top (South to North)    	 */    	double x		= (pixelX - sphereRadius);    	double y		= sphereRadius - pixelY;    	double rho		= Math.sqrt((x * x) + (y * y));    	if (rho > (double) sphereRadius) {    	    throw new IllegalArgumentException("Pixel outside of sphere");    	}    	double[] result		= new double[2];    	if (rho <= 0.0) {    	    result[0]		= 0.0;    	    result[1]		= 0.0;    	}    	else {	    double sinC		= rho / ((double) sphereRadius);	    double cosC		= Math.sqrt(1.0 - sinC * sinC); // Math.cos(Math.asin(sinC));	    result[0]		= Math.asin((y * sinC) / rho) / DegRad;	    result[1]		= Math.atan2(x * sinC, rho * cosC) / DegRad;	}	return (result);    }    	}