src.gov.nasa.worldwind.ogc.kml.KMLRegion Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of worldwindx Show documentation
Show all versions of worldwindx Show documentation
World Wind is a collection of components that interactively display 3D geographic information within Java applications or applets.
/*
* Copyright (C) 2012 United States Government as represented by the Administrator of the
* National Aeronautics and Space Administration.
* All Rights Reserved.
*/
package gov.nasa.worldwind.ogc.kml;
import gov.nasa.worldwind.*;
import gov.nasa.worldwind.cache.ShapeDataCache;
import gov.nasa.worldwind.event.Message;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.ogc.kml.impl.*;
import gov.nasa.worldwind.render.DrawContext;
import gov.nasa.worldwind.util.*;
import java.util.*;
/**
* Represents the KML Region element and provides access to its contents. Regions define an area of interest
* described by a geographic bounding box and an optional minimum and maximum altitude.
*
* Bounding Box A Region's bounding box controls when the Region is active by defining a volume
* that must intersect the viewing frustum. The bounding box is computed according to the altitudeMode
* attribute of a Region's geographic LatLonAltBox
as follows:
*
* - clampToGround (default): The bounding box encloses the terrain surface in the sector
* defined by the north, south, east, and west limits of this Region's
LatLonAltBox
.
* - relativeToGround: The bounding box encloses the volume in the sector defined by the north,
* south, east, and west limits of the Region's
LatLonAltBox
, and who's upper and lower altitude are
* specified by its minAltitude and maxAltitude, relative to ground level. - absolute: The
* bounding box encloses the volume in the sector defined by the north, south, east, and west limits of the Region's
*
LatLonAltBox
, and who's upper and lower altitude are specified by its minAltitude and maxAltitude,
* relative to mean sea level.
*
* Level of Detail
A Region's level of detail determines when it is active by defining an upper
* and lower boundary on the Region's screen area or the Region's distance to the view. The level of detail is computed
* according to the altitudeMode
attribute of a Region's geographic LatLonAltBox
as follows:
*
* - clampToGround (default): The level of detail is determined by computing the distance from
* the eye point and Region sector, scaling the distance by the
KMLTraversalContext's
detail hint, then
* comparing that scaled distance to the Region's min and max pixel sizes in meters (the Region sector's area divided by
* minLodPixels
and maxLodPixels
, respectively). The Region is active when the scaled distance
* is less than or equal to the min pixel size in meters and greater than the max pixel size in meters. The detail hint
* may be specified by calling setDetailHint
on the top level KMLRoot
(the KMLRoot loaded by
* the application). - relativeToGround: The level of detail is determined by computing the
* number of pixels the Region occupies on screen, and comparing that pixel count to the Region's
*
minLodPixels
and maxLodPixels
. The Region is active when the pixel count is greater or
* equal to minLodPixels
and less than maxLodPixels
. - absolute: The
* level of detail is determined by computing the number of pixels the Region occupies on screen, and comparing that
* pixel count to the Region's
minLodPixels
and maxLodPixels
. The Region is active when the
* pixel count is greater or equal to minLodPixels
and less than maxLodPixels
.
*
* In order to prevent Regions with adjacent level of detail ranges from activating at the same time, Region gives
* priority to higher level of detail ranges. For example, suppose that two KML features representing different detail
* levels of a Collada model have Regions with LOD range 100-200 and 200-300. Region avoids activating both features in
* the event that both their level of detail criteria are met by giving priority to the second range: 200-300.
*
* KML Feature Hierarchies
When a Region is attached to a KML feature, the feature and its
* descendants are displayed only when the Region is active. A Region is active when its bounding box is in view and its
* level of detail criteria are met. Region provides the isActive
method for determining if a Region is
* active for a specified DrawContext
.
*
* Regions do not apply directly to KML containers, because a descendant feature can override the container's Region
* with its own Region. If a feature does not specify a Region it inherits the Region of its nearest ancestor. Since a
* child feature's Region may be larger or have a less restrictive level of detail range than its ancestor's Region, the
* visibility of an entire KML feature tree cannot be determined based on a container's Region. Instead, visibility must
* be determined at each leaf feature.
*
* Limitations
The Region bounding box must lie between -90 to 90 degrees latitude, and -180 to
* 180 degrees longitude. Regions that span the date line are currently not supported.
*
* @author tag
* @version $Id: KMLRegion.java 1838 2014-02-05 20:48:12Z dcollins $
*/
public class KMLRegion extends KMLAbstractObject
{
/**
* The default time in milliseconds a RegionData
element may exist in this Region's
* regionDataCache
before it must be regenerated: 6 seconds.
*/
protected static final int DEFAULT_DATA_GENERATION_INTERVAL = 6000;
/**
* The default time in milliseconds a RegionData
element may exist in this Region's
* regionDataCache
without being used before it is evicted: 1 minute.
*/
protected static final int DEFAULT_UNUSED_DATA_LIFETIME = 60000;
/**
* The default value that configures KML scene resolution to screen resolution as the viewing distance changes:
* 2.8.
*/
protected static final double DEFAULT_DETAIL_HINT_ORIGIN = 2.8;
/**
* RegionData
holds a Region's computed data used during a single call to Region.isActive
,
* and is unique to a particular Globe
.
*
* RegionData entries are places in a Region's regionDataCache
, and are retrieved during each call to
* isActive
using the current Globe
as the cache key. RegionData's elements depend on the
* Globe's
ElevationModel
, and therefore cannot be permanently cached. Each RegionData
* entry is valid for a random amount of time between its minExpiryTime
and its
* maxExpiryTime
, after which it must be regenerated. The time is randomized to amortize the cost of
* regenerating data for multiple Regions over multiple frames.
*
* isActive
RegionData's isActive
property indicates whether the Region
* associated with a RegionData entry is active. This is used to share the result of computing isActive
* among multiple calls during the same frame. For example, the preRender and render passes need not each compute
* isActive
, and can therefore share the same computation by ensuring that this property is set at most
* once per frame. Callers determine when to recompute isActive
by comparing the
* DrawContext's
current frame number against the RegionData's activeFrameNumber
. This
* property is accessed by calling isActive
and setActive
.
*
* extent
RegionData's extent
property is an Extent
used to
* determine if a Region's bounding box is in view. This property is accessed by calling getExtent
and
* setExtent
. May be null
.
*
* sector
RegionData's sector
property is a Sector
used to
* determine if Regions with an altitudeMode
of clampToGround
are in view. Accessed by
* calling getSector
and setSector
. When a Region's altitudeMode
is
* clampToGround
, the Region's sector can be used to determine visibility because the Region is defined
* to be on the Globe's
surface.
*
* points
RegionData's points
property indicates a list of model-coordinate
* points representing the corners and interior of the Region. These points are used to determine the distance
* between the Region and the View's
eye point. If the Region has altitude mode of
* clampToGround
, this list must contain five points: the model-coordinate points of the Region's four
* corners and center point on the surface terrain.
*/
protected static class RegionData extends ShapeDataCache.ShapeDataCacheEntry
{
/** Identifies the frame used to calculate this entry's values. Initially -1. */
protected long frameNumber = -1;
/** Identifies the frame used to determine if this entry's Region is active. Initially -1. */
protected long activeFrameNumber = -1;
/** Identifies whether this entry's Region is active. Initially false
. */
protected boolean isActive;
/**
* Indicates the vertical datum against which the altitudes values in this entry's Region are interpreted. One
* of WorldWind.ABSOLUTE
, WorldWind.CLAMP_TO_GROUND
, or
* WorldWind.RELATIVE_TO_GROUND
. Initially -1.
*/
protected int altitudeMode = -1;
/**
* Indicates the Sector
used to determine if a Region who's altitudeMode
is
* clampToGround
is visible. Initially null
.
*/
protected Sector sector;
/**
* Indicates the model-coordinate points representing the corners and interior of this entry's Region. These
* points are used to determine the distance between this entry's Region and the View's
eye point.
* Initially null
.
*/
protected List points;
/**
* Constructs a new RegionData
entry from the Globe
and vertical exaggeration of a
* specified draw context.
*
* @param dc the draw context. Must contain a Globe
.
* @param minExpiryTime the minimum expiration duration, in milliseconds.
* @param maxExpiryTime the maximum expiration duration, in milliseconds.
*/
public RegionData(DrawContext dc, long minExpiryTime, long maxExpiryTime)
{
super(dc, minExpiryTime, maxExpiryTime);
}
/**
* Identifies the frame used to calculate this entry's values.
*
* @return the frame used to calculate this entry's values.
*/
public long getFrameNumber()
{
return frameNumber;
}
/**
* Specifies the frame used to calculate this entry's values.
*
* @param frameNumber the frame used to calculate this entry's values.
*/
public void setFrameNumber(long frameNumber)
{
this.frameNumber = frameNumber;
}
/**
* Identifies the frame used to determine if this entry's Region is active.
*
* @return the frame used to determine if this entry's Region is active.
*/
public long getActiveFrameNumber()
{
return activeFrameNumber;
}
/**
* Specifies the frame used to determine if this entry's Region is active.
*
* @param frameNumber the frame used to determine if this entry's Region is active.
*/
public void setActiveFrameNumber(long frameNumber)
{
this.activeFrameNumber = frameNumber;
}
/**
* Indicates whether this entry's Region is active.
*
* @return true
if this entry's Region is active, otherwise false
.
*/
public boolean isActive()
{
return this.isActive;
}
/**
* Specifies whether this entry's Region is active.
*
* @param active true
to specify that this entry's Region is active, otherwise false
.
*/
public void setActive(boolean active)
{
this.isActive = active;
}
/**
* Indicates the vertical datum against which the altitudes values in this entry's Region are interpreted.
*
* @return the altitude mode of this entry's Region. One of WorldWind.ABSOLUTE
,
* WorldWind.CLAMP_TO_GROUND
, or WorldWind.RELATIVE_TO_GROUND
.
*/
public int getAltitudeMode()
{
return this.altitudeMode;
}
/**
* Specifies the vertical datum against which the altitudes values in this entry's Region should be interpreted.
* Must be one of WorldWind.ABSOLUTE
, WorldWind.CLAMP_TO_GROUND
, or
* WorldWind.RELATIVE_TO_GROUND
.
*
* @param altitudeMode the vertical datum to use.
*/
public void setAltitudeMode(int altitudeMode)
{
this.altitudeMode = altitudeMode;
}
/**
* Indicates the Sector
used to determine if a Region who's altitudeMode
is
* clampToGround
is visible. This returns null
if this entry's Region's has no
* geographic bounding box.
*
* @return the Sector
used to determine if a Region is visible, or null
to specify
* that this entry's Region has no bounding box.
*/
public Sector getSector()
{
return this.sector;
}
/**
* Specifies the Sector
that defines a Region's surface sector on the Globe
. Specify
* null
to indicate that this entry' Region has no geographic bounding box.
*
* @param sector the Sector
that is used to determine if a clampToGround
Region is
* visible, or null
to specify that the entry's Region's has no bounding box.
*/
public void setSector(Sector sector)
{
this.sector = sector;
}
/**
* Indicates the model-coordinate points representing the corners and interior of this entry's Region. This
* returns null
if this entry's Region has no geographic bounding box.
*
* @return the points representing the corners and interior of this entry's Region, or null
if the
* Region has no bounding box.
*/
public List getPoints()
{
return this.points;
}
/**
* Specifies the model-coordinate points representing the corners and interior of this entry's Region. These
* points are used to determine the distance between this entry's Region and the View's
eye point.
* Specify null
to indicate that this entry' Region has no geographic bounding box.
*
* If this entry's Region has altitude mode clampToGround
, this list must contain five points: the
* model-coordinate points of the Region's four corners and center point on the surface terrain.
*
* @param points the points representing the corners and interior of this entry's Region, or null
* to specify that this entry's Region has no bounding box.
*/
public void setPoints(List points)
{
this.points = points;
}
}
/**
* The maximum lifespan of this Region's computed data, in milliseconds. Initialized to
* DEFAULT_DATA_GENERATION_INTERVAL
.
*/
protected long maxExpiryTime = DEFAULT_DATA_GENERATION_INTERVAL;
/**
* The minimum lifespan of this Region's computed data, in milliseconds. Initialized to
* DEFAULT_DATA_GENERATION_INTERVAL - 1000
.
*/
protected long minExpiryTime = Math.max(DEFAULT_DATA_GENERATION_INTERVAL - 1000, 0);
/**
* Holds globe-dependent computed Region data. One entry per globe encountered during isActive
.
* Initialized to a new ShapeDataCache
with maxTimeSinceLastUsed
set to
* DEFAULT_UNUSED_DATA_LIFETIME
.
*/
protected ShapeDataCache regionDataCache = new ShapeDataCache(DEFAULT_UNUSED_DATA_LIFETIME);
/**
* Identifies the active globe-dependent data for the current invocation of isActive
. The active data
* is drawn from the regionDataCache
at the beginning of the isActive
method.
*/
protected RegionData currentData;
/**
* The default value that configures KML scene resolution to screen resolution as the viewing distance changes. The
* KMLRoot's
detail hint specifies deviations from this default. Initially
* DEFAULT_DETAIL_HINT_ORIGIN
.
*/
protected double detailHintOrigin = DEFAULT_DETAIL_HINT_ORIGIN;
/**
* Creates a new KMLRegion
with the specified namespace URI, but otherwise does nothing. The new Region
* has no bounding box and no level of detail range.
*
* @param namespaceURI the qualifying namespace URI. May be null
to indicate no namespace
* qualification.
*/
public KMLRegion(String namespaceURI)
{
super(namespaceURI);
}
/**
* Indicates the bounding box that must be in view for this this Region to be considered active. May be
* null
.
*
* @return this Region's geographic bounding box, or null
indicating that this Region has no bounding
* box restriction.
*/
public KMLLatLonAltBox getLatLonAltBox()
{
return (KMLLatLonAltBox) this.getField("LatLonAltBox");
}
/**
* Indicates the level of detail criteria that must be satisfied for this Region to be considered active. May be
* null
.
*
* @return this Region's level of detail range, or null
indicating that this Region has no level of
* detail restriction.
*/
public KMLLod getLod()
{
return (KMLLod) this.getField("Lod");
}
/**
* Indicates whether this Region is active on the specified DrawContext
. A Region is active if its
* bounding box intersects the viewing frustum, and its level of detail criteria are met for the specified traversal
* context and draw context.
*
* This always returns true
if this Region has no bounding box, or if its bounding box is in the
* viewing frustum and this Region has no lod criteria.
*
* @param tc the current KML traversal context.
* @param dc the DrawContext
used to determine whether this Region is active.
*
* @return true
if this Region is active; otherwise false
.
*
* @throws IllegalArgumentException if either the KMLTraversalContext
or the DrawContext
* is null
.
*/
public boolean isActive(KMLTraversalContext tc, DrawContext dc)
{
if (tc == null)
{
String message = Logging.getMessage("nullValue.TraversalContextIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
if (dc == null)
{
String message = Logging.getMessage("nullValue.DrawContextIsNull");
Logging.logger().severe(message);
throw new IllegalArgumentException(message);
}
this.makeRegionData(dc);
// We cannot use the cached isActive value during picking because the pick frustums are used to determine
// visibility, so the result of isActive is different. Since picking occurs at most once per frame, we omit any
// caching and compute isActive during each pick pass.
if (dc.isPickingMode())
return this.isRegionActive(tc, dc);
// Attempt to re-use the result of isActive during the preRender and render passes for the same Globe. These
// calls use the same frustum to determine visibility and can therefore share the result of isActive. We
// recompute isActive when the frame changes or when Globe changes, and return the computed value below.
if (dc.getFrameTimeStamp() != this.getCurrentData().getActiveFrameNumber())
{
this.getCurrentData().setActive(this.isRegionActive(tc, dc));
this.getCurrentData().setActiveFrameNumber(dc.getFrameTimeStamp());
}
return this.getCurrentData().isActive();
}
/**
* Produces the data used to determine whether this Region is active for the specified DrawContext
.
* This attempts to re-use RegionData
already been calculated this frame, or previously calculated
* RegionData
that is still valid and has not expired. This method is called by isActive
* prior to determining if this Region is actually active.
*
* @param dc the current draw context.
*
* @see #isActive
*/
protected void makeRegionData(DrawContext dc)
{
// Retrieve the cached data for the current globe. If it doesn't yet exist, create it. Most code subsequently
// executed depends on currentData being non-null.
this.currentData = (RegionData) this.regionDataCache.getEntry(dc.getGlobe());
if (this.currentData == null)
{
this.currentData = this.createCacheEntry(dc);
this.regionDataCache.addEntry(this.currentData);
}
// Re-use values already calculated this frame.
if (dc.getFrameTimeStamp() != this.getCurrentData().getFrameNumber())
{
// Regenerate the region and data at a specified frequency.
if (this.mustRegenerateData(dc))
{
this.doMakeRegionData(dc);
this.getCurrentData().restartTimer(dc);
this.getCurrentData().setGlobeStateKey(dc.getGlobe().getGlobeStateKey(dc));
this.getCurrentData().setVerticalExaggeration(dc.getVerticalExaggeration());
}
this.getCurrentData().setFrameNumber(dc.getFrameTimeStamp());
}
}
/**
* Returns the data cache entry for the current invocation of isActive
.
*
* @return the data cache entry for the current invocation of isActive
.
*/
protected RegionData getCurrentData()
{
return this.currentData;
}
/**
* Creates and returns a new RegionData
instance specific to this Region instance.
*
* @param dc the current draw context.
*
* @return data cache entry for the state in the specified draw context.
*/
protected RegionData createCacheEntry(DrawContext dc)
{
return new RegionData(dc, this.minExpiryTime, this.maxExpiryTime);
}
/**
* Indicates whether this Region's data must be recomputed, either as a result of a change in the
* Globe's
state or the expiration of the geometry regeneration interval.
*
* A {@link gov.nasa.worldwind.ogc.kml.KMLRegion.RegionData}
must be current when this method is
* called.
*
* @param dc the current draw context.
*
* @return true
if this Region's data must be regenerated, otherwise false
.
*/
protected boolean mustRegenerateData(DrawContext dc)
{
return this.getCurrentData().isExpired(dc) || !this.getCurrentData().isValid(dc);
}
/**
* Produces the data used to determine whether this Region is active for the specified DrawContext
.
* This method is called by makeRegionData
upon determining that the current RegionData must be
* recomputed, either as a result of a change in the Globe's
state or the expiration of the geometry
* regeneration interval. A {@link gov.nasa.worldwind.ogc.kml.KMLRegion.RegionData}
must be current
* when this method is called.
*
* @param dc the current draw context.
*
* @see #makeRegionData
*/
protected void doMakeRegionData(DrawContext dc)
{
this.getCurrentData().setExtent(null);
this.getCurrentData().setSector(null);
this.getCurrentData().setPoints(null);
KMLLatLonAltBox box = this.getLatLonAltBox();
if (box == null)
return;
int altitudeMode = KMLUtil.convertAltitudeMode(box.getAltitudeMode(), WorldWind.CLAMP_TO_GROUND); // KML default
this.getCurrentData().setAltitudeMode(altitudeMode);
if (altitudeMode == WorldWind.CLAMP_TO_GROUND)
{
this.doMakeClampToGroundRegionData(dc, box);
}
else if (altitudeMode == WorldWind.RELATIVE_TO_GROUND)
{
this.doMakeRelativeToGroundRegionData(dc, box);
}
else // Default to WorldWind.ABSOLUTE.
{
this.doMakeAbsoluteRegionData(dc, box);
}
}
/**
* Produces the Extent
and the Sector
for this Region. Assumes this region's altitude mode
* is clampToGround
. A {@link gov.nasa.worldwind.ogc.kml.KMLRegion.RegionData}
must be
* current when this method is called.
*
* @param dc the current draw context.
* @param box the Region's geographic bounding box.
*/
protected void doMakeClampToGroundRegionData(DrawContext dc, KMLLatLonAltBox box)
{
Sector sector = KMLUtil.createSectorFromLatLonBox(box);
if (sector == null)
return;
// TODO: Regions outside of the normal lat/lon bounds ([-90, 90], [-180, 180]) are not supported. Remove
// TODO: this warning when such regions are supported. See WWJINT-482.
if (!this.isSectorSupported(sector))
{
String message = Logging.getMessage("KML.UnsupportedRegion", sector);
Logging.logger().warning(message);
return;
}
double[] extremeElevations = dc.getGlobe().getMinAndMaxElevations(sector);
Extent extent = Sector.computeBoundingBox(dc.getGlobe(), dc.getVerticalExaggeration(), sector,
extremeElevations[0], extremeElevations[1]);
this.getCurrentData().setExtent(extent);
this.getCurrentData().setSector(sector);
// Cache the model-coordinate points of the Region's four corners and center point on the surface terrain. It is
// safe to cache this value since these points are regenerated every 5-6 seconds along with the RegionData's
// extent and sector. Caching these points rather than computing them every frame reduces the average time of
// Region.isActive by 50%.
Vec4[] corners = sector.computeCornerPoints(dc.getGlobe(), dc.getVerticalExaggeration());
Vec4 centerPoint = sector.computeCenterPoint(dc.getGlobe(), dc.getVerticalExaggeration());
this.getCurrentData().setPoints(Arrays.asList(corners[0], corners[1], corners[2], corners[3], centerPoint));
}
/**
* Produces the Extent
and the Sector
for this Region. Assumes this region's altitude mode
* is relativeToGround
. A {@link gov.nasa.worldwind.ogc.kml.KMLRegion.RegionData}
must be
* current when this method is called.
*
* @param dc the current draw context.
* @param box the Region's geographic bounding box.
*/
protected void doMakeRelativeToGroundRegionData(DrawContext dc, KMLLatLonAltBox box)
{
Sector sector = KMLUtil.createSectorFromLatLonBox(box);
if (sector == null)
return;
if (!this.isSectorSupported(sector))
{
String message = Logging.getMessage("KML.UnsupportedRegion", sector);
Logging.logger().warning(message);
return;
}
Double minAltitude = box.getMinAltitude();
if (minAltitude == null)
minAltitude = 0d; // The default minAltitude is zero.
Double maxAltitude = box.getMaxAltitude();
if (maxAltitude == null)
maxAltitude = 0d; // The default maxAltitude is zero.
double[] extremeElevations = dc.getGlobe().getMinAndMaxElevations(sector);
Extent extent = Sector.computeBoundingBox(dc.getGlobe(), dc.getVerticalExaggeration(), sector,
extremeElevations[0] + minAltitude, extremeElevations[1] + maxAltitude);
this.getCurrentData().setExtent(extent);
this.getCurrentData().setSector(sector);
}
/**
* Produces the Extent
and the Sector
for this Region. Assumes this region's altitude mode
* is absolute
. A {@link gov.nasa.worldwind.ogc.kml.KMLRegion.RegionData}
must be current
* when this method is called.
*
* @param dc the current draw context.
* @param box the Region's geographic bounding box.
*/
protected void doMakeAbsoluteRegionData(DrawContext dc, KMLLatLonAltBox box)
{
Sector sector = KMLUtil.createSectorFromLatLonBox(box);
if (sector == null)
return;
if (!this.isSectorSupported(sector))
{
String message = Logging.getMessage("KML.UnsupportedRegion", sector);
Logging.logger().warning(message);
return;
}
Double minAltitude = box.getMinAltitude();
if (minAltitude == null)
minAltitude = 0d; // The default minAltitude is zero.
Double maxAltitude = box.getMaxAltitude();
if (maxAltitude == null)
maxAltitude = 0d; // The default maxAltitude is zero.
Extent extent = Sector.computeBoundingBox(dc.getGlobe(), dc.getVerticalExaggeration(), sector,
minAltitude, maxAltitude);
this.getCurrentData().setExtent(extent);
this.getCurrentData().setSector(sector);
}
/**
* Determines if a Sector is supported by this Region implementation. This implementation does not support sectors
* with latitude values outside of [-90, 90], or longitude values outside of [-180, 180].
*
* @param sector Sector to test.
*
* @return {@code true} if {@code sector} is with [-90, 90] latitude and [-180, 180] longitude.
*/
protected boolean isSectorSupported(Sector sector)
{
return sector.isWithinLatLonLimits();
}
/**
* Indicates whether this Region is active on the specified DrawContext
. A Region is active if its
* bounding box intersects the viewing frustum, and its level of detail criteria are met for the specified traversal
* context and draw context. This is called by isActive
and its return value may be cached during a
* single frame.
*
* @param tc the current KML traversal context.
* @param dc the DrawContext
used to determine whether this Region is active.
*
* @return true
if this Region is active; otherwise false
.
*/
protected boolean isRegionActive(KMLTraversalContext tc, DrawContext dc)
{
return this.isRegionVisible(dc) && this.meetsLodCriteria(tc, dc);
}
/**
* Indicates whether this Region is visible for the specified DrawContext
. If this Region's
* altitudeMode
is clampToGround
, this Region is considered visible if its sector
* intersects the DrawContext's
visible sector and its frustum intersects the
* DrawContext's
viewing frustum. Otherwise, this Region is considered visible its frustum intersects
* the DrawContext's
viewing frustum.
*
* @param dc the DrawContext
used to test this Region for visibility.
*
* @return true
if this Region is visible, otherwise false
.
*/
protected boolean isRegionVisible(DrawContext dc)
{
// If this Region's altitude mode is clampToGround and it has a non-null sector, compare its sector against the
// DrawContext's visible sector to determine if the Region is visible. In this case the sector can be used to
// determine visibility because the Region is defined to be on the Globe's surface.
if (this.getCurrentData().getAltitudeMode() == WorldWind.CLAMP_TO_GROUND
&& dc.getVisibleSector() != null && this.getCurrentData().getSector() != null
&& !dc.getVisibleSector().intersects(this.getCurrentData().getSector()))
{
return false;
}
// Treat this Region as not visible if its extent occupies less than one pixel on screen. Features that exceed
// the Region's boundary - such as a ScreenOverlay - are not displayed when the Region is insignificantly
// visible. Though it may be the intent of a KML document's author to display such a feature whenever the Region
// is in the frustum, we treat the Region just as though its outside of the frustum and prevent the display of
// any features associated with it.
//noinspection SimplifiableIfStatement
if (this.getCurrentData().getExtent() != null && dc.isSmall(this.getCurrentData().getExtent(), 1))
return false;
return this.intersectsFrustum(dc);
}
/**
* Indicates whether this Region intersects viewing frustum for the specified DrawContext
. If the
* DrawContext
is in picking mode, this indicates whether this Region's bounding box intersects any of
* the pick frustums. Otherwise, this this indicates whether this Region's bounding box intersects the viewing
* frustum. A {@link gov.nasa.worldwind.ogc.kml.KMLRegion.RegionData}
must be current when this method
* is called.
*
* This returns true
if this Region has no bounding box, or if its bounding box cannot be computed for
* any reason.
*
* @param dc the DrawContext
who's frustum is tested against this Region's bounding box.
*
* @return true
if this Region's bounding box intersects the DrawContext's
frustum,
* otherwise false
.
*/
protected boolean intersectsFrustum(DrawContext dc)
{
Extent extent = this.getCurrentData().getExtent();
if (extent == null)
return true; // We do not know the visibility; assume it intersects the frustum.
if (dc.isPickingMode())
return dc.getPickFrustums().intersectsAny(extent);
return dc.getView().getFrustumInModelCoordinates().intersects(extent);
}
/**
* Indicates whether the specified DrawContext
meets this Region's level of detail criteria. A
* {@link gov.nasa.worldwind.ogc.kml.KMLRegion.RegionData}
must be current when this method is called.
*
* This returns true
if this Region has no level of criteria, or if its level of detail cannot be
* compared against the bounding box for any reason.
*
* @param tc the current KML traversal context.
* @param dc the DrawContext
to test.
*
* @return true
if the DrawContext's
meets this Region's level of detail criteria,
* otherwise false
.
*/
protected boolean meetsLodCriteria(KMLTraversalContext tc, DrawContext dc)
{
KMLLod lod = this.getLod();
if (lod == null)
return true; // No level of detail specified; assume the DrawContext meets the level of detail criteria.
if ((lod.getMinLodPixels() == null || lod.getMinLodPixels() <= 0d)
&& (lod.getMaxLodPixels() == null || lod.getMaxLodPixels() < 0d))
return true; // The level of detail range is infinite, so this Region always meets the lod criteria.
if (lod.getMaxLodPixels() != null && lod.getMaxLodPixels() == 0d)
return false; // The maximum number of pixels is zero, so this Region never meets the lod criteria.
int altitudeMode = this.getCurrentData().getAltitudeMode();
if (altitudeMode == WorldWind.CLAMP_TO_GROUND)
{
return this.meetsClampToGroundLodCriteria(tc, dc, lod);
}
else if (altitudeMode == WorldWind.RELATIVE_TO_GROUND)
{
return this.meetsRelativeToGroundLodCriteria(tc, dc, lod);
}
else // Default to WorldWind.ABSOLUTE.
{
return this.meetsAbsoluteLodCriteria(tc, dc, lod);
}
}
/**
* Indicates whether the specified DrawContext
meets this Region's level of detail criteria. Assumes
* this region's altitude mode is clampToGround
. A {@link gov.nasa.worldwind.ogc.kml.KMLRegion.RegionData}
* must be current when this method is called.
*
* @param tc the current KML traversal context.
* @param dc the DrawContext
to test.
* @param lod the level of detail criteria that must be met.
*
* @return true
if the DrawContext's
meets this Region's level of detail criteria,
* otherwise false
.
*/
protected boolean meetsClampToGroundLodCriteria(KMLTraversalContext tc, DrawContext dc, KMLLod lod)
{
// Neither the OGC KML specification nor the Google KML reference specify how to compute a clampToGround
// Region's projected screen area. However, the Google Earth outreach tutorials, and an official post from a
// Google engineer on the Google forums both indicate that clampToGround Regions are represented by a flat
// rectangle on the terrain surface:
// KML Specification version 2.2, section 6.3.4.
// http://groups.google.com/group/kml-support-getting-started/browse_thread/thread/bbba32541bace3cc/df4e1dc64a3018d4?lnk=gst#df4e1dc64a3018d4
// http://earth.google.com/outreach/tutorial_region.html
Sector sector = this.getCurrentData().getSector();
List points = this.getCurrentData().getPoints();
if (sector == null || points == null || points.size() != 5)
return true; // Assume the criteria is met if we don't know this Region's sector or its surface points.
// Get the eye distance for each of the sector's corners and its center.
View view = dc.getView();
double d1 = view.getEyePoint().distanceTo3(points.get(0));
double d2 = view.getEyePoint().distanceTo3(points.get(1));
double d3 = view.getEyePoint().distanceTo3(points.get(2));
double d4 = view.getEyePoint().distanceTo3(points.get(3));
double d5 = view.getEyePoint().distanceTo3(points.get(4));
// Find the minimum eye distance. Compute the sector's size in meters by taking the square root of the sector's
// area in radians, and multiplying that by the globe's radius at the nearest corner. We take the square root
// of the area in radians to match the units of this Region's minLodPixels and maxLodPixels, which are the
// square root of a screen area.
double minDistance = d1;
double numRadians = Math.sqrt(sector.getDeltaLatRadians() * sector.getDeltaLonRadians());
double numMeters = points.get(0).getLength3() * numRadians;
if (d2 < minDistance)
{
minDistance = d2;
numMeters = points.get(1).getLength3() * numRadians;
}
if (d3 < minDistance)
{
minDistance = d3;
numMeters = points.get(2).getLength3() * numRadians;
}
if (d4 < minDistance)
{
minDistance = d4;
numMeters = points.get(3).getLength3() * numRadians;
}
if (d5 < minDistance)
{
minDistance = d5;
numMeters = points.get(4).getLength3() * numRadians;
}
// Compare the scaled distance to the minimum and maximum pixel size in meters, according to the sector's size
// and this Region's minLodPixels and maxLodPixels. This Region's level of detail criteria are met when the
// scaled distance is less than or equal to the minimum pixel size, and greater than the maximum pixel size.
// Said another way, this Region is used when a pixel in the Region's sector is close enough to meet the minimum
// pixel size criteria, yet far enough away not to exceed the maximum pixel size criteria.
// NOTE: It's tempting to instead compare a screen pixel count to the minLodPixels and maxLodPixels, but that
// calculation is window-size dependent and results in activating an excessive number of Regions when a KML
// super overlay is displayed, especially if the window size is large.
Double lodMinPixels = lod.getMinLodPixels();
Double lodMaxPixels = lod.getMaxLodPixels();
double distanceFactor = minDistance * Math.pow(10, -this.getDetailFactor(tc));
// We ignore minLodPixels if it's unspecified, zero, or less than zero. We ignore maxLodPixels if it's
// unspecified or less than 0 (infinity). In these cases any distance passes the test against minLodPixels or
// maxLodPixels.
return (lodMinPixels == null || lodMinPixels <= 0d || (numMeters / lodMinPixels) >= distanceFactor)
&& (lodMaxPixels == null || lodMaxPixels < 0d || (numMeters / lodMaxPixels) < distanceFactor);
}
/**
* Indicates whether the specified DrawContext
meets this Region's level of detail criteria. Assumes
* this region's altitude mode is relativeToGround
. A {@link
* gov.nasa.worldwind.ogc.kml.KMLRegion.RegionData}
must be current when this method is called.
*
* @param tc the current KML traversal context.
* @param dc the DrawContext
to test.
* @param lod the level of detail criteria that must be met.
*
* @return true
if the DrawContext's
meets this Region's level of detail criteria,
* otherwise false
.
*/
@SuppressWarnings({"UnusedDeclaration"})
protected boolean meetsRelativeToGroundLodCriteria(KMLTraversalContext tc, DrawContext dc, KMLLod lod)
{
return this.meetsScreenAreaCriteria(dc, lod);
}
/**
* Indicates whether the specified DrawContext
meets this Region's level of detail criteria. Assumes
* this region's altitude mode is absolute
. A {@link gov.nasa.worldwind.ogc.kml.KMLRegion.RegionData}
* must be current when this method is called.
*
* @param tc the current KML traversal context.
* @param dc the DrawContext
to test.
* @param lod the level of detail criteria that must be met.
*
* @return true
if the DrawContext's
meets this Region's level of detail criteria,
* otherwise false
.
*/
@SuppressWarnings({"UnusedDeclaration"})
protected boolean meetsAbsoluteLodCriteria(KMLTraversalContext tc, DrawContext dc, KMLLod lod)
{
return this.meetsScreenAreaCriteria(dc, lod);
}
/**
* Indicates whether this Region's projected screen area on the specified DrawContext
is in the range
* specified by lod
.
*
* @param dc the DrawContext
to test.
* @param lod the level of detail criteria that must be met.
*
* @return true
if this Region's screen area meets the level of detail criteria, otherwise
* false
.
*/
protected boolean meetsScreenAreaCriteria(DrawContext dc, KMLLod lod)
{
// The DrawContext does not meet this region's minLodPixels criteria if minLodPixels is specified and this
// region's projected screen pixel count is less than minLodPixels.
// The DrawContext does not meet this region's maxLodPixels criteria if maxLodPixels is specified, is not
// negative, and this region's projected screen pixel count is greater than or equal to maxLodPixels. If
// maxLodPixels is negative, this indicates that maxLodPixels is positive infinity and therefore accepts any
// value.
Extent extent = this.getCurrentData().getExtent();
if (extent == null)
return true; // Assume the criteria is met if we don't know this Region's extent.
// Compute the projected screen area of this Region's extent in square pixels in the DrawContext's View.
// According to the KML specification, we take the square root of this value to get a value that is comparable
// against minLodPixels and maxLodPixels. The projected area is positive infinity if the view's eye point is
// inside the extent, or if part of the extent is behind the eye point. In either case we do not take the square
// root, and leave the value as positive infinity.
double numPixels = extent.getProjectedArea(dc.getView());
if (numPixels != Double.POSITIVE_INFINITY)
numPixels = Math.sqrt(numPixels);
// This Region's level of detail criteria are met if the number of pixels is greater than or equal to
// minLodPixels and less than maxLodPixels. We ignore minLodPixels if it's unspecified, zero, or less than zero.
// We ignore maxLodPixels if it's unspecified or less than 0 (infinity). In these cases any distance passes the
// test against minLodPixels or maxLodPixels.
Double lodMinPixels = lod.getMinLodPixels();
Double lodMaxPixels = lod.getMaxLodPixels();
return (lodMinPixels == null || lodMinPixels <= 0d || lodMinPixels <= numPixels)
&& (lodMaxPixels == null || lodMaxPixels < 0d || lodMaxPixels > numPixels);
}
/**
* Indicates the detail factor that configures KML scene resolution to screen resolution as the viewing distance
* changes. This returns the Region's detailHintOrigin
plus the KMLTraversalContext's
* detail hint.
*
* @param tc the KML traversal context that specifies the detail hint.
*
* @return this Region's detailHintOrigin
plus the traversal context's detailHintOrigin
.
*/
protected double getDetailFactor(KMLTraversalContext tc)
{
return this.detailHintOrigin + tc.getDetailHint();
}
@Override
public void applyChange(KMLAbstractObject sourceValues)
{
if (!(sourceValues instanceof KMLRegion))
{
String message = Logging.getMessage("nullValue.SourceIsNull");
Logging.logger().warning(message);
throw new IllegalArgumentException(message);
}
this.reset();
super.applyChange(sourceValues);
}
@Override
public void onChange(Message msg)
{
if (KMLAbstractObject.MSG_BOX_CHANGED.equals(msg.getName()))
this.reset();
super.onChange(msg);
}
protected void reset()
{
this.regionDataCache.removeAllEntries();
this.currentData = null;
}
}