/*
* Copyright 2015-2017 WorldWind Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @exports Path
*/
define([
'../shapes/AbstractShape',
'../error/ArgumentError',
'../shaders/BasicTextureProgram',
'../geom/BoundingBox',
'../util/Color',
'../geom/Location',
'../util/Logger',
'../geom/Matrix',
'../pick/PickedObject',
'../geom/Position',
'../shapes/ShapeAttributes',
'../shapes/SurfacePolyline',
'../geom/Vec2',
'../geom/Vec3'
],
function (AbstractShape,
ArgumentError,
BasicTextureProgram,
BoundingBox,
Color,
Location,
Logger,
Matrix,
PickedObject,
Position,
ShapeAttributes,
SurfacePolyline,
Vec2,
Vec3) {
"use strict";
/**
* Constructs a path.
* @alias Path
* @constructor
* @augments AbstractShape
* @classdesc Represents a line, curve or curtain between specified positions. The path is drawn between input
* positions to achieve a specified path type, which can be one of the following:
* <ul>
* <li>[WorldWind.GREAT_CIRCLE]{@link WorldWind#GREAT_CIRCLE}</li>
* <li>[WorldWind.RHUMB_LINE]{@link WorldWind#RHUMB_LINE}</li>
* <li>[WorldWind.LINEAR]{@link WorldWind#LINEAR}</li>
* </ul>
* <p>
* Paths conform to the terrain if the path's [followTerrain]{@link Path#followTerrain} property is true.
* <p>
* Altitudes within the path's positions are interpreted according to the path's altitude mode, which
* can be one of the following:
* <ul>
* <li>[WorldWind.ABSOLUTE]{@link WorldWind#ABSOLUTE}</li>
* <li>[WorldWind.RELATIVE_TO_GROUND]{@link WorldWind#RELATIVE_TO_GROUND}</li>
* <li>[WorldWind.CLAMP_TO_GROUND]{@link WorldWind#CLAMP_TO_GROUND}</li>
* </ul>
* If the latter, the path positions' altitudes are ignored.
* <p>
* Paths have separate attributes for normal display and highlighted display. They use the interior and
* outline attributes of {@link ShapeAttributes} but do not use the image attributes.
* <p>
* A path displays as a curtain if its [extrude]{@link Path#extrude} property is true. A curtain extends
* from the line formed by the path positions to the ground.
* <p>
* This shape uses a {@link SurfacePolyline} when drawing on 2D globes and this shape's
* [useSurfaceShapeFor2D]{@link AbstractShape#useSurfaceShapeFor2D} is true.
*
* @param {Position[]} positions An array containing the path positions.
* @param {ShapeAttributes} attributes The attributes to associate with this path. May be null, in which case
* default attributes are associated.
* @throws {ArgumentError} If the specified positions array is null or undefined.
*/
var Path = function (positions, attributes) {
if (!positions) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "Path", "constructor", "missingPositions"));
}
AbstractShape.call(this, attributes);
// Private. Documentation is with the defined property below.
this._positions = positions;
// Private. Documentation is with the defined property below.
this._pathType = WorldWind.GREAT_CIRCLE;
// Private. Documentation is with the defined property below.
this._terrainConformance = 10;
// Private. Documentation is with the defined property below.
this._numSubSegments = 10;
this.referencePosition = this.determineReferencePosition(this._positions);
this.scratchPoint = new Vec3(0, 0, 0); // scratch variable
};
Path.prototype = Object.create(AbstractShape.prototype);
Object.defineProperties(Path.prototype, {
/**
* This path's positions.
* @type {Position[]}
* @memberof Path.prototype
*/
positions: {
get: function () {
return this._positions;
},
set: function (positions) {
if (!positions) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "Path", "constructor", "missingPositions"));
}
this._positions = positions;
this.referencePosition = this.determineReferencePosition(this._positions);
this.reset();
}
},
/**
* Indicates whether this path should conform to the terrain.
* @type {Boolean}
* @default false
* @memberof Path.prototype
*/
followTerrain: {
get: function () {
return this._followTerrain;
},
set: function (followTerrain) {
this._followTerrain = followTerrain;
this.reset();
}
},
/**
* Specifies how accurately this path must adhere to the terrain when the path is terrain following. The value
* specifies the maximum number of pixels between tessellation points. Lower values increase accuracy but decrease
* performance.
* @type {Number}
* @default 10
* @memberof Path.prototype
*/
terrainConformance: {
get: function () {
return this._terrainConformance;
},
set: function (terrainConformance) {
this._terrainConformance = terrainConformance;
this.reset();
}
},
/**
* Specifies the number of segments used between specified positions to achieve this path's path type. Higher values
* cause the path to conform more closely to the path type but decrease performance.
* <p/>
* Note: The sub-segments number is ignored when the path follows terrain or when the path type is
* WorldWind.LINEAR.
* @type {Number}
* @default 10
* @memberof Path.prototype
*/
numSubSegments: {
get: function () {
return this._numSubSegments;
},
set: function (numSubSegments) {
this._numSubSegments = numSubSegments >= 0 ? numSubSegments : 0;
this.reset();
}
},
/**
* The type of path to follow when drawing the path. Recognized values are:
* <ul>
* <li>[WorldWind.GREAT_CIRCLE]{@link WorldWind#GREAT_CIRCLE}</li>
* <li>[WorldWind.RHUMB_LINE]{@link WorldWind#RHUMB_LINE}</li>
* <li>[WorldWind.LINEAR]{@link WorldWind#LINEAR}</li>
* </ul>
* @type {String}
* @default WorldWind.GREAT_CIRCLE
* @memberof Path.prototype
*/
pathType: {
get: function () {
return this._pathType;
},
set: function (pathType) {
this._pathType = pathType;
this.reset();
}
},
/**
* Specifies whether to extrude this path to the ground by drawing a filled interior from the path to the
* terrain. The filled interior uses this path's interior attributes.
* @type {Boolean}
* @default false
* @memberof Path.prototype
*/
extrude: {
get: function () {
return this._extrude;
},
set: function (extrude) {
this._extrude = extrude;
this.reset();
}
}
});
// Intentionally not documented.
Path.prototype.determineReferencePosition = function (positions) {
// Assign the first position as the reference position.
return (positions.length > 0) ? positions[0] : null;
};
// Internal. Determines whether this shape's geometry must be re-computed.
Path.prototype.mustGenerateGeometry = function (dc) {
if (!this.currentData.tessellatedPoints) {
return true;
}
if (this.currentData.drawInterior !== this.activeAttributes.drawInterior
|| this.currentData.drawVerticals !== this.activeAttributes.drawVerticals) {
return true;
}
if (!this.followTerrain && this.currentData.numSubSegments !== this.numSubSegments) {
return true;
}
if (this.followTerrain && this.currentData.terrainConformance !== this.terrainConformance) {
return true;
}
if (this.altitudeMode === WorldWind.ABSOLUTE) {
return false;
}
return this.currentData.isExpired
};
Path.prototype.createSurfaceShape = function () {
return new SurfacePolyline(this.positions, null);
};
// Overridden from AbstractShape base class.
Path.prototype.doMakeOrderedRenderable = function (dc) {
// A null reference position is a signal that there are no positions to render.
if (!this.referencePosition) {
return null;
}
// See if the current shape data can be re-used.
if (!this.mustGenerateGeometry(dc)) {
return this;
}
// Set the transformation matrix to correspond to the reference position.
var refPt = this.currentData.referencePoint;
dc.surfacePointForMode(this.referencePosition.latitude, this.referencePosition.longitude,
this.referencePosition.altitude, this._altitudeMode, refPt);
this.currentData.transformationMatrix.setToTranslation(refPt[0], refPt[1], refPt[2]);
// Tessellate the path in geographic coordinates.
var tessellatedPositions = this.makeTessellatedPositions(dc);
if (tessellatedPositions.length < 2) {
return null;
}
// Convert the tessellated geographic coordinates to the Cartesian coordinates that will be rendered.
var tessellatedPoints = this.computeRenderedPath(dc, tessellatedPositions);
this.currentData.tessellatedPoints = tessellatedPoints;
this.currentData.drawInterior = this.activeAttributes.drawInterior;
this.currentData.drawVerticals = this.activeAttributes.drawVerticals;
this.currentData.numSubSegments = this.numSubSegments;
this.currentData.terrainConformance = this.terrainConformance;
this.resetExpiration(this.currentData);
this.currentData.fillVbo = true;
// Create the extent from the Cartesian points. Those points are relative to this path's reference point, so
// translate the computed extent to the reference point.
if (!this.currentData.extent) {
this.currentData.extent = new BoundingBox();
}
this.currentData.extent.setToPoints(tessellatedPoints);
this.currentData.extent.translate(this.currentData.referencePoint);
return this;
};
// Private. Intentionally not documented.
Path.prototype.makeTessellatedPositions = function (dc) {
var tessellatedPositions = [],
navState = dc.navigatorState,
showVerticals = this.mustDrawVerticals(dc),
ptA = new Vec3(0, 0, 0),
ptB = new Vec3(0, 0, 0),
posA = this._positions[0],
posB, eyeDistance, pixelSize;
if (showVerticals) {
this.currentData.verticalIndices = new Int16Array(this.positions.length * 2);
this.currentData.verticalIndices[0] = 0;
this.currentData.verticalIndices[1] = 1;
}
tessellatedPositions.push(posA);
dc.surfacePointForMode(posA.latitude, posA.longitude, posA.altitude, this._altitudeMode, ptA);
for (var i = 1, len = this._positions.length; i < len; i++) {
posB = this._positions[i];
dc.surfacePointForMode(posB.latitude, posB.longitude, posB.altitude, this._altitudeMode, ptB);
eyeDistance = navState.eyePoint.distanceTo(ptA);
pixelSize = navState.pixelSizeAtDistance(eyeDistance);
if (ptA.distanceTo(ptB) < pixelSize * 8 && this.altitudeMode !== WorldWind.ABSOLUTE) {
tessellatedPositions.push(posB); // distance is short so no need for sub-segments
} else {
this.makeSegment(dc, posA, posB, ptA, ptB, tessellatedPositions);
}
posA = posB;
ptA.copy(ptB);
if (showVerticals) {
var k = 2 * (tessellatedPositions.length - 1);
this.currentData.verticalIndices[i * 2] = k;
this.currentData.verticalIndices[i * 2 + 1] = k + 1;
}
}
return tessellatedPositions;
};
// Private. Intentionally not documented.
Path.prototype.makeSegment = function (dc, posA, posB, ptA, ptB, tessellatedPositions) {
var navState = dc.navigatorState,
eyePoint = navState.eyePoint,
pos = new Location(0, 0),
height = 0,
arcLength, segmentAzimuth, segmentDistance, s, p, distance;
// If it's just a straight line and not terrain following, then the segment is just two points.
if (this._pathType === WorldWind.LINEAR && !this._followTerrain) {
if (!ptA.equals(ptB)) {
tessellatedPositions.push(posB);
}
return;
}
// Compute the segment length.
if (this._pathType === WorldWind.LINEAR) {
segmentDistance = Location.linearDistance(posA, posB);
} else if (this._pathType === WorldWind.RHUMB_LINE) {
segmentDistance = Location.rhumbDistance(posA, posB);
} else {
segmentDistance = Location.greatCircleDistance(posA, posB);
}
if (this._altitudeMode !== WorldWind.CLAMP_TO_GROUND) {
height = 0.5 * (posA.altitude + posB.altitude);
}
arcLength = segmentDistance * (dc.globe.equatorialRadius + height * dc.verticalExaggeration);
if (arcLength <= 0) { // segment is 0 length
return;
}
// Compute the azimuth to apply while tessellating the segment.
if (this._pathType === WorldWind.LINEAR) {
segmentAzimuth = Location.linearAzimuth(posA, posB);
} else if (this._pathType === WorldWind.RHUMB_LINE) {
segmentAzimuth = Location.rhumbAzimuth(posA, posB);
} else {
segmentAzimuth = Location.greatCircleAzimuth(posA, posB);
}
this.scratchPoint.copy(ptA);
for (s = 0, p = 0; s < 1;) {
if (this._followTerrain) {
p += this._terrainConformance * navState.pixelSizeAtDistance(this.scratchPoint.distanceTo(eyePoint));
} else {
p += arcLength / this._numSubSegments;
}
// Stop adding intermediate positions when we reach the arc length, or the remaining distance is in
// millimeters on Earth.
if (arcLength < p || arcLength - p < 1e-9)
break;
s = p / arcLength;
distance = s * segmentDistance;
if (this._pathType === WorldWind.LINEAR) {
Location.linearLocation(posA, segmentAzimuth, distance, pos);
} else if (this._pathType === WorldWind.RHUMB_LINE) {
Location.rhumbLocation(posA, segmentAzimuth, distance, pos);
} else {
Location.greatCircleLocation(posA, segmentAzimuth, distance, pos);
}
pos.altitude = (1 - s) * posA.altitude + s * posB.altitude;
tessellatedPositions.push(new Position(pos.latitude, pos.longitude, pos.altitude));
if (this._followTerrain) {
// Compute a new reference point for eye distance.
dc.surfacePointForMode(pos.latitude, pos.longitude, pos.altitude,
WorldWind.CLAMP_TO_GROUND, this.scratchPoint);
}
}
tessellatedPositions.push(posB);
};
// Private. Intentionally not documented.
Path.prototype.computeRenderedPath = function (dc, tessellatedPositions) {
var capturePoles = this.mustDrawInterior(dc) || this.mustDrawVerticals(dc),
eyeDistSquared = Number.MAX_VALUE,
eyePoint = dc.navigatorState.eyePoint,
numPoints = (capturePoles ? 2 : 1) * tessellatedPositions.length,
tessellatedPoints = new Float32Array(numPoints * 3),
stride = capturePoles ? 6 : 3,
pt = new Vec3(0, 0, 0),
altitudeMode, pos, k, dSquared;
if (this._followTerrain && this.altitudeMode !== WorldWind.CLAMP_TO_GROUND) {
altitudeMode = WorldWind.RELATIVE_TO_GROUND;
} else {
altitudeMode = this.altitudeMode;
}
for (var i = 0, len = tessellatedPositions.length; i < len; i++) {
pos = tessellatedPositions[i];
dc.surfacePointForMode(pos.latitude, pos.longitude, pos.altitude, altitudeMode, pt);
dSquared = pt.distanceToSquared(eyePoint);
if (dSquared < eyeDistSquared) {
eyeDistSquared = dSquared;
}
pt.subtract(this.currentData.referencePoint);
k = stride * i;
tessellatedPoints[k] = pt[0];
tessellatedPoints[k + 1] = pt[1];
tessellatedPoints[k + 2] = pt[2];
if (capturePoles) {
dc.surfacePointForMode(pos.latitude, pos.longitude, 0, WorldWind.CLAMP_TO_GROUND, pt);
dSquared = pt.distanceToSquared(eyePoint);
if (dSquared < eyeDistSquared) {
eyeDistSquared = dSquared;
}
pt.subtract(this.currentData.referencePoint);
tessellatedPoints[k + 3] = pt[0];
tessellatedPoints[k + 4] = pt[1];
tessellatedPoints[k + 5] = pt[2];
}
}
this.currentData.pointBufferHasExtrusionPoints = capturePoles;
this.currentData.eyeDistance = Math.sqrt(eyeDistSquared);
return tessellatedPoints;
};
// Private. Intentionally not documented.
Path.prototype.mustDrawInterior = function (dc) {
return this.activeAttributes.drawInterior
&& this._extrude
&& this._altitudeMode !== WorldWind.CLAMP_TO_GROUND;
};
// Private. Intentionally not documented.
Path.prototype.mustDrawVerticals = function (dc) {
return this.activeAttributes.drawOutline && this.activeAttributes.drawVerticals
&& this.altitudeMode !== WorldWind.CLAMP_TO_GROUND;
};
// Overridden from AbstractShape base class.
Path.prototype.doRenderOrdered = function (dc) {
var gl = dc.currentGlContext,
program = dc.currentProgram,
currentData = this.currentData,
numPoints = currentData.tessellatedPoints.length / 3,
vboId, opacity, color, pickColor, stride, nPts;
this.applyMvpMatrix(dc);
if (!currentData.vboCacheKey) {
currentData.vboCacheKey = dc.gpuResourceCache.generateCacheKey();
}
vboId = dc.gpuResourceCache.resourceForKey(currentData.vboCacheKey);
if (!vboId) {
vboId = gl.createBuffer();
dc.gpuResourceCache.putResource(this.currentData.vboCacheKey, vboId,
currentData.tessellatedPoints.length * 4);
currentData.fillVbo = true;
}
// Bind and if necessary fill the VBO. We fill the VBO here rather than in doMakeOrderedRenderable so that
// there's no possibility of the VBO being ejected from the cache between the time it's filled and
// the time it's used.
gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
if (currentData.fillVbo) {
gl.bufferData(gl.ARRAY_BUFFER, currentData.tessellatedPoints,
gl.STATIC_DRAW);
dc.frameStatistics.incrementVboLoadCount(1);
}
program.loadTextureEnabled(gl, false);
if (dc.pickingMode) {
pickColor = dc.uniquePickColor();
}
if (this.mustDrawInterior(dc)) {
color = this.activeAttributes.interiorColor;
opacity = color.alpha * dc.currentLayer.opacity;
// Disable writing the shape's fragments to the depth buffer when the interior is semi-transparent.
if (opacity < 1 && !dc.pickingMode) {
gl.depthMask(false);
}
program.loadColor(gl, dc.pickingMode ? pickColor : color);
program.loadOpacity(gl, dc.pickingMode ? (opacity > 0 ? 1 : 0) : opacity);
gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, numPoints);
}
if (this.activeAttributes.drawOutline) {
if ((this.mustDrawVerticals(dc) && this.mustDrawInterior(dc))
|| this.altitudeMode === WorldWind.CLAMP_TO_GROUND) {
// Make the verticals stand out from the interior, or the outline stand out from the terrain.
this.applyMvpMatrixForOutline(dc);
}
color = this.activeAttributes.outlineColor;
opacity = color.alpha * dc.currentLayer.opacity;
// Disable writing the shape's fragments to the depth buffer when the interior is semi-transparent.
if (opacity < 1 && !dc.pickingMode) {
gl.depthMask(false);
}
program.loadColor(gl, dc.pickingMode ? pickColor : color);
program.loadOpacity(gl, dc.pickingMode ? 1 : opacity);
gl.lineWidth(this.activeAttributes.outlineWidth);
if (this.currentData.pointBufferHasExtrusionPoints) {
stride = 24;
nPts = numPoints / 2;
} else {
stride = 12;
nPts = numPoints;
}
gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, stride, 0);
gl.drawArrays(gl.LINE_STRIP, 0, nPts);
if (this.mustDrawVerticals(dc)) {
if (!currentData.verticalIndicesVboCacheKey) {
currentData.verticalIndicesVboCacheKey = dc.gpuResourceCache.generateCacheKey();
}
vboId = dc.gpuResourceCache.resourceForKey(currentData.verticalIndicesVboCacheKey);
if (!vboId) {
vboId = gl.createBuffer();
dc.gpuResourceCache.putResource(currentData.verticalIndicesVboCacheKey, vboId,
currentData.verticalIndices.length * 4);
currentData.fillVbo = true;
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vboId);
if (currentData.fillVbo) {
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, currentData.verticalIndices,
gl.STATIC_DRAW);
dc.frameStatistics.incrementVboLoadCount(1);
}
gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
gl.drawElements(gl.LINES, currentData.verticalIndices.length,
gl.UNSIGNED_SHORT, 0);
}
}
currentData.fillVbo = false;
if (dc.pickingMode) {
var po = new PickedObject(pickColor, this.pickDelegate ? this.pickDelegate : this, null, dc.currentLayer,
false);
dc.resolvePick(po);
}
};
// Overridden from AbstractShape base class.
Path.prototype.beginDrawing = function (dc) {
var gl = dc.currentGlContext;
if (this.mustDrawInterior(dc)) {
gl.disable(gl.CULL_FACE);
}
dc.findAndBindProgram(BasicTextureProgram);
gl.enableVertexAttribArray(dc.currentProgram.vertexPointLocation);
};
// Overridden from AbstractShape base class.
Path.prototype.endDrawing = function (dc) {
var gl = dc.currentGlContext;
gl.disableVertexAttribArray(dc.currentProgram.vertexPointLocation);
gl.depthMask(true);
gl.lineWidth(1);
gl.enable(gl.CULL_FACE);
};
return Path;
});