All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gov.nasa.worldwind.render.SurfaceIcon Maven / Gradle / Ivy

The newest version!
/*
 * 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.render;

import com.jogamp.opengl.util.texture.TextureCoords;
import gov.nasa.worldwind.*;
import gov.nasa.worldwind.drag.*;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.globes.Globe;
import gov.nasa.worldwind.util.*;

import com.jogamp.opengl.*;
import java.awt.*;
import java.awt.geom.*;
import java.util.List;

/**
 * Renders an icon image over the terrain surface.
 *
 * @author Patrick Murris
 * @version $Id: SurfaceIcon.java 1772 2013-12-18 02:43:27Z tgaskins $
 */
public class SurfaceIcon extends AbstractSurfaceRenderable implements Movable, Draggable
{
    private Object imageSource;
    private boolean useMipMaps = true;
    private LatLon location;
    private Vec4 locationOffset;                    // Pixels
    private double scale = 1d;
    private Angle heading = Angle.ZERO;             // CW from north
    private Color color = Color.WHITE;
    private boolean maintainSize = false;
    private double maxSize = Double.MAX_VALUE;      // Meter
    private double minSize = .1;                    // Meter

    protected WWTexture texture;
    protected int imageWidth = 32;
    protected int imageHeight = 32;
    protected boolean dragEnabled = true;
    protected DraggableSupport draggableSupport = null;

    public SurfaceIcon(Object imageSource)
    {
        this(imageSource, null);
    }

    public SurfaceIcon(Object imageSource, LatLon location)
    {
        this.setImageSource(imageSource);
        if (location != null)
            this.setLocation(location);
    }

    /**
     * Get the icon reference location on the globe.
     *
     * @return the icon reference location on the globe.
     */
    public LatLon getLocation()
    {
        return this.location;
    }

    /**
     * Set the icon reference location on the globe.
     *
     * @param location the icon reference location on the globe.
     *
     * @throws IllegalArgumentException if location is null.
     */
    public void setLocation(LatLon location)
    {
        if (location == null)
        {
            String message = Logging.getMessage("nullValue.LatLonIsNull");
            Logging.logger().severe(message);
            throw new IllegalArgumentException(message);
        }

        this.location = location;
        this.onPropertyChanged();
    }

    /**
     * Get the icon displacement in pixels relative to the reference location.  Can be null.
     * 

* When null the icon will be drawn with it's image center on top of it's reference location - see * {@link #setLocation(LatLon)}. Otherwise the icon will be shifted of a distance equivalent to the number of pixels * specified as x and y offset values. Positive values will move the icon to the right for * x and up for y. Negative values will have the opposite effect. * * @return the icon displacement in pixels relative to the reference location. */ public Vec4 getLocationOffset() { return this.locationOffset; } /** * Set the icon displacement in pixels relative to the reference location. Can be null. *

* When null the icon will be drawn with it's image center on top of it's refence location - see {@link * #setLocation(LatLon)}. Otherwise the icon will be shifted of a distance equivalent to the number of pixels * specified as x and y offset values. Positive values will move the icon to the right for * x and up for y. Negative values will have the opposite effect. * * @param locationOffset the icon displacement in pixels relative to the reference location. */ public void setLocationOffset(Vec4 locationOffset) { this.locationOffset = locationOffset; // can be null this.onPropertyChanged(); } /** * Get the source for the icon image. Can be a file path to a local image or a {@link java.awt.image.BufferedImage} * reference. * * @return the source for the icon image. */ public Object getImageSource() { return this.imageSource; } /** * Set the source for the icon image. Can be a file path to a local image or a {@link java.awt.image.BufferedImage} * reference. * * @param imageSource the source for the icon image. * * @throws IllegalArgumentException if imageSource is null. */ public void setImageSource(Object imageSource) { if (imageSource == null) { String message = Logging.getMessage("nullValue.ImageSource"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.imageSource = imageSource; this.texture = null; this.onPropertyChanged(); } /** * Returns whether the icon will apply mip-map filtering to it's source image. If true the icon image * is drawn using mip-maps. If false the icon is drawn without mip-maps, resulting in aliasing if the * icon image is drawn smaller than it's native size in pixels. * * @return true if the icon image is drawn with mip-map filtering; false otherwise. */ public boolean isUseMipMaps() { return this.useMipMaps; } /** * Sets whether the icon will apply mip-map filtering to it's source image. If true the icon image is * drawn using mip-maps. If false the icon is drawn without mip-maps, resulting in aliasing if the icon * image is drawn smaller than it's native size in pixels. * * @param useMipMaps true if the icon image should be drawn with mip-map filtering; false * otherwise. */ public void setUseMipMaps(boolean useMipMaps) { this.useMipMaps = useMipMaps; this.texture = null; this.onPropertyChanged(); } /** * Get the current scaling factor applied to the source image. * * @return the current scaling factor applied to the source image. */ public double getScale() { return this.scale; } /** * Set the scaling factor to apply to the source image. A value of 1 will produce no change, a value * greater then 1 will enlarge the image and a value smaller then 1 will reduce it. * * @param scale the scaling factor to apply to the source image. * * @throws IllegalArgumentException if scale is zero or negative. */ public void setScale(double scale) { if (scale <= 0) { String message = Logging.getMessage("generic.ArgumentOutOfRange", "scale must be greater then zero"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.scale = scale; this.onPropertyChanged(); } /** * Get the current heading {@link Angle}, clockwise from North or null. * * @return the current heading {@link Angle}, clockwise from North or null. */ public Angle getHeading() { return this.heading; } /** * Set the heading {@link Angle}, clockwise from North. Setting this value to null will have the icon * follow the view heading so as to always face the eye. The icon will rotate around it's reference location. * * @param heading the heading {@link Angle}, clockwise from North or null. */ public void setHeading(Angle heading) { this.heading = heading; // can be null this.onPropertyChanged(); } /** * Determines whether the icon constantly maintains it's apparent size. If true the icon is constantly * redrawn at the proper size depending on it's distance from the eye. If false the icon will be drawn * only once per level of the underlying tile pyramid. Thus it's apparent size will vary up to twice it's 'normal' * dimension in between levels. * * @return true if the icon constantly maintains it's apparent size. */ public boolean isMaintainSize() { return this.maintainSize; } /** * Sets whether the icon constantly maintains it's apparent size. If true the icon is constantly * redrawn at the proper size depending on it's distance from the eye. If false the icon will be drawn * only once per level of the underlying tile pyramid. Thus it's apparent size will vary up to twice it's 'normal' * dimension in between levels. * * @param state true if the icon should constantly maintains it's apparent size. */ public void setMaintainSize(boolean state) { this.maintainSize = state; } /** * Get the minimum size in meter the icon image is allowed to be reduced to once applied to the terrain surface. * This limit applies to the source image largest dimension. *

* The icon will try to maintain it's apparent size depending on it's distance from the eye and will extend over a * rectangular area which largest dimension is bounded by the values provided with {@link #setMinSize(double)} and * {@link #setMaxSize(double)}. * * @return the minimum size of the icon in meter. */ public double getMinSize() { return this.minSize; } /** * Set the minimum size in meter the icon image is allowed to be reduced to once applied to the terrain surface. * This limit applies to the source image largest dimension. *

* The icon will try to maintain it's apparent size depending on it's distance from the eye and will extend over a * rectangular area which largest dimension is bounded by the values provided with setMinSize(double) * and {@link #setMaxSize(double)}. * * @param sizeInMeter the minimum size of the icon in meter. */ public void setMinSize(double sizeInMeter) { this.minSize = sizeInMeter; this.onPropertyChanged(); } /** * Get the maximum size in meter the icon image is allowed to be enlarged to once applied to the terrain surface. * This limit applies to the source image largest dimension. *

* The icon will try to maintain it's apparent size depending on it's distance from the eye and will extend over a * rectangular area which largest dimension is bounded by the values provided with {@link #setMinSize(double)} and * {@link #setMaxSize(double)}. * * @return the maximum size of the icon in meter. */ public double getMaxSize() { return this.maxSize; } /** * Get the maximum size in meter the icon image is allowed to be enlarged to once applied to the terrain surface. * This limit applies to the source image largest dimension. *

* The icon will try to maintain it's apparent size depending on it's distance from the eye and will extend over a * rectangular area which largest dimension is bounded by the values provided with {@link #setMinSize(double)} and * setMaxSize(double). * * @param sizeInMeter the maximum size of the icon in meter. */ public void setMaxSize(double sizeInMeter) { this.maxSize = sizeInMeter; this.onPropertyChanged(); } /** * Get the {@link Color} the source image is combined with. * * @return the {@link Color} the source image is combined with. */ public Color getColor() { return this.color; } /** * Set the {@link Color} the source image will be combined with - default to white. *

* A non white color will mostly affect the white portions from the original image. This is mostly useful to alter * the appearance of 'colorless' icons - which mainly contain black, white and shades of gray. * * @param color the {@link Color} the source image will be combined with. * * @throws IllegalArgumentException if color is null. */ public void setColor(Color color) { if (color == null) { String message = Logging.getMessage("nullValue.ColorIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.color = color; this.onPropertyChanged(); } protected boolean isMaintainAppearance() { return this.getHeading() == null || this.isMaintainSize(); // always facing or constant size } // *** SurfaceObject interface *** /** * {@inheritDoc} *

* Overridden to return a unique state key if the icon is configured to always redraw. SurfaceIcon does not use a * cached representation if it's heading is configured to follow the view, or if it's configured to maintain a * constant screen size. * * @see #getHeading() * @see #isMaintainSize() */ @Override public Object getStateKey(DrawContext dc) { // If the icon always redraws, return a unique object that is not equivalent to any other state key. if (this.isMaintainAppearance()) return new Object(); return super.getStateKey(dc); } public List getSectors(DrawContext dc) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } return this.computeSectors(dc); } public Extent getExtent(DrawContext dc) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } return this.computeExtent(dc); } public void drawGeographic(DrawContext dc, SurfaceTileDrawContext sdc) { WWTexture texture = getTexture(); if (texture == null) return; this.beginDraw(dc); try { if (texture.bind(dc)) { // Update image width and height this.imageWidth = texture.getWidth(dc); this.imageHeight = texture.getHeight(dc); // Apply texture local transform GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. gl.glMatrixMode(GL.GL_TEXTURE); this.getTexture().applyInternalTransform(dc); // Apply draw color this.applyDrawColor(dc); //Draw this.drawIcon(dc, sdc); } } catch (Exception e) { // TODO: log error } finally { // Restore gl state this.endDraw(dc); } } protected void onPropertyChanged() { this.updateModifiedTime(); this.clearCaches(); } @Override protected void clearCaches() { super.clearCaches(); } protected List computeSectors(DrawContext dc) { if (this.location == null) return null; Globe globe = dc.getGlobe(); // Compute real world icon extent depending on distance from eye Rectangle2D.Double rect = computeDrawDimension(dc, this.location); // meter // If the icon does not redraw all the time, double it's dimension if (!this.isMaintainAppearance()) { rect.setRect(rect.x, rect.y, rect.width * 2, rect.height * 2); } // Compute bounding sector and apply location offset to it double cosLat = Math.max(this.location.getLatitude().cos(), .01); // avoids division by zero at the poles double dLatRadians = rect.height / globe.getRadius(); double dLonRadians = rect.width / globe.getRadius() / cosLat; double offsetLatRadians = locationOffset != null ? locationOffset.y * dLatRadians / this.imageHeight : 0; double offsetLonRadians = locationOffset != null ? locationOffset.x * dLonRadians / this.imageWidth : 0; Sector sector = new Sector( this.location.getLatitude().subtractRadians(dLatRadians / 2).addRadians(offsetLatRadians), this.location.getLatitude().addRadians(dLatRadians / 2).addRadians(offsetLatRadians), this.location.getLongitude().subtractRadians(dLonRadians / 2).addRadians(offsetLonRadians), this.location.getLongitude().addRadians(dLonRadians / 2).addRadians(offsetLonRadians) ); // Rotate sector around location sector = computeRotatedSectorBounds(sector, this.location, computeDrawHeading(dc)); return computeNormalizedSectors(sector); } protected Rectangle2D.Double computeDrawDimension(DrawContext dc, LatLon location) { // Compute icon extent at 1:1 depending on distance from eye double pixelSize = computePixelSizeAtLocation(dc, location); return computeDrawDimension(pixelSize); } protected Rectangle2D.Double computeDrawDimension(double pixelSize) { // Compute icon extent at 1:1 depending on target tile pixel size double height = this.imageHeight * this.scale * pixelSize; double width = this.imageWidth * this.scale * pixelSize; // Clamp to size range double size = height > width ? height : width; double scale = size > this.maxSize ? this.maxSize / size : size < this.minSize ? this.minSize / size : 1; return new Rectangle2D.Double(0, 0, width * scale, height * scale); // meter } protected Angle computeDrawHeading(DrawContext dc) { if (this.heading != null) return this.heading; return getViewHeading(dc); } protected void beginDraw(DrawContext dc) { GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. int attributeMask = GL2.GL_TRANSFORM_BIT // for modelview | GL2.GL_CURRENT_BIT // for current color | GL2.GL_COLOR_BUFFER_BIT // for alpha test func and ref, and blend | GL2.GL_ENABLE_BIT; // for enable/disable changes gl.glPushAttrib(attributeMask); // Suppress any fully transparent image pixels gl.glEnable(GL2.GL_ALPHA_TEST); gl.glAlphaFunc(GL2.GL_GREATER, 0.001f); gl.glMatrixMode(GL2.GL_TEXTURE); gl.glPushMatrix(); gl.glMatrixMode(GL2.GL_MODELVIEW); gl.glPushMatrix(); if (dc.isPickingMode()) { // Set up to replace the non-transparent texture colors with the single pick color. gl.glEnable(GL.GL_TEXTURE_2D); gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_MODE, GL2.GL_COMBINE); gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_SRC0_RGB, GL2.GL_PREVIOUS); gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_COMBINE_RGB, GL2.GL_REPLACE); } else { gl.glEnable(GL.GL_TEXTURE_2D); gl.glEnable(GL.GL_BLEND); gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA); } } protected void endDraw(DrawContext dc) { GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. if (dc.isPickingMode()) { gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_MODE, OGLUtil.DEFAULT_TEX_ENV_MODE); gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_SRC0_RGB, OGLUtil.DEFAULT_SRC0_RGB); gl.glTexEnvf(GL2.GL_TEXTURE_ENV, GL2.GL_COMBINE_RGB, OGLUtil.DEFAULT_COMBINE_RGB); } gl.glBindTexture(GL.GL_TEXTURE_2D, 0); gl.glMatrixMode(GL2.GL_MODELVIEW); gl.glPopMatrix(); gl.glMatrixMode(GL2.GL_TEXTURE); gl.glPopMatrix(); gl.glPopAttrib(); } @SuppressWarnings({"UnusedDeclaration"}) protected void applyDrawTransform(DrawContext dc, SurfaceTileDrawContext sdc, LatLon location, double drawScale) { // Compute icon viewport point // Apply hemisphere offset if needed - for icons that may cross the date line double offset = computeHemisphereOffset(sdc.getSector(), location); Vec4 point = new Vec4(location.getLongitude().degrees + offset, location.getLatitude().degrees, 1); point = point.transformBy4(sdc.getModelviewMatrix()); GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. // Translate to location point gl.glTranslated(point.x(), point.y(), point.z()); // Add x scaling transform to maintain icon width and aspect ratio at any latitude gl.glScaled(drawScale / location.getLatitude().cos(), drawScale, 1); // Add rotation to account for icon heading gl.glRotated(this.computeDrawHeading(dc).degrees, 0, 0, -1); // Translate to lower left corner gl.glTranslated(-this.imageWidth / 2, -this.imageHeight / 2, 0); // Apply location offset if any if (this.locationOffset != null) gl.glTranslated(this.locationOffset.x, this.locationOffset.y, 0); } protected double computeDrawScale(DrawContext dc, SurfaceTileDrawContext sdc, LatLon location) { // Compute scaling to maintain apparent size double drawPixelSize; double regionPixelSize = this.computeDrawPixelSize(dc, sdc); if (this.isMaintainAppearance()) // Compute precise size depending on eye distance drawPixelSize = this.computeDrawDimension(dc, location).width / this.imageWidth; else // Compute size according to draw tile resolution drawPixelSize = this.computeDrawDimension(regionPixelSize).width / this.imageWidth; return drawPixelSize / regionPixelSize; } protected void applyDrawColor(DrawContext dc) { if (!dc.isPickingMode()) { GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. applyPremultipliedAlphaColor(gl, this.color, getOpacity()); } } protected void drawIcon(DrawContext dc, SurfaceTileDrawContext sdc) { GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. gl.glMatrixMode(GL2.GL_MODELVIEW); double drawScale = this.computeDrawScale(dc, sdc, this.location); this.applyDrawTransform(dc, sdc, this.location, drawScale); gl.glScaled(this.imageWidth, this.imageHeight, 1d); dc.drawUnitQuad(new TextureCoords(0, 0, 1, 1)); } protected WWTexture getTexture() { if (this.texture == null) this.texture = new BasicWWTexture(this.imageSource, this.useMipMaps); return this.texture; } // *** Movable interface public Position getReferencePosition() { return new Position(this.location, 0); } public void move(Position delta) { if (delta == null) { String msg = Logging.getMessage("nullValue.PositionIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } Position referencePos = this.getReferencePosition(); if (referencePos == null) return; this.moveTo(referencePos.add(delta)); } public void moveTo(Position position) { if (position == null) { String msg = Logging.getMessage("nullValue.PositionIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.setLocation(position); } @Override public boolean isDragEnabled() { return this.dragEnabled; } @Override public void setDragEnabled(boolean enabled) { this.dragEnabled = enabled; } @Override public void drag(DragContext dragContext) { if (!this.dragEnabled) return; if (this.draggableSupport == null) this.draggableSupport = new DraggableSupport(this, WorldWind.CLAMP_TO_GROUND); this.doDrag(dragContext); } protected void doDrag(DragContext dragContext) { this.draggableSupport.dragGlobeSizeConstant(dragContext); } //**************************************************************// //******************** Sector Cache Info *********************// //**************************************************************// protected static class SectorInfo { protected List sectors; protected Object globeStateKey; public SectorInfo(List sectors, DrawContext dc) { // Surface icon sectors depend on the state of the globe used to compute it. this.sectors = sectors; this.globeStateKey = dc.getGlobe().getStateKey(dc); } public boolean isValid(DrawContext dc) { return this.globeStateKey.equals(dc.getGlobe().getStateKey(dc)); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy