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

gov.nasa.worldwind.formats.shapefile.ShapefileExtrudedPolygons Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2014 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration.
 * All Rights Reserved.
 */
package gov.nasa.worldwind.formats.shapefile;

import com.jogamp.common.nio.Buffers;
import gov.nasa.worldwind.cache.*;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.layers.Layer;
import gov.nasa.worldwind.pick.*;
import gov.nasa.worldwind.render.*;
import gov.nasa.worldwind.terrain.Terrain;
import gov.nasa.worldwind.util.*;

import com.jogamp.opengl.*;
import java.awt.*;
import java.nio.*;
import java.util.*;
import java.util.List;
import java.util.Queue;

/**
 * @author dcollins
 * @version $Id: ShapefileExtrudedPolygons.java 2324 2014-09-17 20:25:35Z dcollins $
 */
public class ShapefileExtrudedPolygons extends ShapefileRenderable implements OrderedRenderable
{
    public static class Record extends ShapefileRenderable.Record
    {
        // Record properties.
        protected Double height; // may be null
        // Data structures supporting drawing.
        protected Tile tile;
        protected IntBuffer interiorIndices;
        protected IntBuffer outlineIndices;

        public Record(ShapefileRenderable shapefileRenderable, ShapefileRecord shapefileRecord)
        {
            super(shapefileRenderable, shapefileRecord);

            this.height = ShapefileUtils.extractHeightAttribute(shapefileRecord); // may be null
        }

        public Double getHeight()
        {
            return this.height;
        }

        public List intersect(Line line, Terrain terrain) throws InterruptedException
        {
            if (line == null)
            {
                String msg = Logging.getMessage("nullValue.LineIsNull");
                Logging.logger().severe(msg);
                throw new IllegalArgumentException(msg);
            }

            if (terrain == null)
            {
                String msg = Logging.getMessage("nullValue.TerrainIsNull");
                Logging.logger().severe(msg);
                throw new IllegalArgumentException(msg);
            }

            if (!this.visible) // records marked as not visible don't intersect anything
            {
                return null;
            }

            ArrayList intersections = new ArrayList();
            ((ShapefileExtrudedPolygons) this.shapefileRenderable).intersectTileRecord(line, terrain, this,
                intersections);

            return intersections.size() > 0 ? intersections : null;
        }
    }

    protected static class RecordGroup
    {
        // Record group properties.
        public final ShapeAttributes attributes;
        public ArrayList records = new ArrayList();
        // Data structures supporting drawing.
        public IntBuffer indices;
        public Range interiorIndexRange = new Range(0, 0);
        public Range outlineIndexRange = new Range(0, 0);
        public Object vboKey = new Object();

        public RecordGroup(ShapeAttributes attributes)
        {
            this.attributes = attributes;
        }
    }

    protected static class Tile
    {
        // Tile properties.
        public final Sector sector;
        public final int level;
        // Tile records, attribute groups and child tiles.
        public ArrayList records = new ArrayList();
        public ArrayList attributeGroups = new ArrayList();
        public Tile[] children;
        // Tile shape data.
        public ShapeDataCache dataCache = new ShapeDataCache(60000);
        public ShapeData currentData;
        public IntersectionData intersectionData = new IntersectionData();

        public Tile(Sector sector, int level)
        {
            this.sector = sector;
            this.level = level;
        }
    }

    protected static class ShapeData extends ShapeDataCache.ShapeDataCacheEntry
    {
        public FloatBuffer vertices;
        public Vec4 referencePoint;
        public Matrix transformMatrix;
        public Object vboKey = new Object();
        public boolean vboExpired;

        public ShapeData(DrawContext dc, long minExpiryTime, long maxExpiryTime)
        {
            super(dc, minExpiryTime, maxExpiryTime);
        }
    }

    protected static class IntersectionData extends ShapeData
    {
        protected Terrain terrain;
        protected boolean tessellationValid;

        public IntersectionData()
        {
            super(null, 0, 0);
        }

        public boolean isValid(Terrain terrain)
        {
            return this.terrain == terrain
                && this.verticalExaggeration == terrain.getVerticalExaggeration()
                && (this.globeStateKey != null && globeStateKey.equals(terrain.getGlobe().getGlobeStateKey()));
        }

        public void invalidate()
        {
            this.terrain = null;
            this.verticalExaggeration = 1;
            this.globeStateKey = null;
            this.tessellationValid = false;
        }

        public Terrain getTerrain()
        {
            return this.terrain;
        }

        public void setTerrain(Terrain terrain)
        {
            this.terrain = terrain;
        }

        public boolean isTessellationValid()
        {
            return this.tessellationValid;
        }

        public void setTessellationValid(boolean valid)
        {
            this.tessellationValid = valid;
        }
    }

    // Properties.
    protected double defaultHeight;
    protected double defaultBaseDepth;
    protected double maxHeight;
    // Tile quadtree structures.
    protected Tile rootTile;
    protected int tileMaxLevel = 3;
    protected int tileMaxCapacity = 10000;
    // Data structures supporting polygon tessellation and drawing.
    protected ArrayList currentTiles = new ArrayList();
    protected PolygonTessellator tess = new PolygonTessellator();
    protected byte[] colorByteArray = new byte[6];
    protected float[] colorFloatArray = new float[3];
    protected double[] matrixArray = new double[16];
    // Data structures supporting picking.
    protected Layer pickLayer;
    protected PickSupport pickSupport = new PickSupport();
    protected ByteBuffer pickColors;
    protected Object pickColorsVboKey = new Object();

    /**
     * Creates a new ShapefileExtrudedPolygons with the specified shapefile. The normal attributes and the highlight
     * attributes for each ShapefileRenderable.Record are assigned default values. In order to modify
     * ShapefileRenderable.Record shape attributes or key-value attributes during construction, use {@link
     * #ShapefileExtrudedPolygons(Shapefile, gov.nasa.worldwind.render.ShapeAttributes,
     * gov.nasa.worldwind.render.ShapeAttributes, gov.nasa.worldwind.formats.shapefile.ShapefileRenderable.AttributeDelegate)}.
     *
     * @param shapefile The shapefile to display.
     *
     * @throws IllegalArgumentException if the shapefile is null.
     */
    public ShapefileExtrudedPolygons(Shapefile shapefile)
    {
        if (shapefile == null)
        {
            String msg = Logging.getMessage("nullValue.ShapefileIsNull");
            Logging.logger().severe(msg);
            throw new IllegalArgumentException(msg);
        }

        this.init(shapefile, null, null, null);
    }

    /**
     * Creates a new ShapefileExtrudedPolygons with the specified shapefile. The normal attributes, the highlight
     * attributes and the attribute delegate are optional. Specifying a non-null value for normalAttrs or highlightAttrs
     * causes each ShapefileRenderable.Record to adopt those attributes. Specifying a non-null value for the attribute
     * delegate enables callbacks during creation of each ShapefileRenderable.Record. See {@link AttributeDelegate} for
     * more information.
     *
     * @param shapefile         The shapefile to display.
     * @param normalAttrs       The normal attributes for each ShapefileRenderable.Record. May be null to use the
     *                          default attributes.
     * @param highlightAttrs    The highlight attributes for each ShapefileRenderable.Record. May be null to use the
     *                          default highlight attributes.
     * @param attributeDelegate Optional callback for configuring each ShapefileRenderable.Record's shape attributes and
     *                          key-value attributes. May be null.
     *
     * @throws IllegalArgumentException if the shapefile is null.
     */
    public ShapefileExtrudedPolygons(Shapefile shapefile, ShapeAttributes normalAttrs, ShapeAttributes highlightAttrs,
        ShapefileRenderable.AttributeDelegate attributeDelegate)
    {
        if (shapefile == null)
        {
            String msg = Logging.getMessage("nullValue.ShapefileIsNull");
            Logging.logger().severe(msg);
            throw new IllegalArgumentException(msg);
        }

        this.init(shapefile, normalAttrs, highlightAttrs, attributeDelegate);
    }

    @Override
    protected void assembleRecords(Shapefile shapefile)
    {
        this.rootTile = new Tile(this.sector, 0);

        super.assembleRecords(shapefile);

        if (this.mustSplitTile(this.rootTile))
        {
            this.splitTile(this.rootTile);
        }

        this.rootTile.records.trimToSize(); // Reduce memory overhead from unused ArrayList capacity.
    }

    @Override
    protected boolean mustAssembleRecord(ShapefileRecord shapefileRecord)
    {
        return super.mustAssembleRecord(shapefileRecord)
            && (shapefileRecord.isPolylineRecord()
            || shapefileRecord.isPolygonRecord()); // accept both polyline and polygon records
    }

    @Override
    protected void assembleRecord(ShapefileRecord shapefileRecord)
    {
        Record record = this.createRecord(shapefileRecord);
        this.addRecord(shapefileRecord, record);

        if (record.height != null && this.maxHeight < record.height)
        {
            this.maxHeight = record.height;
        }

        this.rootTile.records.add(record);
        record.tile = this.rootTile;
    }

    protected ShapefileExtrudedPolygons.Record createRecord(ShapefileRecord shapefileRecord)
    {
        return new ShapefileExtrudedPolygons.Record(this, shapefileRecord);
    }

    protected boolean mustSplitTile(Tile tile)
    {
        return tile.level < this.tileMaxLevel && tile.records.size() > this.tileMaxCapacity;
    }

    protected void splitTile(Tile tile)
    {
        // Create four child tiles by subdividing the tile's sector in latitude and longitude.
        Sector[] childSectors = tile.sector.subdivide();
        tile.children = new Tile[4];
        tile.children[0] = new Tile(childSectors[0], tile.level + 1);
        tile.children[1] = new Tile(childSectors[1], tile.level + 1);
        tile.children[2] = new Tile(childSectors[2], tile.level + 1);
        tile.children[3] = new Tile(childSectors[3], tile.level + 1);

        // Move any records completely contained in a child tile's sector into the child's list of records. This may
        // include records that are marked as not visible, as recomputing the tile tree for record visibility changes
        // would be expensive.
        Iterator iterator = tile.records.iterator();
        while (iterator.hasNext())
        {
            Record record = iterator.next();
            for (int i = 0; i < 4; i++)
            {
                if (tile.children[i].sector.contains(record.sector))
                {
                    tile.children[i].records.add(record); // add it to the child
                    record.tile = tile.children[i]; // assign the record's tile
                    iterator.remove(); // remove it from the parent
                    break; // skip to the next record
                }
            }
        }

        // Recursively split child tiles as necessary, moving their records into each child's descendants. The recursive
        // split stops when a child tile reaches a maximum level, or when the number of records contained within the
        // tile is small enough.
        for (int i = 0; i < 4; i++)
        {
            if (this.mustSplitTile(tile.children[i]))
            {
                this.splitTile(tile.children[i]);
            }

            tile.children[i].records.trimToSize(); // Reduce memory overhead from unused ArrayList capacity.
        }
    }

    @Override
    protected void recordDidChange(ShapefileRenderable.Record record)
    {
        Tile tile = ((ShapefileExtrudedPolygons.Record) record).tile;
        if (tile != null) // tile is null when attributes are specified during construction
        {
            this.invalidateTileAttributeGroups(tile);
        }
    }

    public double getDefaultHeight()
    {
        return this.defaultHeight;
    }

    public void setDefaultHeight(double defaultHeight)
    {
        this.defaultHeight = defaultHeight;
        this.invalidateAllTileGeometry();
    }

    public double getDefaultBaseDepth()
    {
        return this.defaultBaseDepth;
    }

    public void setDefaultBaseDepth(double defaultBaseDepth)
    {
        this.defaultBaseDepth = defaultBaseDepth;
        this.invalidateAllTileGeometry();
    }

    @Override
    public double getDistanceFromEye()
    {
        return 0;
    }

    @Override
    public void pick(DrawContext dc, Point pickPoint)
    {
        if (dc == null)
        {
            String msg = Logging.getMessage("nullValue.DrawContextIsNull");
            Logging.logger().severe(msg);
            throw new IllegalArgumentException(msg);
        }

        if (!this.visible)
            return;

        if (this.rootTile == null) // Shapefile is empty or contains only null records.
            return;

        this.pickOrderedSurfaceRenderable(dc, pickPoint); // pick is called during ordered rendering
    }

    @Override
    public void render(DrawContext dc)
    {
        if (dc == null)
        {
            String msg = Logging.getMessage("nullValue.DrawContextIsNull");
            Logging.logger().severe(msg);
            throw new IllegalArgumentException(msg);
        }

        if (!this.visible)
            return;

        if (this.rootTile == null) // Shapefile is empty or contains only null records.
            return;

        if (dc.isOrderedRenderingMode())
            this.drawOrderedSurfaceRenderable(dc);
        else
            this.makeOrderedSurfaceRenderable(dc);
    }

    protected void makeOrderedSurfaceRenderable(DrawContext dc)
    {
        this.assembleTiles(dc); // performs a visibility test against the top level tile

        if (this.currentTiles.isEmpty()) // don't add an ordered renderable when there's nothing to draw
        {
            return;
        }

        this.pickLayer = dc.getCurrentLayer();
        dc.addOrderedSurfaceRenderable(this);
    }

    protected void assembleTiles(DrawContext dc)
    {
        this.currentTiles.clear();
        this.addTileOrDescendants(dc, this.rootTile);
    }

    protected void addTileOrDescendants(DrawContext dc, Tile tile)
    {
        // Get or create the tile's current shape data, which holds the rendered geometry for the current draw context.
        tile.currentData = (ShapeData) tile.dataCache.getEntry(dc.getGlobe());
        if (tile.currentData == null)
        {
            tile.currentData = new ShapeData(dc, 3000, 9000);
            tile.dataCache.addEntry(tile.currentData);
        }

        // Determine whether or not the tile is visible. If the tile is not visible, then neither are the tile's records
        // or the tile's children. Note that a tile with no records may have children, so we can't use the tile's record
        // count as a determination of whether or not to test its children.
        if (!this.isTileVisible(dc, tile))
        {
            return;
        }

        // Add the tile to the list of tiles to draw, regenerating the tile's geometry and the tile's attribute groups
        // as necessary.
        if (tile.records.size() > 0)
        {
            this.adjustTileExpiration(dc, tile); // reduce the remaining expiration time as the eye distance decreases
            if (this.mustRegenerateTileGeometry(dc, tile))
            {
                this.regenerateTileGeometry(dc, tile);
            }

            if (this.mustAssembleTileAttributeGroups(tile))
            {
                this.assembleTileAttributeGroups(tile);
            }

            this.currentTiles.add(tile);
        }

        // Process the tile's children, if any.
        if (tile.children != null)
        {
            for (Tile childTile : tile.children)
            {
                this.addTileOrDescendants(dc, childTile);
            }
        }
    }

    protected boolean isTileVisible(DrawContext dc, Tile tile)
    {
        Extent extent = this.makeTileExtent(dc.getTerrain(), tile);

        if (dc.isSmall(extent, 1))
        {
            return false;
        }

        if (dc.isPickingMode())
        {
            return dc.getPickFrustums().intersectsAny(extent);
        }

        return dc.getView().getFrustumInModelCoordinates().intersects(extent);
    }

    protected boolean mustRegenerateTileGeometry(DrawContext dc, Tile tile)
    {
        return tile.currentData.isExpired(dc) || !tile.currentData.isValid(dc);
    }

    protected void adjustTileExpiration(DrawContext dc, Tile tile)
    {
        // If the new eye distance is significantly closer than cached data's the current eye distance, reduce the
        // timer's remaining time by 50%. This reduction is performed only once each time the timer is reset.
        if (tile.currentData.referencePoint != null)
        {
            double newEyeDistance = dc.getView().getEyePoint().distanceTo3(tile.currentData.referencePoint);
            tile.currentData.adjustTimer(dc, newEyeDistance);
        }
    }

    protected void invalidateTileGeometry(Tile tile)
    {
        tile.dataCache.setAllExpired(true); // force the tile vertices to be regenerated

        synchronized (tile) // synchronize access to tile intersection data
        {
            tile.intersectionData.invalidate();
        }
    }

    protected void invalidateAllTileGeometry()
    {
        Queue tileQueue = new ArrayDeque();
        tileQueue.add(this.rootTile);

        while (!tileQueue.isEmpty())
        {
            Tile tile = tileQueue.poll();
            this.invalidateTileGeometry(tile);

            if (tile.children != null)
            {
                tileQueue.addAll(Arrays.asList(tile.children));
            }
        }
    }

    protected void regenerateTileGeometry(DrawContext dc, Tile tile)
    {
        ShapeData shapeData = tile.currentData;

        // Synchronize simultaneous tile updates between rendering, intersect and Record.intersect. Access to this
        // instance's coordinate buffer must be synchronized.
        synchronized (this)
        {
            this.tessellateTile(dc.getTerrain(), tile, shapeData);
        }

        shapeData.setEyeDistance(dc.getView().getEyePoint().distanceTo3(shapeData.referencePoint));
        shapeData.setGlobeStateKey(dc.getGlobe().getGlobeStateKey(dc));
        shapeData.setVerticalExaggeration(dc.getVerticalExaggeration());
        shapeData.restartTimer(dc);
    }

    protected Extent makeTileExtent(Terrain terrain, Tile tile)
    {
        // Compute the tile's minimum and maximum height as height above and below the extreme elevations in the tile's
        // sector. We use the overall maximum height of all records in order to ensure that a tile's extent includes its
        // descendants when the parent tile's max height is less than its descendants.
        double[] extremes = terrain.getGlobe().getMinAndMaxElevations(tile.sector);
        double minHeight = extremes[0] - this.defaultBaseDepth;
        double maxHeight = extremes[1] + Math.max(this.maxHeight, this.defaultHeight);

        // Compute the tile's extent for the specified terrain. Associated the shape data's extent with the terrain in
        // order to determine when it becomes invalid.
        return Sector.computeBoundingBox(terrain.getGlobe(), terrain.getVerticalExaggeration(), tile.sector,
            minHeight, maxHeight);
    }

    protected void tessellateTile(Terrain terrain, Tile tile, ShapeData shapeData)
    {
        // Allocate the model coordinate vertices to hold the upper and lower points for all records in the tile. The
        // records in the tile never changes, so the number of vertices in the tile never changes.
        int vertexStride = 3;
        FloatBuffer vertices = shapeData.vertices;
        if (vertices == null)
        {
            int numPoints = 0;
            for (Record record : tile.records)
            {
                numPoints += record.numberOfPoints;
            }

            vertices = Buffers.newDirectFloatBuffer(2 * vertexStride * numPoints);
        }

        double[] location = new double[2];
        float[] vertex = new float[6];
        Vec4 rp = null;

        // Generate the model coordinate vertices and indices for all records in the tile. This may include records that
        // are marked as not visible, as recomputing the vertices and indices for record visibility changes would be
        // expensive. The tessellated interior and outline indices are generated only once, since each record's indices
        // never change.
        for (Record record : tile.records)
        {
            double height = record.height != null ? record.height : this.defaultHeight;
            double depth = this.defaultBaseDepth;
            double NdotR = 0;
            Vec4 N = null;

            this.tess.setEnabled(record.interiorIndices == null); // generate polygon interior and outline indices once
            this.tess.reset();
            this.tess.setPolygonNormal(0, 0, 1); // tessellate in geographic coordinates
            this.tess.beginPolygon();

            for (int i = 0; i < record.getBoundaryCount(); i++)
            {
                this.tess.beginContour();

                VecBuffer points = record.getBoundaryPoints(i);
                for (int j = 0; j < points.getSize(); j++)
                {
                    points.get(j, location);
                    Vec4 p = terrain.getSurfacePoint(Angle.fromDegrees(location[1]), Angle.fromDegrees(location[0]), 0);

                    // Tessellate indices in geographic coordinates. This produces an index tessellation that is
                    // independent of the record's model coordinates, since the count and organization of top and bottom
                    // of vertices is always the same.
                    int index = vertices.position() / vertexStride; // index of top vertex
                    this.tess.addVertex(location[0], location[1], 0, index); // map lon,lat to x,y

                    if (rp == null) // first vertex in the tile
                    {
                        rp = p;
                    }

                    if (N == null) // first vertex in the record
                    {
                        N = terrain.getGlobe().computeSurfaceNormalAtPoint(p);
                        NdotR = p.x * N.x + p.y * N.y + p.z * N.z;
                    }

                    // Add the model coordinate top and bottom vertices, with heights relative to the terrain.
                    double t = height + NdotR - (p.x * N.x + p.y * N.y + p.z * N.z);
                    double b = -depth;
                    vertex[0] = (float) (p.x + N.x * t - rp.x);
                    vertex[1] = (float) (p.y + N.y * t - rp.y);
                    vertex[2] = (float) (p.z + N.z * t - rp.z);
                    vertex[3] = (float) (p.x + N.x * b - rp.x);
                    vertex[4] = (float) (p.y + N.y * b - rp.y);
                    vertex[5] = (float) (p.z + N.z * b - rp.z);
                    vertices.put(vertex);
                }

                this.tess.endContour();
            }

            this.tess.endPolygon();
            this.assembleRecordIndices(this.tess, record);
        }

        shapeData.vertices = (FloatBuffer) vertices.rewind();
        shapeData.referencePoint = rp;
        shapeData.transformMatrix = Matrix.fromTranslation(rp.x, rp.y, rp.z);
        shapeData.vboExpired = true;
    }

    protected void assembleRecordIndices(PolygonTessellator tessellator, Record record)
    {
        if (!tessellator.isEnabled())
            return;

        // Get the tessellated interior and boundary indices, representing a triangle tessellation and line segment
        // tessellation of the record's top vertices. Flip each buffer in order to limit the buffer range we use to
        // values added during tessellation.
        IntBuffer tessInterior = (IntBuffer) tessellator.getInteriorIndices().flip();
        IntBuffer tessBoundary = (IntBuffer) tessellator.getBoundaryIndices().flip();

        // Allocate the record's interior and outline indices. This accounts for the number of tessellated interior
        // and boundary indices, plus the indices necessary to tessellate the record's sides with triangles and lines.
        IntBuffer interiorIndices = IntBuffer.allocate(tessInterior.remaining() + 3 * tessBoundary.remaining());
        IntBuffer outlineIndices = IntBuffer.allocate(2 * tessBoundary.remaining());

        // Fill the triangle index buffer with the triangle tessellation of the polygon's top vertices.
        interiorIndices.put(tessInterior);

        // Fill the triangle index buffer with a triangle tessellation using two triangles to connect the top and bottom
        // vertices at each boundary line. Fill the line index buffer with a horizontal line for each boundary line
        // segment, and a vertical line at the first vertex of each boundary line segment.
        for (int i = tessBoundary.position(); i < tessBoundary.limit(); i += 2)
        {
            int top1 = tessBoundary.get(i);
            int top2 = tessBoundary.get(i + 1);
            int bot1 = top1 + 1; // top and bottom vertices are adjacent
            int bot2 = top2 + 1;
            // side top left triangle
            interiorIndices.put(top1);
            interiorIndices.put(bot1);
            interiorIndices.put(top2);
            // side bottom right triangle
            interiorIndices.put(top2);
            interiorIndices.put(bot1);
            interiorIndices.put(bot2);
            // top horizontal line
            outlineIndices.put(top1);
            outlineIndices.put(top2);
            // vertical line
            outlineIndices.put(top1);
            outlineIndices.put(bot1);
        }

        record.interiorIndices = (IntBuffer) interiorIndices.rewind();
        record.outlineIndices = (IntBuffer) outlineIndices.rewind();
    }

    protected boolean mustAssembleTileAttributeGroups(Tile tile)
    {
        return tile.attributeGroups.isEmpty();
    }

    protected void invalidateTileAttributeGroups(Tile tile)
    {
        tile.attributeGroups.clear();
    }

    protected void assembleTileAttributeGroups(Tile tile)
    {
        tile.attributeGroups.clear();

        // Assemble the tile's records into groups with common attributes. Attributes are grouped by reference using an
        // InstanceHashMap, so that subsequent changes to an Attribute instance will be reflected in the record group
        // automatically. We take care to avoid assembling groups based on any Attribute property, as those properties
        // may change without re-assembling these groups. However, changes to a record's visibility state, highlight
        // state, normal attributes reference and highlight attributes reference invalidate this grouping.
        Map attrMap = new IdentityHashMap();
        for (Record record : tile.records)
        {
            if (!record.isVisible()) // ignore records marked as not visible
                continue;

            ShapeAttributes attrs = this.determineActiveAttributes(record);
            RecordGroup group = attrMap.get(attrs);

            if (group == null) // create a new group if one doesn't already exist
            {
                group = new RecordGroup(attrs);
                attrMap.put(attrs, group); // add it to the map to prevent duplicates
                tile.attributeGroups.add(group); // add it to the tile's attribute group list
            }

            group.records.add(record);
            group.interiorIndexRange.length += record.interiorIndices.remaining();
            group.outlineIndexRange.length += record.outlineIndices.remaining();
        }

        // Make the indices for each record group. We take care to make indices for both the interior and the outline,
        // regardless of the current state of Attributes.isDrawInterior and Attributes.isDrawOutline. This enable these
        // properties change state without needing to re-assemble these groups.
        for (RecordGroup group : tile.attributeGroups)
        {
            int indexCount = group.interiorIndexRange.length + group.outlineIndexRange.length;
            IntBuffer indices = Buffers.newDirectIntBuffer(indexCount);

            group.interiorIndexRange.location = indices.position();
            for (Record record : group.records) // assemble the group's triangle indices in a single contiguous range
            {
                indices.put(record.interiorIndices);
                record.interiorIndices.rewind();
            }

            group.outlineIndexRange.location = indices.position();
            for (Record record : group.records) // assemble the group's line indices in a single contiguous range
            {
                indices.put(record.outlineIndices);
                record.outlineIndices.rewind();
            }

            group.indices = (IntBuffer) indices.rewind();
            group.records.clear();
            group.records.trimToSize(); // Reduce memory overhead from unused ArrayList capacity.
        }
    }

    protected void pickOrderedSurfaceRenderable(DrawContext dc, Point pickPoint)
    {
        try
        {
            this.pickSupport.clearPickList();
            this.pickSupport.beginPicking(dc);
            this.beginDrawing(dc);

            for (Tile tile : this.currentTiles)
            {
                Color color = dc.getUniquePickColor();
                dc.getGL().getGL2().glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue());
                this.pickSupport.addPickableObject(color.getRGB(), tile);
                this.drawTile(dc, tile);
            }

            // TODO: Pick rectangle support
            PickedObject po = this.pickSupport.getTopObject(dc, pickPoint); // resolve the picked tile, if any
            if (po != null)
            {
                this.pickSupport.clearPickList();
                this.drawTileInUniqueColors(dc, (Tile) po.getObject());
                this.pickSupport.resolvePick(dc, pickPoint, this.pickLayer); // resolve the picked records, if any
            }
        }
        finally
        {
            this.endDrawing(dc);
            this.pickSupport.endPicking(dc);
            this.pickSupport.clearPickList();
        }
    }

    protected void drawOrderedSurfaceRenderable(DrawContext dc)
    {
        try
        {
            this.beginDrawing(dc);

            for (Tile tile : this.currentTiles)
            {
                if (dc.isPickingMode())
                {
                    Color color = dc.getUniquePickColor();
                    dc.getGL().getGL2().glColor3ub((byte) color.getRed(), (byte) color.getGreen(),
                        (byte) color.getBlue());
                    this.pickSupport.addPickableObject(color.getRGB(), tile);
                }

                this.drawTile(dc, tile);
            }
        }
        finally
        {
            this.endDrawing(dc);
        }
    }

    protected void beginDrawing(DrawContext dc)
    {
        GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
        gl.glEnable(GL.GL_CULL_FACE);
        gl.glEnableClientState(GL2.GL_VERTEX_ARRAY); // all drawing uses vertex arrays
        gl.glDepthFunc(GL.GL_LEQUAL);
        gl.glMatrixMode(GL2.GL_MODELVIEW);
        gl.glPushMatrix();

        if (!dc.isPickingMode())
        {
            gl.glEnable(GL.GL_BLEND);
            gl.glEnable(GL.GL_LINE_SMOOTH);
            gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
            gl.glHint(GL.GL_LINE_SMOOTH_HINT, GL.GL_FASTEST);
        }
    }

    protected void endDrawing(DrawContext dc)
    {
        GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
        gl.glDisable(GL.GL_CULL_FACE);
        gl.glDisableClientState(GL2.GL_VERTEX_ARRAY);
        gl.glColor4f(1, 1, 1, 1);
        gl.glDepthFunc(GL.GL_LESS);
        gl.glLineWidth(1);
        gl.glPopMatrix();

        if (!dc.isPickingMode())
        {
            gl.glDisable(GL.GL_BLEND);
            gl.glDisable(GL.GL_LINE_SMOOTH);
            gl.glBlendFunc(GL.GL_ONE, GL.GL_ZERO);
            gl.glHint(GL.GL_LINE_SMOOTH_HINT, GL.GL_DONT_CARE);
        }

        if (dc.getGLRuntimeCapabilities().isUseVertexBufferObject())
        {
            gl.glBindBuffer(GL.GL_ARRAY_BUFFER, 0);
            gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, 0);
        }
    }

    protected void drawTile(DrawContext dc, Tile tile)
    {
        GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.
        ShapeData shapeData = tile.currentData;

        int[] vboId = null;
        boolean useVbo = dc.getGLRuntimeCapabilities().isUseVertexBufferObject();
        if (useVbo && (vboId = (int[]) dc.getGpuResourceCache().get(shapeData.vboKey)) == null)
        {
            long vboSize = 4 * shapeData.vertices.remaining(); // 4 bytes for each float vertex component
            vboId = new int[1];
            gl.glGenBuffers(1, vboId, 0);
            gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboId[0]);
            gl.glBufferData(GL.GL_ARRAY_BUFFER, vboSize, shapeData.vertices, GL.GL_STATIC_DRAW);
            gl.glVertexPointer(3, GL.GL_FLOAT, 0, 0);
            dc.getGpuResourceCache().put(shapeData.vboKey, vboId, GpuResourceCache.VBO_BUFFERS, vboSize);
        }
        else if (useVbo)
        {
            gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboId[0]);
            if (shapeData.vboExpired)
            {
                gl.glBufferSubData(GL.GL_ARRAY_BUFFER, 0, 4 * shapeData.vertices.remaining(), shapeData.vertices);
                shapeData.vboExpired = false;
            }
            gl.glVertexPointer(3, GL.GL_FLOAT, 0, 0);
        }
        else
        {
            gl.glVertexPointer(3, GL.GL_FLOAT, 0, shapeData.vertices);
        }

        Matrix modelview = dc.getView().getModelviewMatrix().multiply(shapeData.transformMatrix);
        modelview.toArray(this.matrixArray, 0, false);
        gl.glLoadMatrixd(this.matrixArray, 0);

        for (RecordGroup attrGroup : tile.attributeGroups)
        {
            this.drawTileAttributeGroup(dc, attrGroup);
        }
    }

    protected void drawTileAttributeGroup(DrawContext dc, RecordGroup attributeGroup)
    {
        GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility.

        int[] vboId = null;
        boolean useVbo = dc.getGLRuntimeCapabilities().isUseVertexBufferObject();
        if (useVbo && (vboId = (int[]) dc.getGpuResourceCache().get(attributeGroup.vboKey)) == null)
        {
            long vboSize = 4 * attributeGroup.indices.remaining(); // 4 bytes for each unsigned int index
            vboId = new int[1];
            gl.glGenBuffers(1, vboId, 0);
            gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, vboId[0]);
            gl.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, vboSize, attributeGroup.indices, GL.GL_STATIC_DRAW);
            dc.getGpuResourceCache().put(attributeGroup.vboKey, vboId, GpuResourceCache.VBO_BUFFERS, vboSize);
        }
        else if (useVbo)
        {
            gl.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, vboId[0]);
        }

        if (attributeGroup.attributes.isDrawInterior())
        {
            if (!dc.isPickingMode())
            {
                float[] color = this.colorFloatArray;
                attributeGroup.attributes.getInteriorMaterial().getDiffuse().getRGBColorComponents(color);
                gl.glColor3f(color[0], color[1], color[2]);
            }

            if (useVbo)
            {
                gl.glDrawElements(GL.GL_TRIANGLES, attributeGroup.interiorIndexRange.length, GL.GL_UNSIGNED_INT,
                    4 * attributeGroup.interiorIndexRange.location);
            }
            else
            {
                gl.glDrawElements(GL.GL_TRIANGLES, attributeGroup.interiorIndexRange.length, GL.GL_UNSIGNED_INT,
                    attributeGroup.indices.position(attributeGroup.interiorIndexRange.location));
                attributeGroup.indices.rewind();
            }
        }

        if (attributeGroup.attributes.isDrawOutline())
        {
            gl.glLineWidth((float) attributeGroup.attributes.getOutlineWidth());

            if (!dc.isPickingMode())
            {
                float[] color = this.colorFloatArray;
                attributeGroup.attributes.getOutlineMaterial().getDiffuse().getRGBColorComponents(color);
                gl.glColor3f(color[0], color[1], color[2]);
            }

            if (useVbo)
            {
                gl.glDrawElements(GL.GL_LINES, attributeGroup.outlineIndexRange.length, GL.GL_UNSIGNED_INT,
                    4 * attributeGroup.outlineIndexRange.location);
            }
            else
            {
                gl.glDrawElements(GL.GL_LINES, attributeGroup.outlineIndexRange.length, GL.GL_UNSIGNED_INT,
                    attributeGroup.indices.position(attributeGroup.outlineIndexRange.location));
                attributeGroup.indices.rewind();
            }
        }
    }

    protected void drawTileInUniqueColors(DrawContext dc, Tile tile)
    {
        GL2 gl = dc.getGL().getGL2();
        ShapeData shapeData = tile.currentData;

        int pickColorsSize = shapeData.vertices.remaining(); // 1 RGB color for each XYZ vertex
        if (this.pickColors == null || this.pickColors.capacity() < pickColorsSize)
        {
            this.pickColors = Buffers.newDirectByteBuffer(pickColorsSize);
            dc.getGpuResourceCache().remove(this.pickColorsVboKey); // remove any associated VBO from GPU memory
        }
        this.pickColors.clear();

        ByteBuffer colors;
        int[] vboId = null;
        boolean useVbo = dc.getGLRuntimeCapabilities().isUseVertexBufferObject();
        if (useVbo && (vboId = (int[]) dc.getGpuResourceCache().get(this.pickColorsVboKey)) == null)
        {
            vboId = new int[1];
            gl.glGenBuffers(1, vboId, 0);
            gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboId[0]);
            gl.glBufferData(GL.GL_ARRAY_BUFFER, this.pickColors.remaining(), this.pickColors, GL2.GL_DYNAMIC_DRAW);
            dc.getGpuResourceCache().put(this.pickColorsVboKey, vboId, GpuResourceCache.VBO_BUFFERS,
                this.pickColors.remaining());
            colors = gl.glMapBuffer(GL.GL_ARRAY_BUFFER, GL.GL_WRITE_ONLY);
        }
        else if (useVbo)
        {
            gl.glBindBuffer(GL.GL_ARRAY_BUFFER, vboId[0]);
            colors = gl.glMapBuffer(GL.GL_ARRAY_BUFFER, GL.GL_WRITE_ONLY);
        }
        else
        {
            colors = pickColors;
        }

        byte[] vertexColors = this.colorByteArray;
        for (Record record : tile.records)
        {
            // Get a unique pick color for the record, and add it to the list of pickable objects. We must generate a
            // color for every record, regardless of its visibility, since the tile's color array must match the
            // tile's vertex array, which includes invisible records.
            Color color = dc.getUniquePickColor();
            this.pickSupport.addPickableObject(color.getRGB(), record);

            // top vertex
            vertexColors[0] = (byte) color.getRed();
            vertexColors[1] = (byte) color.getGreen();
            vertexColors[2] = (byte) color.getBlue();
            // bottom vertex
            vertexColors[3] = vertexColors[0];
            vertexColors[4] = vertexColors[1];
            vertexColors[5] = vertexColors[2];

            // Add the unique color for the top and bottom vertices of the record.
            for (int i = 0; i < record.numberOfPoints; i++)
            {
                colors.put(vertexColors);
            }
        }

        colors.flip();

        try
        {
            gl.glEnableClientState(GL2.GL_COLOR_ARRAY);

            if (useVbo)
            {
                gl.glUnmapBuffer(GL.GL_ARRAY_BUFFER);
                gl.glColorPointer(3, GL.GL_UNSIGNED_BYTE, 0, 0);
            }
            else
            {
                gl.glColorPointer(3, GL.GL_UNSIGNED_BYTE, 0, colors);
            }

            this.drawTile(dc, tile);
        }
        finally
        {
            gl.glDisableClientState(GL2.GL_COLOR_ARRAY);
        }
    }

    /**
     * Compute the intersections of a specified line with the geometry corresponding to this shapefile's records. Each
     * record's geometry is created relative to the specified terrain rather than the terrain used during rendering,
     * which may be at lower level of detail than required for accurate intersection determination.
     *
     * @param line    the line to intersect.
     * @param terrain the {@link Terrain} to use when computing each record's geometry.
     *
     * @return a list of intersections identifying where the line intersects this shapefile's records, or null if the
     *         line does not intersect any record.
     *
     * @throws IllegalArgumentException if any argument is null.
     * @throws InterruptedException     if the operation is interrupted.
     * @see Terrain
     */
    public List intersect(Line line, Terrain terrain) throws InterruptedException
    {
        if (line == null)
        {
            String msg = Logging.getMessage("nullValue.LineIsNull");
            Logging.logger().severe(msg);
            throw new IllegalArgumentException(msg);
        }

        if (terrain == null)
        {
            String msg = Logging.getMessage("nullValue.TerrainIsNull");
            Logging.logger().severe(msg);
            throw new IllegalArgumentException(msg);
        }

        ArrayList intersections = new ArrayList();
        this.intersectTileOrDescendants(line, terrain, this.rootTile, intersections);

        return intersections.size() > 0 ? intersections : null;
    }

    protected void intersectTileOrDescendants(Line line, Terrain terrain, Tile tile, List results)
    {
        // Regenerate the tile's intersection geometry as necessary. Synchronized simultaneous read/write access to the
        // tile's intersection data between calls to intersect or Record.intersect on separate threads.
        ShapeData shapeData;
        synchronized (tile)
        {
            shapeData = this.prepareTileIntersectionData(line, terrain, tile);
        }

        if (shapeData == null) // The line does not intersect the tile.
        {
            return;
        }

        // Intersect the line with the tile's records. Translate the line from model coordinates to tile local
        // coordinates in order to perform this operation once on the line, rather than many times for each tile vertex.
        // Intersection points are translated back into model coordinates.
        if (tile.records.size() > 0)
        {
            Line localLine = new Line(line.getOrigin().subtract3(shapeData.referencePoint), line.getDirection());
            for (Record record : tile.records)
            {
                if (record.isVisible()) // records marked as not visible don't intersect anything
                {
                    this.intersectRecordInterior(localLine, terrain, record, shapeData, results);
                }
            }
        }

        // Intersect the line with the tile's children, if any.
        if (tile.children != null)
        {
            for (Tile childTile : tile.children)
            {
                this.intersectTileOrDescendants(line, terrain, childTile, results);
            }
        }
    }

    protected void intersectTileRecord(Line line, Terrain terrain, Record record, List results)
    {
        // Regenerate the tile's intersection geometry as necessary. Synchronized simultaneous read/write access to the
        // tile's intersection data between calls to intersect or Record.intersect on separate threads.
        ShapeData shapeData;
        synchronized (record.tile)
        {
            shapeData = this.prepareTileIntersectionData(line, terrain, record.tile);
        }

        if (shapeData == null) // The line does not intersect the tile.
        {
            return;
        }

        // Intersect the line with the record. Translate the line from model coordinates to tile local coordinates,
        // then translate intersection points back into model coordinates.
        Line localLine = new Line(line.getOrigin().subtract3(shapeData.referencePoint), line.getDirection());
        this.intersectRecordInterior(localLine, terrain, record, shapeData, results);
    }

    protected ShapeData prepareTileIntersectionData(Line line, Terrain terrain, Tile tile)
    {
        // Force regeneration of the tile's intersection extent and intersection geometry when the specified terrain
        // changes. We regenerate the extent now and flag the geometry as invalid in order to force its regeneration
        // later. This is necessary since we want to avoid regenerating the geometry when the line does not intersect
        // the tile's extent.
        IntersectionData shapeData = tile.intersectionData;
        if (!shapeData.isValid(terrain))
        {
            shapeData.setExtent(this.makeTileExtent(terrain, tile)); // regenerate the intersection extent
            shapeData.setTessellationValid(false); // force regeneration of the intersection geometry
            shapeData.setTerrain(terrain);
            shapeData.setGlobeStateKey(terrain.getGlobe().getGlobeStateKey());
            shapeData.setVerticalExaggeration(terrain.getVerticalExaggeration());
        }

        // Determine whether or not the tile's extent intersects the line. If the line does not intersect the tile's
        // extent, then it cannot intersect the tile's records or the tile's children. Note that a tile with no records
        // may have children, so we can't use the tile's record count as a determination of whether or not to test its
        // children.
        if (!shapeData.getExtent().intersects(line))
        {
            return null;
        }

        // Regenerate the tile's intersection geometry as necessary. Suppress tessellation of tiles with no records.
        // Synchronize simultaneous tile updates between rendering, intersect and Record.intersect. Access to this
        // instance's coordinate buffer must be synchronized.
        if (tile.records.size() > 0 && !shapeData.isTessellationValid())
        {
            synchronized (this)
            {
                this.tessellateTile(terrain, tile, shapeData);
            }

            shapeData.setTessellationValid(true);
        }

        return shapeData;
    }

    protected void intersectRecordInterior(Line localLine, Terrain terrain, Record record, ShapeData shapeData,
        List results)
    {
        FloatBuffer vertices = shapeData.vertices;
        IntBuffer indices = record.interiorIndices;

        List recordIntersections = Triangle.intersectTriangles(localLine, vertices, indices);
        if (recordIntersections != null)
        {
            for (Intersection intersection : recordIntersections)
            {
                // Translate the intersection point from tile local coordinates to model coordinates.
                Vec4 pt = intersection.getIntersectionPoint().add3(shapeData.referencePoint);
                intersection.setIntersectionPoint(pt);

                // Compute intersection position relative to ground.
                Position pos = terrain.getGlobe().computePositionFromPoint(pt);
                Vec4 gp = terrain.getSurfacePoint(pos.getLatitude(), pos.getLongitude(), 0);
                double dist = Math.sqrt(pt.dotSelf3()) - Math.sqrt(gp.dotSelf3());
                intersection.setIntersectionPosition(new Position(pos, dist));

                // Associate the record with the intersection and add it to the list of intersection results.
                intersection.setObject(record);
                results.add(intersection);
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy