/*
* 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.
*/
define([
'../shapes/AnnotationAttributes',
'../error/ArgumentError',
'../shaders/BasicTextureProgram',
'../util/Color',
'../util/Font',
'../util/Insets',
'../util/Logger',
'../geom/Matrix',
'../util/Offset',
'../pick/PickedObject',
'../render/Renderable',
'../shapes/TextAttributes',
'../geom/Vec2',
'../geom/Vec3',
'../util/WWMath'
],
function (AnnotationAttributes,
ArgumentError,
BasicTextureProgram,
Color,
Font,
Insets,
Logger,
Matrix,
Offset,
PickedObject,
Renderable,
TextAttributes,
Vec2,
Vec3,
WWMath) {
"use strict";
/**
* Constructs an annotation.
* @alias Annotation
* @constructor
* @augments Renderable
* @classdesc Represents an Annotation shape. An annotation displays a callout, a text and a leader pointing
* the annotation's geographic position to the ground.
* @param {Position} position The annotations's geographic position.
* @param {AnnotationAttributes} attributes The attributes to associate with this annotation.
* @throws {ArgumentError} If the specified position is null or undefined.
*/
var Annotation = function (position, attributes) {
if (!position) {
throw new ArgumentError(
Logger.logMessage(Logger.LEVEL_SEVERE, "Annotation", "constructor", "missingPosition"));
}
Renderable.call(this);
/**
* This annotation's geographic position.
* @type {Position}
*/
this.position = position;
/**
* The annotation's attributes.
* @type {AnnotationAttributes}
* @default see [AnnotationAttributes]{@link AnnotationAttributes}
*/
this.attributes = attributes ? attributes : new AnnotationAttributes(null);
/**
* This annotation's altitude mode. May be one of
* <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>
* @default WorldWind.ABSOLUTE
*/
this.altitudeMode = WorldWind.ABSOLUTE;
// Internal use only. Intentionally not documented.
this.layer = null;
// Internal use only. Intentionally not documented.
this.lastStateKey = null;
// Internal use only. Intentionally not documented.
this.calloutTransform = Matrix.fromIdentity();
// Internal use only. Intentionally not documented.
this.calloutOffset = new WorldWind.Offset(
WorldWind.OFFSET_FRACTION, 0.5,
WorldWind.OFFSET_FRACTION, 0);
// Internal use only. Intentionally not documented.
this.label = "";
// Internal use only. Intentionally not documented.
this.labelTexture = null;
// Internal use only. Intentionally not documented.
this.labelTransform = Matrix.fromIdentity();
// Internal use only. Intentionally not documented.
this.placePoint = new Vec3(0, 0, 0);
// Internal use only. Intentionally not documented.
this.depthOffset = -2.05;
// Internal use only. Intentionally not documented.
this.calloutPoints = null;
};
Annotation.matrix = Matrix.fromIdentity();
Annotation.screenPoint = new Vec3(0, 0, 0);
Annotation.scratchPoint = new Vec3(0, 0, 0);
Annotation.prototype = Object.create(Renderable.prototype);
Object.defineProperties(Annotation.prototype, {
/**
* The text for this annotation.
* @type {String}
* @memberof Annotation.prototype
*/
text: {
get: function () {
return this.label;
},
set: function (value) {
this.label = value;
this.lastStateKey = null;
}
}
});
/**
* Draws this shape as an ordered renderable. Applications do not call this function. It is called by
* [WorldWindow]{@link WorldWindow} during rendering.
* @param {DrawContext} dc The current draw context.
*/
Annotation.prototype.renderOrdered = function (dc) {
this.drawOrderedAnnotation(dc);
if (dc.pickingMode) {
var po = new PickedObject(this.pickColor.clone(), this,
this.position, this.layer, false);
if (dc.pickPoint) {
if (this.labelBounds.containsPoint(
dc.navigatorState.convertPointToViewport(dc.pickPoint, Annotation.scratchPoint))) {
po.labelPicked = true;
}
}
dc.resolvePick(po);
}
};
/**
* Creates a new annotation that is a copy of this annotation.
* @returns {Annotation} The new annotation.
*/
Annotation.prototype.clone = function () {
var clone = new Annotation(this.position);
clone.copy(this);
clone.pickDelegate = this.pickDelegate ? this.pickDelegate : this;
return clone;
};
/**
* Copies the contents of a specified annotation to this annotation.
* @param {Annotation} that The Annotation to copy.
*/
Annotation.prototype.copy = function (that) {
this.position = that.position;
this.enabled = that.enabled;
this.attributes = that.attributes;
this.label = that.label;
this.altitudeMode = that.altitudeMode;
this.pickDelegate = that.pickDelegate;
this.depthOffset = that.depthOffset;
return this;
};
/**
* Renders this annotation. This method is typically not called by applications but is called by
* {@link RenderableLayer} during rendering. For this shape this method creates and
* enques an ordered renderable with the draw context and does not actually draw the annotation.
* @param {DrawContext} dc The current draw context.
*/
Annotation.prototype.render = function (dc) {
if (!this.enabled) {
return;
}
if (!dc.accumulateOrderedRenderables) {
return;
}
if (dc.globe.projectionLimits
&& !dc.globe.projectionLimits.containsLocation(this.position.latitude, this.position.longitude)) {
return;
}
var orderedAnnotation;
if (this.lastFrameTime !== dc.timestamp) {
orderedAnnotation = this.makeOrderedRenderable(dc);
} else {
var annotationCopy = this.clone();
orderedAnnotation = annotationCopy.makeOrderedRenderable(dc);
}
if (!orderedAnnotation) {
return;
}
orderedAnnotation.layer = dc.currentLayer;
this.lastFrameTime = dc.timestamp;
dc.addOrderedRenderable(orderedAnnotation);
};
// Internal. Intentionally not documented.
Annotation.prototype.drawOrderedAnnotation = function (dc) {
this.beginDrawing(dc);
try {
this.doDrawOrderedAnnotation(dc);
} finally {
this.endDrawing(dc);
}
};
/* Intentionally not documented
* Creates an ordered renderable for this shape.
* @protected
* @param {DrawContext} dc The current draw context.
* @returns {OrderedRenderable} The ordered renderable. May be null, in which case an ordered renderable
* cannot be created or should not be created at the time this method is called.
*/
Annotation.prototype.makeOrderedRenderable = function (dc) {
var w, h, s, iLeft, iRight, iTop, iBottom,
offset, leaderGapHeight;
// Wraps the text based and the width and height that were set for the
// annotation
this.label = dc.textSupport.wrap(
this.label,
this.attributes.width, this.attributes.height,
this.attributes.textAttributes.font);
// Compute the annotation's model point.
dc.surfacePointForMode(this.position.latitude, this.position.longitude, this.position.altitude,
this.altitudeMode, this.placePoint);
this.eyeDistance = dc.navigatorState.eyePoint.distanceTo(this.placePoint);
// Compute the annotation's screen point in the OpenGL coordinate system of the WorldWindow
// by projecting its model coordinate point onto the viewport. Apply a depth offset in order
// to cause the annotation to appear above nearby terrain.
if (!dc.navigatorState.projectWithDepth(this.placePoint, this.depthOffset, Annotation.screenPoint)) {
return null;
}
var labelFont = this.attributes.textAttributes.font;
var labelKey = this.label + labelFont.toString();
this.labelTexture = dc.gpuResourceCache.resourceForKey(labelKey);
if (!this.labelTexture) {
this.labelTexture = dc.textSupport.createTexture(dc, this.label, labelFont, false);
dc.gpuResourceCache.putResource(labelKey, this.labelTexture, this.labelTexture.size);
}
w = this.labelTexture.imageWidth;
h = this.labelTexture.imageHeight;
s = this.attributes.scale;
iLeft = this.attributes.insets.left;
iRight = this.attributes.insets.right;
iTop = this.attributes.insets.top;
iBottom = this.attributes.insets.bottom;
leaderGapHeight = this.attributes.leaderGapHeight;
offset = this.calloutOffset.offsetForSize((w + iLeft + iRight) * s, (h + iTop + iBottom) * s);
this.calloutTransform.setTranslation(
Annotation.screenPoint[0] - offset[0],
Annotation.screenPoint[1] + leaderGapHeight,
Annotation.screenPoint[2]);
this.labelTransform.setTranslation(
Annotation.screenPoint[0] - offset[0] + iLeft * s,
Annotation.screenPoint[1] + leaderGapHeight + iBottom * s,
Annotation.screenPoint[2]);
this.labelTransform.setScale(w * s, h * s, 1);
this.labelBounds = WWMath.boundingRectForUnitQuad(this.labelTransform);
// Compute dimensions of the callout taking in consideration the insets
var width = (w + iLeft + iRight) * s;
var height = (h + iTop + iBottom) * s;
var leaderOffsetX = (width / 2);
var leaderOffsetY = -leaderGapHeight;
if (!this.attributes.drawLeader) {
leaderOffsetY = 0;
}
if (this.attributes.stateKey != this.lastStateKey) {
this.calloutPoints = this.createCallout(
width, height,
leaderOffsetX, leaderOffsetY,
this.attributes.leaderGapWidth, this.attributes.cornerRadius);
}
return this;
};
// Internal. Intentionally not documented.
Annotation.prototype.beginDrawing = function (dc) {
var gl = dc.currentGlContext,
program;
dc.findAndBindProgram(BasicTextureProgram);
program = dc.currentProgram;
gl.enableVertexAttribArray(program.vertexPointLocation);
gl.enableVertexAttribArray(program.vertexTexCoordLocation);
program.loadModulateColor(gl, dc.pickingMode);
};
// Internal. Intentionally not documented.
Annotation.prototype.endDrawing = function (dc) {
var gl = dc.currentGlContext,
program = dc.currentProgram;
// Clear the vertex attribute state.
gl.disableVertexAttribArray(program.vertexPointLocation);
gl.disableVertexAttribArray(program.vertexTexCoordLocation);
// Clear GL bindings.
dc.bindProgram(null);
};
// Internal. Intentionally not documented.
Annotation.prototype.drawCorner = function (x0, y0, cornerRadius, start, end, steps, buffer, startIdx) {
if (cornerRadius < 1) {
return startIdx;
}
var step = (end - start) / (steps - 1);
for (var i = 1; i < steps - 1; i++) {
var a = start + step * i;
var x = x0 + Math.cos(a) * cornerRadius;
var y = y0 + Math.sin(a) * cornerRadius;
buffer[startIdx++] = x;
buffer[startIdx++] = y;
}
return startIdx;
};
// Internal. Intentionally not documented.
Annotation.prototype.createCallout = function (width, height, leaderOffsetX, leaderOffsetY, leaderGapWidth,
cornerRadius) {
var cornerSteps = 16;
var numVertices = 2 * (12 + (cornerRadius < 1 ? 0 : 4 * (cornerSteps - 2)));
var buffer = new Float32Array(numVertices);
var idx = 0;
//Bottom right
buffer[idx++] = width / 2 + leaderGapWidth / 2;
buffer[idx++] = 0;
buffer[idx++] = width - cornerRadius;
buffer[idx++] = 0;
idx = this.drawCorner(width - cornerRadius, cornerRadius, cornerRadius, -Math.PI / 2, 0,
cornerSteps, buffer, idx);
//Right
buffer[idx++] = width;
buffer[idx++] = cornerRadius;
buffer[idx++] = width;
buffer[idx++] = height - cornerRadius;
idx = this.drawCorner(width - cornerRadius, height - cornerRadius, cornerRadius, 0, Math.PI / 2,
cornerSteps, buffer, idx);
//Top
buffer[idx++] = width - cornerRadius;
buffer[idx++] = height;
buffer[idx++] = cornerRadius;
buffer[idx++] = height;
idx = this.drawCorner(cornerRadius, height - cornerRadius, cornerRadius, Math.PI / 2, Math.PI,
cornerSteps, buffer, idx);
//Left
buffer[idx++] = 0;
buffer[idx++] = height - cornerRadius;
buffer[idx++] = 0;
buffer[idx++] = cornerRadius;
idx = this.drawCorner(cornerRadius, cornerRadius, cornerRadius, Math.PI, Math.PI * 1.5,
cornerSteps, buffer, idx);
//Bottom left
buffer[idx++] = cornerRadius;
buffer[idx++] = 0;
buffer[idx++] = width / 2 - leaderGapWidth / 2;
buffer[idx++] = 0;
//Draw leader
buffer[idx++] = leaderOffsetX;
buffer[idx++] = leaderOffsetY;
buffer[idx++] = width / 2 + leaderGapWidth / 2;
buffer[idx] = 0;
return buffer;
};
// Internal. Intentionally not documented.
Annotation.prototype.doDrawOrderedAnnotation = function (dc) {
var gl = dc.currentGlContext,
program = dc.currentProgram,
textureBound;
var refreshBuffers = false;
if (dc.pickingMode) {
this.pickColor = dc.uniquePickColor();
}
program.loadOpacity(gl, this.attributes.opacity);
// Attributes have changed. We need to track this because the callout vbo data may
// have changed if scaled or text wrapping changes callout dimensions
var calloutAttributesChanged = (this.attributes.stateKey != this.lastStateKey);
// Create new cache key if callout drawing points have changed
if (!this.calloutCacheKey || calloutAttributesChanged) {
this.calloutCacheKey = dc.gpuResourceCache.generateCacheKey();
}
var calloutVboId = dc.gpuResourceCache.resourceForKey(this.calloutCacheKey);
if (!calloutVboId) {
calloutVboId = gl.createBuffer();
dc.gpuResourceCache.putResource(this.calloutCacheKey, calloutVboId,
this.calloutPoints.length * 4);
refreshBuffers = true;
}
// Remove the last generated vbo data if attributes changed
if (calloutAttributesChanged && this.calloutCacheKey) {
dc.gpuResourceCache.removeResource(this.calloutCacheKey);
}
// Store current statekey because we are no longer using it
// in this iteration
this.lastStateKey = this.attributes.stateKey;
// Compute and specify the MVP matrix.
Annotation.matrix.copy(dc.screenProjection);
Annotation.matrix.multiplyMatrix(this.calloutTransform);
program.loadModelviewProjection(gl, Annotation.matrix);
gl.bindBuffer(gl.ARRAY_BUFFER, calloutVboId);
if (refreshBuffers) {
gl.bufferData(gl.ARRAY_BUFFER,
this.calloutPoints, gl.STATIC_DRAW);
dc.frameStatistics.incrementVboLoadCount(1);
}
program.loadColor(gl, dc.pickingMode ? this.pickColor : this.attributes.backgroundColor);
program.loadTextureEnabled(gl, false);
gl.vertexAttribPointer(program.vertexPointLocation, 2, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer(program.vertexTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_FAN, 0, this.calloutPoints.length / 2);
// Draw text
Annotation.matrix.copy(dc.screenProjection);
Annotation.matrix.multiplyMatrix(this.labelTransform);
program.loadModelviewProjection(gl, Annotation.matrix);
Annotation.matrix.setToIdentity();
Annotation.matrix.multiplyByTextureTransform(this.labelTexture);
program.loadTextureMatrix(gl, Annotation.matrix);
program.loadColor(gl, dc.pickingMode ? this.pickColor : this.attributes.textAttributes.color);
textureBound = this.labelTexture.bind(dc);
program.loadTextureEnabled(gl, textureBound);
// Configure GL to use the draw context's unit quad VBOs for both model coordinates and texture coordinates.
// Most browsers can share the same buffer for vertex and texture coordinates, but Internet Explorer requires
// that they be in separate buffers, so the code below uses the 3D buffer for vertex coords and the 2D
// buffer for texture coords.
gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer3());
gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer());
gl.vertexAttribPointer(program.vertexTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
};
return Annotation;
});