You've seen the beautiful images of Earth from space taken by NASA astronauts and satellites. By viewing a series of images one after another like frames of a movie, we can watch changes to the Earth as they happened. You can follow hurricanes moving towards land and observe ice caps contracting. You can replay and study changes due to weather, natural events, and human activities. NASA has assembled dozens of such animations, all free. This article describes how to write Java software that uses the OpenGL graphics interface to display these images on a 3D globe.
A team of expert scientists, engineers, and artists at NASA's Scientific Visualization Studio (SVS) carefully compose these animations. This group performs their magic at NASA's Goddard Space Flight Center in Maryland. They have been creating animated visualizations of Earth and space science data for over 15 years. They select images from a seemingly infinite collection of NASA imagery. They design each animation to illustrate a particular phenomenon or event. Here are two examples:

Figure 1.
African fires during 2002

Figure 2.
Atmospheric water vapor during 1998
Recently, SVS established an image server to make some of the best earth science animations available programmatically over the internet to software applications. Currently about 80 are available; that will grow to over 130 by October, 2005. You can see the growing list at the SVS server site [14]. The full SVS catalog contains over 2500 animations and is at the SVS home page [15].
SVS animations dynamically illustrate the formation of what's shown in these static images: The fires in Africa progress southwards with the season. The vapor and rain (yellow) flow across the globe as the days progress. You can see more single shots like those shown above at the SVS website, but they're really meant to be seen "in motion," wrapped around a 3D Earth. The SVS website doesn't do that. You need a software program running on a local computer to get the full effect. You also need the ability to interact with the model by turning the world around and zooming in and out. In this article, I describe a bare-bones program, in order to keep the code easily understandable. The best program for viewing SVS animations is the free and open source program NASA World Wind [16].
SVS imagery is designed to be used in educational software. The web service responds to HTTP requests by returning an image. There's a strict protocol and request language, with the request parameters tacked on to what we'd consider a conventional URL.
Four steps are required to retrieve and display SVS animations in 3D:
The code I'll use to demonstrate and explain all this is a small Java application called EarthFlicks, which is available for download at the end of this article. EarthFlicks creates two windows: one for selecting and retrieving animations from SVS, the other for showing and playing them. Figure 3 shows what it looks like:

Figure 3.
EarthFlicks
Selecting an animation in the table and clicking the Import button causes EarthFlicks to retrieve and cache locally all the images for that animation. When they are subsequently played, EarthFlicks recognizes them in the cache and draws the images from there, rather than from the SVS server.
EarthFlicks has been kept as simple as possible in order to keep its code clear and useful in a tutorial. However, retrieving and playing SVS animations requires a lot of code; more than I would have guessed before I started writing EarthFlicks. Not all of that code can be listed in this article. What are listed are important details unique to the task. The rest is documented in the source files themselves, which are available at the location mentioned above.
As of July 2005, SVS holds 90 animations and 25 static images, such as the Blue Marble imagery of a cloudless Earth and really cool composites of the Earth at night. This collection is growing and is expected to contain 130 animations by October 2005.
The server advertises the available animations in the capabilities document [17]. This XML document includes the names of the images, URLs for requesting them, the image formats it can provide them in, descriptions of their sizes, regions of the Earth, and other information that's useful or not, depending on how the images are to be used. WMS is an ISO standard for web services that serve maps and other geographic information. The specification is at the OpenGIS specification page [18]. There's also a tutorial I wrote at The Code Project [19] for C# programmers, but describes WMS in general. The standard is in its third revision, 1.3.0, which was final in 2004. Industry and government have been using WMS for about three years.
WMS defines two primary requests and their parameters. One request,
GetCapabilities, asks for a server's table of contents,
as above. The other, GetMap, asks for a specific
image. The first part of either request is the server's address and
the path to its WMS web service. This is everything before the
question mark in the link above. The second part, the URL's query
string, contains the actual request and its parameters. An image request is
similar to the capabilities request, but has many more parameters, as I
describe below.
The simplicity of this scheme really impresses me. It makes it
possible to use the standard network-software infrastructure that a
run-time platform already provides. The classes and libraries a
programmer needs to issue the requests are merely the ones they'd use
to create and send any HTTP request. In Java, that's simply the
java.net.URL class.
An image request needs some of the information included in the capabilities document. Example 1 shows an excerpt from the SVS server's capabilities document. It describes just one of the available animations, which is a sequence of images showing the smoke flow from large Southern California fires in the winter of 2003. One frame of that animation is shown in Figure 4.

Figure 4.
Aerosols from California fires
Example 1. The partial Layer entry for the California Fires animation
<Layer opaque="0" noSubsets="1"
fixedWidth="640" fixedHeight="384">
<Name>3132_21145</Name>
<Title>Aerosols from 2003 Southern California Fires
(640x384 Animation)</Title>
<EX_GeographicBoundingBox>
<westBoundLongitude>-160</westBoundLongitude>
<eastBoundLongitude>-60</eastBoundLongitude>
<southBoundLatitude>10</southBoundLatitude>
<northBoundLatitude>58</northBoundLatitude>
</EX_GeographicBoundingBox>
<Dimension name="time" units="ISO8601"
default="2003-11-01">
2003-10-23/2003-11-01/P1D</Dimension>
</Layer>
Animations and single images are called
layers in WMS. The attributes of the Layer element
indicate that its images contain transparent pixels (opaque=0),
that images must be requested only at the latitudes and longitudes given in the
EX_GeographicBoundingBox element
(noSubsets=1), and that the server will always return an
image that is 640 pixels wide and 384 pixels high
(fixedWidth=640 and fixedHeight=384).
The Name element uniquely identifies this sequence of
images on the server. This is not a name you'd show an application
user, however. That's why the layer definition also contains
a Title element. Some image sequences span the entire
globe, but others, like this one, focus on a particular region. That
region is identified by latitude and longitude in the
element EX_GeographicBoundingBox.
So where is the sequence that forms the animation? That is
implied in the Dimension element. Notice that the dimension element's
body is a string with three parts separated by slashes
("/"):
2003-10-23/2003-11-01/P1D
The first part indicates the first date and time of the sequence,
the second part the final date and time, and the third part specifies
the period (P) of the images between. In this case, the
period is one day (P1D). What all this tells us is that
the server holds separate images corresponding to every day from
October 23, 2003 to November 1, 2003. We
can request any or all of those.
Other animations might have dimensions in days, hours, months, or minutes, or a combination of those. They may have dimensions other than time, too, but I don't go into that here. The full syntax for dimensions is described in the WMS specification.
Example 1 shows only some of the information about
layer 3132_21145. The full entry in the capabilities
document also contains an Abstract element that gives a more
elaborate textual description of the connection between the heavy
rainfall and the mudslides. Also there are keywords, attribution (to
NASA), a URL to the logo to go with that, and a URL to retrieve a
legend image identifying the meaning of the colors in the images. The
SVS server documentation has more detailed examples and a description of
layer definitions. That documentation is at
the SVS
document site [20].The EarthFlicks source code contains classes to
download, parse, and make sense of the server's capabilities
document.
The individual images of an SVS animation are retrieved one by one
with a separate WMS GetMap request for each image in the sequence. In
most cases, those requests vary only in the date and time of the image
they ask for.
Example 2 contains another excerpt from the SVS capabilities
document. It describes an animation illustrating the changes in the
global biosphere between August 1997 and July 2003. Notice the
Dimension element, which is much more complex than the
one described in the previous section. Here, there are seven date
ranges rather than one; each is separated by a comma. The seven ranges
each specify contiguous dates, but those ranges are not contiguous
with each other. They are in chronological order,
however. Note, too, that no times are given in these dimensions, only
dates.
Example 2. The partial Layer entry for the Global Biosphere animation
<Layer opaque="1" noSubsets="1"
fixedWidth="2048" fixedHeight="1024">
<Name>2914_17554</Name>
<Title>Global Biosphere from August, 1997 to July, 2003
(2048x1024 Animation)</Title>
<EX_GeographicBoundingBox>
<westBoundLongitude>-180</westBoundLongitude>
<eastBoundLongitude>180</eastBoundLongitude>
<southBoundLatitude>-90</southBoundLatitude>
<northBoundLatitude>90</northBoundLatitude>
</EX_GeographicBoundingBox>
<Dimension name="time" units="ISO8601"
default="2003-07-20">
1997-08-13/1997-12-27/P8D,
1998-01-01/1998-12-27/P8D,
1999-01-01/1999-12-27/P8D,
2000-01-01/2000-12-26/P8D,
2001-01-01/2001-12-27/P8D,
2002-01-01/2002-12-27/P8D,
2003-01-01/2003-07-20/P8D</Dimension>
</Layer>
The WMS request for the first image in the sequence of Table 2 is this:
If you click this link, you should eventually see a map of the world with some pretty colors on it, as shown in Figure 5. It's the first image we're going to wrap around a globe later. The image is three and a half megabytes in size, so it takes a while to download if you're on a slow connection.

Figure 5.
The first image of the Global Biosphere animation
The WMS request for this image is very similar to that for the
California Fires sequence of the previous section. The parameter names
are the same, but the values differ for LAYERS, WIDTH, HEIGHT, BBOX,
and TIME. The layer name, image width, image height, and latitudes and
longitudes are different, and the request asks for a time specific to
this animation.
The request to retrieve the next image in the sequence is this one:
The only difference from the request for the first image is
the TIME, which is eight days later than the previous
time. We know to ask for this date because the layer's period
specification in the first range of the dimension element
is P8D. (See Table 2.) SVS will currently only return
images in PNG format. Even so, that format must be included in the map
request: FORMAT=image/png.
EarthFlicks retrieves all of an animation's images by looping
through its dimensions and issuing a request for each
image. It uses an internal class named MapRequest, which
is passed the individual parameter values and then creates the full
URL for the request. Images are retrieved by creating
a java.net.URL object and invoking
its openConnection() method. See the EarthFlicks
Retriever class in Example 3 for the code that does this.
As you've seen, images can be multi-megabytes of data. Retrieving
them is not something to be doing in an application's user-interface
thread. EarthFlicks performs the retrieval in a separate
thread. The Retriever class establishes the connection
for each image's retrieval and reads the returned stream to capture and
save the image bits. Example 3 shows the run() method of
the Retriever class, where this is done.
Example 3.
The run() method of the EarthFlicks Retriever class
package EarthFlicks;
public abstract class Retriever implements Runnable
{
public class Status
{
public static final int NOT_STARTED = 0;
public static final int CONNECTING = 1;
public static final int RETRIEVING = 2;
public static final int POSTPROCESSING = 3;
public static final int DONE = 4;
public static final int INTERRUPTED = 10;
public static final int ERROR = 11;
}
// These change with each call to start(),
// some change as retrieval progresses.
private int status = Retriever.Status.NOT_STARTED;
private int bytesRead;
private int contentLength;
private String contentType;
private Exception error;
private Thread thread;
protected String destination;
// These variables are set by the client.
private java.net.URL url;
private Object clientObject;
protected RetrieverClient client;
public void start()
{
this.reset();
this.thread = new Thread(this);
this.thread.start();
}
public void run()
{
try
{
this.status = Status.CONNECTING;
this.begin();
if (Controller.getCaches().containsUrl(this.getUrl()))
{
java.io.File destFile =
Controller.getCaches().getFile(this.getUrl());
this.destination = destFile.getPath();
this.contentLength = (int)destFile.length();
this.bytesRead = this.contentLength;
this.status = Status.POSTPROCESSING;
}
else
{
// Retrieve the contents.
int numBytesRead = 0;
java.net.URLConnection connection =
this.url.openConnection();
connection.setAllowUserInteraction(true);
java.io.InputStream incoming =
connection.getInputStream();
this.contentLength = connection.getContentLength();
this.contentType = connection.getContentType();
java.io.File destFile
java.io.File.createTempFile("EFL_",
makeSuffix(this.contentType));
destFile.deleteOnExit(); // It's copied later.
this.destination = destFile.getPath();
java.io.FileOutputStream outgoing =
new java.io.FileOutputStream(destFile);
this.status = Status.RETRIEVING;
byte[] buffer = new byte[4096];
try
{
while (!Thread.currentThread().isInterrupted()
&& numBytesRead >= 0)
{
numBytesRead = incoming.read(buffer);
if (numBytesRead > 0)
{
this.bytesRead += numBytesRead;
outgoing.write(buffer, 0, numBytesRead);
this.update();
}
}
}
finally
{
if (incoming != null)
try {incoming.close();}
catch (java.io.IOException e) {};
if (outgoing != null)
try {outgoing.close();}
catch (java.io.IOException e) {};
}
this.status = Status.POSTPROCESSING;
}
}
catch (Exception e)
{
this.status = Status.ERROR;
this.error = e;
}
finally
{
if (Thread.currentThread().isInterrupted())
this.status = Status.INTERRUPTED;
this.update();
this.inThreadEnd();
if (this.status == Status.POSTPROCESSING)
this.status = Status.DONE;
this.end();
}
}
Before issuing an HTTP request to the server to retrieve an image,
the run() method checks to see whether that image is in a
local image cache that EarthFlicks maintains. Many of the animations
consist of tens or hundreds of images. To play an animation at any
useful frequency those images must be local, and probably on the
computer's local disk rather than even a fast local network.
The code for the image cache is included in this article's code
bundle.
Notice that the run() method of Retriever
includes calls to methods
named begin(), update(),
and end(). These methods cause invocation of callback
methods in the retriever's client, the code that created and started
the retriever, so that the client code can update progress bars and
status readouts and otherwise monitor the retrieval. Example 4 below
shows these method implementations in Retriever. Since
the retrieval is running in a thread separate from the UI, the
callbacks cannot be invoked from the retrieval thread. They must be
invoked from the UI thread if the client is to safely update the user
interface. They are therefore only requested by Retriever to be
invoked later in the UI thread. The methods that actually invoke the
client's callbacks
are doBegin(), doUpdate(),
and doEnd(), also listed in Example 4.
Example 4.
The callback methods of the EarthFlicks Retriever class
private void begin()
{
final RetrieverEvent event = this.makeEvent();
java.awt.EventQueue.invokeLater(
new Runnable()
{
public void run() {doBegin(event);}
}
);
}
private void update()
{
final RetrieverEvent event = this.makeEvent();
java.awt.EventQueue.invokeLater(
new Runnable()
{
public void run() {doUpdate(event);}
}
);
}
private void end()
{
final RetrieverEvent event = this.makeEvent();
java.awt.EventQueue.invokeLater(
new Runnable()
{
public void run() {doEnd(event);}
}
);
}
protected void doBegin(RetrieverEvent event)
{
if (this.getClient() != null)
this.getClient().begin(event);
}
protected void doUpdate(RetrieverEvent event)
{
if (this.getClient() != null)
this.getClient().update(event);
}
protected void doEnd(RetrieverEvent event)
{
if (this.getClient() != null)
this.getClient().end(event);
}
At the point an image is fully retrieved, it could be read and
translated by the javax.imageio.ImageIO class and
displayed in a window as a java.awt.Image. But it would
be only two-dimensional, like the pictures above. The SVS images are
most compelling when wrapped around a 3D globe. That requires
conversion of the image to a texture map that can be displayed with
OpenGL using the Java bindings for OpenGL API (JOGL [17]).
Two things must be done to convert a PNG image to a texture map:
OpenGL wants RGB or RGBA images. They can be in any one of many possible bit depths and color precisions. In EarthFlicks I convert the PNG images to 3- or 4-component unsigned-byte format, using 3-component RGB for images without transparency and 4-component RGBA for images with transparency. Whether or not the PNG image contains transparency values is indicated in its image file.
EarthFlicks opens the image with
the javax.imageio.ImageIO class, which copies the image
into a
java.awt.image.BufferedImage object, and then reads the buffered image
one pixel at a time and writes to a new file an unsigned-byte for each of the
pixel's red, green, blue, and alpha components. The code is shown in Example 5,
which lists a method in the MapRetriever class derived from the
Retriever class described in the previous section.
Example 5.
MapRetriever code converting a retrieved image to RGB or RGBA
private void convertToRgb(RetrieverEvent event) throws Exception
{
// Read the image.
java.awt.image.BufferedImage image =
javax.imageio.ImageIO.read(new
java.io.File(event.getDestination()));
// Transform the image's orientation to that of OpenGL.
java.awt.geom.AffineTransform xform =
java.awt.geom.AffineTransform.getScaleInstance(1, -1);
xform.translate(0, -image.getHeight(null));
java.awt.image.AffineTransformOp op = new
java.awt.image.AffineTransformOp(xform,
java.awt.image.AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
image = op.filter(image, null);
// Convert the image to RGB or RGBA.
java.awt.image.Raster raster = image.getRaster();
int width = image.getWidth();
int height = image.getHeight();
java.awt.image.ColorModel colorModel = image.getColorModel();
byte[] convertedImage = null;
String suffix = "." + Integer.toString(width) + "x"
+ Integer.toString(height);
if (colorModel.hasAlpha())
{
suffix += ".rgba";
int size = 4 * width * height;
convertedImage = new byte[size];
int index = 0;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
Object pixel = raster.getDataElements(x, y, null);
byte red = (byte)colorModel.getRed(pixel);
byte green = (byte)colorModel.getGreen(pixel);
byte blue = (byte)colorModel.getBlue(pixel);
byte alpha = (byte)colorModel.getAlpha(pixel);
convertedImage[index++] = red;
convertedImage[index++] = green;
convertedImage[index++] = blue;
convertedImage[index++] = alpha;
}
}
}
else
{
suffix += ".rgb";
int size = 3 * width * height;
convertedImage = new byte[size];
int index = 0;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
Object pixel = raster.getDataElements(x, y, null);
byte red = (byte)colorModel.getRed(pixel);
byte green = (byte)colorModel.getGreen(pixel);
byte blue = (byte)colorModel.getBlue(pixel);
convertedImage[index++] = red;
convertedImage[index++] = green;
convertedImage[index++] = blue;
}
}
}
// Save the converted image to a file.
java.io.File newDest =
java.io.File.createTempFile("EFL_", suffix);
newDest.deleteOnExit(); // Cached later if needed.
java.io.FileOutputStream outgoing = null;
try
{
outgoing = new java.io.FileOutputStream(newDest);
outgoing.write(convertedImage);
}
finally
{
if (outgoing != null)
try {outgoing.close();}
catch (java.io.IOException e) {};
}
this.destination = newDest.getPath();
}
Performing the conversion this way and forming unsigned-byte color
components eliminates any headaches from dealing with format
variations. The ImageIO class takes care of converting the colors from
whatever form they're in to simple red, green, blue, and alpha
eight-bit values. In fact, this scheme will work with any file format
that ImageIO can read, not just PNG. The disadvantage to this scheme
is that it's probably the slowest way to do it. Some format variants
map one to one with RGB or RGBA when read by ImageIO, but EarthFlicks
makes no attempt to detect or take advantage of those situations.
In Example 5, the image is also "flipped" so that the origin is at the lower left rather than the original upper left. Most image formats have an upper-left origin, but OpenGL assumes a lower-left.
The result of this conversion is an RGB or RGBA image captured in
a BufferedImage and ready for its dimensions to be scaled
to powers of two if necessary, using
OpenGL's glScaleImage() function. This function scales an
image to any power of two, but EarthFlicks has it scale to the next
largest for the dimension (360 goes to 512, for example). The
conversion and scaling steps produce an RGB or RGBA image suitable for
OpenGL to use as a texture map. EarthFlicks saves these images as
files on the local disk, with a filename suffix of .rgb
or .rgba. Their widths and heights are encoded in their
file names.
SVS images typically do not come with an underlying image of the Earth's surface showing through. This is not a problem for images that completely cover the globe. But many SVS animations make no sense unless they're superimposed on a globe with base imagery of Earth terrain. EarthFlicks addresses this by implementing an Earth model that's covered with NASA's Blue Marble imagery, shown in Figure 6.

Figure 6.
The Blue Marble imagery
Several resolutions of the Blue Marble imagery are available. EarthFlicks uses three of those, automatically switching to the most appropriate and efficient as the user zooms in and out.
The SVS images can
be overlaid onto the Earth's surface. There are many ways to do
this. I chose to create a solid sphere matching the base globe, and
apply the SVS images as texture maps to that. The code for this is in
the EarthFlicks Shell class, in the Globe
package. That class computes the vertices of triangle strips to
represent the surface, and assigns appropriate texture coordinates to
fit each SVS image to the correct latitudes and longitudes.
Applying the texture to the shell is both simplified and
complicated by the texture cache EarthFlicks uses. To best use the
computer's graphics memory, EarthFlicks maintains a cache that keeps
the most recently used textures in memory. This cache is responsible
for opening texture files when they are first needed, managing them in memory,
and binding them to OpenGL texture
identifiers. The TextureCache class does all of this. The
code for that class also shows how EarthFlicks specifies textures and
makes them current in OpenGL.
I've found that the fastest way to get an image from disk to memory
is by memory mapping the image
file. Java's java.nio.MappedByteBuffer makes this very
easy. EarthFlicks maintains a singleton mapped-file cache that maps
and manages memory-mapped image files. The implementation is in
the MappedFileCache class in the Globe
package.
This mapped-file cache is the third cache I've described for EarthFlicks. To recap, there's one cache to hold the converted images to be used as texture maps permanently on disk. There's another cache to manage run-time texture loading and binding to OpenGL. Finally, there's a third cache to manage memory-mapped image files.
At this point, there exist sequences of images from the SVS server converted from PNG to images that can be used by OpenGL as texture maps, all cached on a local disk. There's also code to map these images into memory, bind them to OpenGL, and display them on a globe with a pretty Blue Marble background surface. But these images do not "play" as animations yet.
Playing the images is probably the simplest part of all. The ImagePlayer class in EarthFlicks accepts an
image sequence and plays it by setting a timer and displaying
consecutive images of the animation at each timer
tick. Its start() method kicks off the animation. When
all the images in the sequence have been displayed, ImagePlayer
invokes a callback to EarthFlicks' Controller class to return the user
interface to the pre-animation state, ready for the animation to be
run again.
If an image sequence is specific to a region of the
globe, ImagePlayer positions the viewpoint over that
region before playing the animation. It also has methods to stop,
pause, and resume the animation, and to remove it from the globe.
One last thing to do is provide a means for the user to turn the globe and zoom in and out. EarthFlicks implements a very simple mechanism for this: a rectangular region defined by latitude and longitude is defined as a viewing window onto the globe. Moving this region makes the globe appear to rotate. Expanding or contracting it has the effect of zooming in and out. This is not a full 3D manipulation model that allows the user to "fly around" in any orientation. The user is always looking directly at the globe, and the viewing direction is always perpendicular to the Earth's surface. If EarthFlicks were to more realistically represent the Earth by applying surface elevation, this manipulation model would be inadequate.
This manipulation scheme does have a couple of advantages: it allows for a very quick, high-level clipping scheme to avoid drawing regions of the globe that are not in view. And it keeps the user oriented to the globe so that "up" is always north.
The SVS animations are designed to be used in visualization programs for education. I've described in this article how to retrieve the images composing those animations and display them on a 3D model of the Earth. There isn't room here to describe or even list all the code involved in doing that, so I've tried to identify, explain, and illustrate the tasks unique to this type of application and the SVS imagery. Much of this information, of course, can also apply to images and image sequences available from other WMS servers.
The author can be reached at
Creation of this article and the example application was funded by NASA.
Links:
[1] http://www.java.net/author/tom-gaskins
[2] http://www.java.net/article/2005/07/21/earth-animations-education-nasa#scientific_visualization_studio
[3] http://www.java.net/article/2005/07/21/earth-animations-education-nasa#svs_animations
[4] http://www.java.net/article/2005/07/21/earth-animations-education-nasa#earthflicks
[5] http://www.java.net/pub/a/today/2005/08/16/earth.html?page=2#step_1
[6] http://www.java.net/pub/a/today/2005/08/16/earth.html?page=2#describing_capabilities_entry
[7] http://www.java.net/pub/a/today/2005/08/16/earth.html?page=3#step_2
[8] http://www.java.net/pub/a/today/2005/08/16/earth.html?page=3#threaded_retrieval
[9] http://www.java.net/pub/a/today/2005/08/16/earth.html?page=4#step_3
[10] http://www.java.net/pub/a/today/2005/08/16/earth.html?page=5#step_4
[11] http://www.java.net/pub/a/today/2005/08/16/earth.html?page=5#playing_with_globe
[12] http://www.java.net/pub/a/today/2005/08/16/earth.html?page=5#conclusion
[13] http://www.java.net/pub/a/today/2005/08/16/earth.html?page=5#resources
[14] http://aes.gsfc.nasa.gov/documents/available.html
[15] http://aes.gsfc.nasa.gov
[16] http://worldwind.arc.nasa.gov
[17] http://www.java.net/%3Ccs_comment
[18] http://www.opengeospatial.org/specs/?page=specs
[19] http://www.codeproject.com/csharp/WMSOverviewAll.asp
[20] http://aes.gsfc.nasa.gov/documents/overview.html
[21] http://aes.gsfc.nasa.gov/cgi-bin/wms?SERVICE=WMS&REQUEST=GetMap&LAYERS=2914_17554&FORMAT=image/png&WIDTH=2048&HEIGHT=1024&CRS=CRS:84&BBOX=-180,-90,180,90&TIME=1997-08-13
[22] http://aes.gsfc.nasa.gov/
[23] http://opengl.org
[24] https://jogl.dev.java.net/
[25] http://www.opengeospatial.org/specs/
[26] http://java.sun.com/j2se/1.4.2/docs/guide/nio/index.html