// **********************************************************************
//
// <copyright>
//
// BBN Technologies
// 10 Moulton Street
// Cambridge, MA 02138
// (617) 873-8000
//
// Copyright (C) BBNT Solutions LLC. All rights reserved.
//
// </copyright>
// **********************************************************************
//
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/layer/daynight/DayNightLayer.java,v $
// $RCSfile: DayNightLayer.java,v $
// $Revision: 1.12 $
// $Date: 2006/04/07 17:36:01 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.layer.daynight;
/* Java Core */
import java.awt.Color;
import java.awt.event.ActionListener;
import java.util.Properties;
import javax.swing.Timer;
import com.bbn.openmap.I18n;
import com.bbn.openmap.MoreMath;
import com.bbn.openmap.event.ProjectionListener;
import com.bbn.openmap.layer.OMGraphicHandlerLayer;
import com.bbn.openmap.omGraphics.OMCircle;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.omGraphics.OMRaster;
import com.bbn.openmap.proj.Cylindrical;
import com.bbn.openmap.proj.Length;
import com.bbn.openmap.proj.Projection;
import com.bbn.openmap.proj.coords.LatLonPoint;
import com.bbn.openmap.util.Debug;
import com.bbn.openmap.util.PropUtils;
/**
* The DayNightLayer is a layer that draws the day/Night terminator on
* the map. When the layer is re-projected, it figures out the
* brightest point on the earth (closest to the sun), and creates an
* image that has daytime pixels clear and the nighttime pixels
* shaded. There are a couple of options available for the layer. The
* terminator can be faded from light to dark, and the width of the
* fading can be adjusted. The color of the shading can be changed.
* The shading can reflect the current time, or be set to display the
* shading of a specified time. A time interval can be set to have the
* layer automatically update at regular intervals.
*
* <P>
* The openmap.properties file can control the layer with the
* following settings:<pre>
*
* # These are all optional, and can be omitted if you want to use the defaults.
* # draw terminator as poly (faster calculation than image,
* # defaults to true).
* daynight.doPolyTerminator=true
* # number of vertices for polygon terminator line. this is only valid
* # if doPolyTerminator is true...
* daynight.terminatorVerts=360
* # termFade - the distance of the transition of fade, as a percentage of PI.
* daynight.termFade=.1
* # currentTime - true to display the shading at the computer's current time.
* daynight.currentTime=true
* # overlayTime - time, in milliseconds from java/unix epoch, to set the layer
* # time being displayed. currentTime has to be false for this to be used.
* daynight.overlayTime=919453689000
* # updateInterval - time in milliseconds between updates. currentTime has to be
* # true for this to be used.
* daynight.updateInterval=300000
* # Color of the shading (32bit Hex ARGB)
* daynight.nighttimeColor=64000000
*
* </pre><p> In addition, you can get this layer to work with the
* OpenMap viewer by editing your openmap.properties file: <pre>
*
* # layers
* openmap.layers=daynight ...
* # class
* daynight.class=com.bbn.openmap.layer.daynight.DayNightLayer
* # name
* daynight.prettyName=Day/Night Shading
*
* </pre>
*
*/
public class DayNightLayer extends OMGraphicHandlerLayer implements
ProjectionListener, ActionListener {
/**
* Default value of fade to the terminator line, set to .10f. This
* means that the last 10% of the horizon will be faded out.
*/
public static final float DEFAULT_TERM_FADE = .10f;
/**
* Default update interval, which is never - updates occur on
* re-projections.
*/
public static final int DO_NOT_UPDATE = -1;
/** The color of daytime - default is white and clear. */
protected Color daytimeColor = new Color(0x00FFFFFF, true);
/** the color of darkness - default is black. */
protected Color nighttimeColor = new Color(0x7F000000, true);
/**
* Percentage of the distance from the horizon to the brightest
* point to start fading to darkness. Expected to be between 0.0
* and 0.5.
*/
protected float termFade = DEFAULT_TERM_FADE;
/**
* If true, the layer will set the darkness according to the
* current time.
*/
protected boolean currentTime = true;
/**
* The time used to create the layer, in milliseconds from
* java/unix epoch.
*/
protected long overlayTime = 0;
/**
* Update interval to automatically update the layer, in
* milli-seconds
*/
protected int updateInterval = 300000;
/** Update timer. */
protected Timer timer;
/**
* Create the terminator line as a polygon.
*/
protected boolean doPolyTerminator = true;
/**
* The number of vertices of the polygon terminator line.
*/
protected int terminatorVerts = 360;
/////// Properties
public static final String DaytimeColorProperty = "daytimeColor";
public static final String NighttimeColorProperty = "nighttimeColor";
public static final String TermFadeProperty = "termFade";
public static final String CurrentTimeProperty = "useCurrentTime";
public static final String OverlayTimeProperty = "overlayTime";
public static final String UpdateIntervalProperty = "updateInterval";
public static final String DoPolyTerminatorProperty = "doPolyTerminator";
public static final String TerminatorVertsProperty = "terminatorVerts";
/**
* The default constructor for the Layer. All of the attributes
* are set to their default values.
*/
public DayNightLayer() {
setName("Day-Night");
}
/**
* The properties and prefix are managed and decoded here, for the
* standard uses of the DayNightLayer.
*
* @param prefix string prefix used in the properties file for
* this layer.
* @param properties the properties set in the properties file.
*/
public void setProperties(String prefix, java.util.Properties properties) {
super.setProperties(prefix, properties);
prefix = PropUtils.getScopedPropertyPrefix(prefix);
overlayTime = PropUtils.longFromProperties(properties, prefix
+ OverlayTimeProperty, overlayTime);
if (overlayTime <= 0) {
currentTime = true;
}
currentTime = PropUtils.booleanFromProperties(properties, prefix
+ CurrentTimeProperty, currentTime);
updateInterval = PropUtils.intFromProperties(properties, prefix
+ UpdateIntervalProperty, updateInterval);
if (updateInterval > 0) {
timer = new Timer(updateInterval, this);
}
termFade = PropUtils.floatFromProperties(properties, prefix
+ TermFadeProperty, termFade);
if (termFade < 0 || termFade >= .5) {
Debug.output("DayNightLayer: termFade funky value ignored.");
termFade = DEFAULT_TERM_FADE;
}
daytimeColor = (Color) PropUtils.parseColorFromProperties(properties,
prefix + DaytimeColorProperty,
daytimeColor);
nighttimeColor = (Color) PropUtils.parseColorFromProperties(properties,
prefix + NighttimeColorProperty,
nighttimeColor);
doPolyTerminator = PropUtils.booleanFromProperties(properties, prefix
+ DoPolyTerminatorProperty, doPolyTerminator);
terminatorVerts = PropUtils.intFromProperties(properties, prefix
+ TerminatorVertsProperty, terminatorVerts);
}
public Properties getProperties(Properties props) {
props = super.getProperties(props);
String prefix = PropUtils.getScopedPropertyPrefix(this);
props.put(prefix + OverlayTimeProperty, Long.toString(overlayTime));
props.put(prefix + CurrentTimeProperty,
new Boolean(currentTime).toString());
props.put(prefix + UpdateIntervalProperty,
Integer.toString(updateInterval));
props.put(prefix + TermFadeProperty, Float.toString(termFade));
props.put(prefix + DaytimeColorProperty,
Integer.toHexString(daytimeColor.getRGB()));
props.put(prefix + NighttimeColorProperty,
Integer.toHexString(nighttimeColor.getRGB()));
props.put(prefix + DoPolyTerminatorProperty,
new Boolean(doPolyTerminator).toString());
props.put(prefix + TerminatorVertsProperty,
Integer.toString(terminatorVerts));
return props;
}
public Properties getPropertyInfo(Properties props) {
props = super.getPropertyInfo(props);
String interString;
interString = i18n.get(DayNightLayer.class,
OverlayTimeProperty,
I18n.TOOLTIP,
"The time used to create the layer, in milliseconds from java/unix epoch (leave empty for current time).");
props.put(OverlayTimeProperty, interString);
interString = i18n.get(DayNightLayer.class,
OverlayTimeProperty,
OverlayTimeProperty);
props.put(OverlayTimeProperty + LabelEditorProperty, interString);
interString = i18n.get(DayNightLayer.class,
CurrentTimeProperty,
I18n.TOOLTIP,
"If true, the layer will set the darkness according to the current time.");
props.put(CurrentTimeProperty, interString);
interString = i18n.get(DayNightLayer.class,
CurrentTimeProperty,
CurrentTimeProperty);
props.put(CurrentTimeProperty + LabelEditorProperty, interString);
props.put(CurrentTimeProperty + ScopedEditorProperty,
"com.bbn.openmap.util.propertyEditor.YesNoPropertyEditor");
interString = i18n.get(DayNightLayer.class,
UpdateIntervalProperty,
I18n.TOOLTIP,
"Update interval to automatically update the layer, in milli-seconds.");
props.put(UpdateIntervalProperty, interString);
interString = i18n.get(DayNightLayer.class,
UpdateIntervalProperty,
UpdateIntervalProperty);
props.put(UpdateIntervalProperty + LabelEditorProperty, interString);
interString = i18n.get(DayNightLayer.class,
TermFadeProperty,
I18n.TOOLTIP,
"Percentage of the distance from the horizon to the brightest point to start fading to darkness, 0.0 to 0.5.");
props.put(TermFadeProperty, interString);
interString = i18n.get(DayNightLayer.class,
TermFadeProperty,
TermFadeProperty);
props.put(TermFadeProperty + LabelEditorProperty, interString);
interString = i18n.get(DayNightLayer.class,
DaytimeColorProperty,
I18n.TOOLTIP,
"Color for the daytime area, if polygon terminator isn't used.");
props.put(DaytimeColorProperty, interString);
interString = i18n.get(DayNightLayer.class,
DaytimeColorProperty,
DaytimeColorProperty);
props.put(DaytimeColorProperty + LabelEditorProperty, interString);
props.put(DaytimeColorProperty + ScopedEditorProperty,
"com.bbn.openmap.util.propertyEditor.ColorPropertyEditor");
interString = i18n.get(DayNightLayer.class,
NighttimeColorProperty,
I18n.TOOLTIP,
"Color for the nighttime area.");
props.put(NighttimeColorProperty, interString);
interString = i18n.get(DayNightLayer.class,
NighttimeColorProperty,
NighttimeColorProperty);
props.put(NighttimeColorProperty + LabelEditorProperty, interString);
props.put(NighttimeColorProperty + ScopedEditorProperty,
"com.bbn.openmap.util.propertyEditor.ColorPropertyEditor");
interString = i18n.get(DayNightLayer.class,
DoPolyTerminatorProperty,
I18n.TOOLTIP,
"Render with polygon instead of image (it's faster).");
props.put(DoPolyTerminatorProperty, interString);
interString = i18n.get(DayNightLayer.class,
DoPolyTerminatorProperty,
DoPolyTerminatorProperty);
props.put(DoPolyTerminatorProperty + LabelEditorProperty, interString);
props.put(DoPolyTerminatorProperty + ScopedEditorProperty,
"com.bbn.openmap.util.propertyEditor.YesNoPropertyEditor");
interString = i18n.get(DayNightLayer.class,
TerminatorVertsProperty,
I18n.TOOLTIP,
"Number of vertices of the polygon terminator (more is smoother).");
props.put(TerminatorVertsProperty, interString);
interString = i18n.get(DayNightLayer.class,
TerminatorVertsProperty,
TerminatorVertsProperty);
props.put(TerminatorVertsProperty + LabelEditorProperty, interString);
props.put(initPropertiesProperty, CurrentTimeProperty + " "
+ OverlayTimeProperty + " " + UpdateIntervalProperty + " "
+ NighttimeColorProperty + " " + DoPolyTerminatorProperty + " "
+ TerminatorVertsProperty + " " + DaytimeColorProperty + " "
+ TermFadeProperty + " " + RemovableProperty + " "
+ AddAsBackgroundProperty);
return props;
}
/**
* Handle an ActionEvent from the Timer.
*
* @param ae action event from the timer.
*/
public void actionPerformed(java.awt.event.ActionEvent ae) {
super.actionPerformed(ae);
if (Debug.debugging("daynight")) {
Debug.output(getName() + "| updating image via timer...");
}
doPrepare();
}
/**
* Create the OMGraphic that acts as an overlay showing the
* day/night terminator. The brightest spot on the earth is
* calculated, and then each pixel is inverse projected to find
* out its coordinates. Then the great circle distance is
* calculated. The terminator is assumed to be the great circle
* where all the points are PI/2 away from the bright point. If
* the termFade variable is set, then the difference in color over
* the terminator is feathered, on equal amount of the terminator.
*
* @param projection the projection of the screen,
* @return OMGraphic containing image to use for the layer. The
* image has been projected.
*/
protected OMGraphic createImage(Projection projection) {
if (currentTime)
overlayTime = System.currentTimeMillis();
if (Debug.debugging("daynight")) {
Debug.output("DayNightLayer: Calculating sun position at time "
+ Long.toString(overlayTime));
}
LatLonPoint brightPoint = SunPosition.sunPosition(overlayTime);
Debug.message("daynight", "DayNightLayer: Calculated sun position");
// Do a fast and relatively inexpensive calculation of the
// terminator. NOTE: for non-cylindrical projections we don't
// create a full-hemisphere circle so that we don't get
// flip-rendering problem...
if (doPolyTerminator) {
Debug.message("daynight",
"DayNightLayer: Creating polygon terminator");
LatLonPoint darkPoint = brightPoint.getPoint(Math.PI, Math.PI / 4);
OMCircle circle = new OMCircle((float)darkPoint.getY(), (float)darkPoint.getX(), (projection instanceof Cylindrical) ? 90f
: 89.0f,//HACK
Length.DECIMAL_DEGREE, terminatorVerts);
circle.setPolarCorrection(true);
circle.setFillPaint(nighttimeColor);
circle.setLinePaint(nighttimeColor);
circle.generate(projection);
Debug.message("daynight",
"DayNightLayer: Done creating polygon terminator");
return circle;
}
int width = projection.getWidth();
int height = projection.getHeight();
int[] pixels = new int[width * height];
OMRaster ret = new OMRaster((int) 0, (int) 0, width, height, pixels);
Debug.message("daynight", getName()
+ "|createImage: Center of bright spot lat= "
+ brightPoint.getLatitude() + ", lon= "
+ brightPoint.getLongitude());
// Light is clear and/or white
int light = daytimeColor.getRGB();
// Allocate the memory here for the testPoint
LatLonPoint testPoint = new LatLonPoint.Float(0f, 0f);
// great circle distance between the bright point and each
// pixel.
double distance;
// Set the darkeness value
int dark = nighttimeColor.getRGB();// ARGB
int darkness = dark >>> 24;// darkness alpha
int value;
// Calculate the fae limits around the terminator
float upperFadeLimit = (float) (MoreMath.HALF_PI * (1.0 + termFade));
float lowerFadeLimit = (float) (MoreMath.HALF_PI * (1.0 - termFade));
int fadeColorValue = 0x00FFFFFF & (dark); // RGB
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
projection.inverse(i, j, testPoint);
distance = brightPoint.distance(testPoint);
if (distance > upperFadeLimit) {
pixels[j * width + i] = dark;
} else if (distance > lowerFadeLimit) {
value = (int) (darkness * (1 - ((upperFadeLimit - distance) / (upperFadeLimit - lowerFadeLimit))));
value <<= 24;
pixels[j * width + i] = fadeColorValue | value;
} else {
pixels[j * width + i] = light;
}
}
}
ret.generate(projection);
return ret;
}
/**
* Prepares the graphics for the layer. This is where the
* getRectangle() method call is made on the location.
* <p>
* Occasionally it is necessary to abort a prepare call. When this
* happens, the map will set the cancel bit in the LayerThread,
* (the thread that is running the prepare). If this Layer needs
* to do any cleanups during the abort, it should do so, but
* return out of the prepare asap.
*
*/
public synchronized OMGraphicList prepare() {
OMGraphicList list = getList();
if (list == null) {
list = new OMGraphicList();
} else {
list.clear();
}
Debug.message("basic", getName() + "|DayNightLayer.prepare(): doing it");
OMGraphic ras = createImage(getProjection());
if (timer != null)
timer.restart();
list.add(ras);
return list;
}
/**
* Get the time of the overlay.
*/
public long getOverlayTime() {
return overlayTime;
}
/**
* Set the time for the overlay.
*/
public void setOverlayTime(long ot) {
overlayTime = ot;
currentTime = false;
doPrepare();
}
/**
* Returns whether the layer will set the overlayTime to the time
* the image is created.
*/
public boolean getCurrentTime() {
return currentTime;
}
/**
* Set whether the layer should set the overlayTime to the time
* the image is created. If the time is being set to reflect a
* time other than the current time, this needs to be set to
* false. It actually is, if you manually set the overlay time.
*/
public void setCurrentTime(boolean ct) {
currentTime = ct;
}
/**
* Get the timer being used for automatic updates. May be null if
* a timer is not set.
*/
public Timer getTimer() {
return timer;
}
/**
* If you want the layer to update itself at certain intervals,
* you can set the timer to do that. Set it to null to disable it.
*/
public void setTimer(Timer t) {
timer = t;
}
}