var inherit = require('./inherit');
var meshFeature = require('./meshFeature');
var registry = require('./registry');
var util = require('./util');
/**
* Isoline feature specification.
*
* @typedef {geo.feature.spec} geo.isolineFeature.spec
* @extend geo.feature.spec
* @property {object[]} [data=[]] An array of arbitrary objects used to
* construct the feature.
* @property {geo.isolineFeature.styleSpec} [style] An object that contains
* style values for the feature.
* @property {geo.isolineFeature.isolineSpec} [isoline] The isoline
* specification for the feature.
*/
/**
* Style specification for an isoline feature. Extends
* {@link geo.lineFeasture.styleSpec} and {@link geo.textFeasture.styleSpec}.
*
* @typedef {geo.feature.styleSpec} geo.isolineFeature.styleSpec
* @extends geo.feature.styleSpec
* @extends geo.textFeature.styleSpec
* @extends geo.lineFeature.styleSpec
* @property {geo.geoPosition|function} [position=data] The position of each
* data element. This defaults to just using `x`, `y`, and `z` properties
* of the data element itself. The position is in the feature's gcs
* coordinates.
* @property {number|function} [value=data.z] The value of each data element.
* This defaults to the `z` property of the data elements. If the value of
* a grid point is `null` or `undefined`, the point and elements that use
* that point won't be included in the results.
* @property {geo.geoColor|function} [strokeColor='black'] Color to stroke each
* line.
* @property {number|function} [strokeWidth] The weight of the line stroke in
* pixels. This defaults to the line value's level + 0.5.
* @property {boolean|function} [rotateWithMap=true] Rotate label text when the
* map rotates.
* @property {number|function} [rotation] Text rotation in radians. This
* defaults to the label oriented so that top of the text is toward the
* higher value. There is a utility function that can be used for common
* rotation preferences. See {@link geo.isolineFeature#rotationFunction}.
* For instance, `rotation=geo.isolineFeature.rotationFunction('map')`.
* @property {string|function} [fontSize='12px'] The font size.
* @property {geo.geoColor|function} [textStrokeColor='white'] Text
* stroke color. This adds contrast between the label and the isoline.
* @property {geo.geoColor|function} [textStrokeWidth=2] Text stroke width in
* pixels.
*/
/**
* Isoline specification. All of these properties can be functions, which get
* passed the {@link geo.meshFeature.meshInfo} object.
*
* @typedef {geo.meshFeature.meshSpec} geo.isolineFeature.isolineSpec
* @extends geo.meshFeature.meshSpec
* @property {number} [min] Minimum isoline value. If unspecified, taken from
* the computed minimum of the `value` style.
* @property {number} [max] Maximum isoline value. If unspecified, taken from
* the computed maximum of the `value` style.
* @property {number} [count=15] Approximate number of isolines shown through
* the value range. Used if `spacing` or `values` is not specified.
* @property {boolean} [autofit=true] If `count` is used to determine the
* isolines, and this is truthy, the isoline values will be round numbers.
* If falsy, they will include the exact minimum and maximum values.
* @property {number} [spacing] Distance in value units between isolines.
* Used if specified and `values` is not specified.
* @property {number[]|geo.isolineFeature.valueEntry[]} [values] An array of
* explicit values for isolines.
* @property {number[]} [levels=[5, 5]] If `values` is not used to explicitly
* set isoline levels, this determines the spacing of levels which can be
* used to style lines distinctly. Most isolines will be level 0. If
* `levels` is an array of [`n0`, `n1`, ...], every `n0`th line will be
* level 1, every `n0 * n1`th line will be level 2, etc.
* @property {boolean|function} [label] Truthy if a label should be shown for a
* isoline value. If a function, this is called with
* `(geo.isolineFeature.valueEntry, index)`. This defaults to
* `valueEntry.level >= 1`.
* @property {string|function} [labelText] Text for a label. If a function,
* this is called with `(geo.isolineFeature.valueEntry, index)`. This
* defaults to `valueEntry.value`.
* @property {number|function} [labelSpacing=200] Minimum distance between
* labels on an isoline in screen pixels. If a function, this is called
* with `(geo.isolineFeature.valueEntry, index)`.
* @property {number|function} [labelOffset=0] Offset for labels along an
* isoline relative to where they would be placed by default on a scale of
* [-0.5, 0.5]. +/- 1 would move the text to the next repeated occurrence
* of the label. If a function, this is called with
* `(geo.isolineFeature.valueEntry, index)`.
* @property {number|function} [labelViewport=10000] If the main position of a
* label would be further than this many pixels from the current viewport,
* don't create it. This prevents creating an excessive number of labels
* when zoomed in, but requires regenerating labels occasionally when
* panning. If <= 0, all labels are generated regardless of location.
* @property {boolean|function} [labelAutoUpdate=true] If truthy, when the map
* is panned (including zoom, rotation, etc.), periodically regenerate
* labels. This uses an internal function that has a threshold based on a
* fixed change in zoom, size, and other parameters. Set `labelAutoUpdate`
* to `false` and handle the {@link geo.event.pan} elsewhere.
*/
/**
* Isoline value entry.
*
* @typedef {object} geo.isolineFeature.valueEntry
* @property {number} value The value of the isoline.
* @property {number} level The level of the isoline.
* @property {number} [position] An index of the position of the isoline. For
* evenly spaced or autofit values, this is the value modulo the spacing.
* Otherwise, this is the index position within the list of values. This is
* computed when calculating isolines.
* @property {string} [label] The label to display on this value. This is
* computed from the `label` and `labelText` styles when calculating
* isolines.
*/
/**
* Computed isoline information.
*
* @typedef {object} geo.isolineFeature.isolineInfo
* @property {geo.isolineFeature.valueEntry[]} values The values used to
* produce the isolines.
* @property {geo.meshFeature.meshInfo} mesh The normalized mesh.
* @property {array[]} lines An array of arrays. Each entry is a list of
* vertices that also have a `value` property with the appropriate entry in
* `values`. If the line should show a label, it will also have a `label`
* property with the text of the label.
* @property {boolean} hasLabels `true` if there are any lines that have
* labels that need to be shown if there is enough resolution.
*/
/* This includes both the marching triangles and marching squares conditions.
* The triangle pattern has three values, where 0 is less below the threshold
* and 1 is above it. The square pattern has four values in the order
* ul-ur-ll-lr. For each line a pattern produces, the line is created with a
* low and high vertex from each of two edges. Additionally, the create line
* is such that the low value is outside of a clockwise winding.
*
* Performance note: Initially this table used string keys (e.g., '0001'), but
* the string lookup was vastly slower than an integer lookup.
*/
var patternLineTable = {
/* triangles with one high vertex */
17 /* 001 */: [{l0: 1, h0: 2, l1: 0, h1: 2}],
18 /* 010 */: [{l0: 0, h0: 1, l1: 2, h1: 1}],
20 /* 100 */: [{l0: 2, h0: 0, l1: 1, h1: 0}],
/* triangles with one low vertex */
22 /* 110 */: [{l0: 2, h0: 0, l1: 2, h1: 1}],
21 /* 101 */: [{l0: 1, h0: 2, l1: 1, h1: 0}],
19 /* 011 */: [{l0: 0, h0: 1, l1: 0, h1: 2}],
/* squares with one high vertex */
1 /* 0001 */: [{l0: 2, h0: 3, l1: 1, h1: 3}],
2 /* 0010 */: [{l0: 0, h0: 2, l1: 3, h1: 2}],
4 /* 0100 */: [{l0: 3, h0: 1, l1: 0, h1: 1}],
8 /* 1000 */: [{l0: 1, h0: 0, l1: 2, h1: 0}],
/* squares with one low vertex */
14 /* 1110 */: [{l0: 3, h0: 1, l1: 3, h1: 2}],
13 /* 1101 */: [{l0: 2, h0: 3, l1: 2, h1: 0}],
11 /* 1011 */: [{l0: 1, h0: 0, l1: 1, h1: 3}],
7 /* 0111 */: [{l0: 0, h0: 2, l1: 0, h1: 1}],
/* squares with two low vertices sharing a side */
3 /* 0011 */: [{l0: 0, h0: 2, l1: 1, h1: 3}],
10 /* 1010 */: [{l0: 1, h0: 0, l1: 3, h1: 2}],
12 /* 1100 */: [{l0: 3, h0: 1, l1: 2, h1: 0}],
5 /* 0101 */: [{l0: 2, h0: 3, l1: 0, h1: 1}],
/* squares with two low vertices on opposite corners. These could generate
* a different pair of lines each. */
6 /* 0110 */: [{l0: 0, h0: 2, l1: 0, h1: 1}, {l0: 3, h0: 1, l1: 3, h1: 2}],
9 /* 1001 */: [{l0: 1, h0: 0, l1: 1, h1: 3}, {l0: 2, h0: 3, l1: 2, h1: 0}]
};
/**
* Create a new instance of class isolineFeature.
*
* @class
* @alias geo.isolineFeature
* @extends geo.meshFeature
*
* @borrows geo.isolineFeature#mesh as geo.isolineFeature#contour
* @borrows geo.isolineFeature#mesh as geo.isolineFeature#isoline
*
* @param {geo.isolineFeature.spec} arg
* @returns {geo.isolineFeature}
*/
var isolineFeature = function (arg) {
'use strict';
if (!(this instanceof isolineFeature)) {
return new isolineFeature(arg);
}
var $ = require('jquery');
var transform = require('./transform');
var geo_event = require('./event');
var textFeature = require('./textFeature');
arg = arg || {};
meshFeature.call(this, arg);
/**
* @private
*/
var m_this = this,
m_isolines,
m_lastLabelPositions,
m_lineFeature,
m_labelLayer,
m_labelFeature,
s_draw = this.draw,
s_exit = this._exit,
s_init = this._init,
s_modified = this.modified,
s_update = this._update;
this.featureType = 'isoline';
this.contour = m_this.mesh;
this.isoline = m_this.mesh;
/**
* Create a set of isolines. This is a set of lines that could be used for a
* line feature and to inform a text feature.
*
* @returns {geo.isolineFeature.isolineInfo} An object with the isoline
* information.
*/
this._createIsolines = function () {
var valueFunc = m_this.style.get('value'),
usedFunc = m_this.style('used') !== undefined ?
m_this.style.get('used') :
function (d, i) { return util.isNonNullFinite(valueFunc(d, i)); },
values,
hasLabels = false,
lines = [];
var mesh = m_this._createMesh({
used: usedFunc,
value: valueFunc
});
values = m_this._getValueList(mesh);
if (!values.length) {
return {};
}
values.forEach(function (value) {
var valueLines = m_this._isolinesForValue(mesh, value);
if (valueLines.length) {
lines = lines.concat(valueLines);
hasLabels = hasLabels || !!value.label;
}
});
/* We may want to rdpSimplify the result to remove very small segments, but
* if we do, it must NOT change the winding direction. */
return {
lines: lines,
mesh: mesh,
values: values,
hasLabels: hasLabels
};
};
/**
* Generate an array of values for which isolines will be generated.
*
* @param {geo.meshFeature.meshInfo} mesh The normalized mesh.
* @returns {geo.isolineFeature.valueEntry[]} The values in ascending order.
*/
this._getValueList = function (mesh) {
var isoline = m_this.isoline,
values = isoline.get('values')(mesh),
spacing = isoline.get('spacing')(mesh),
count = isoline.get('count')(mesh),
autofit = isoline.get('autofit')(mesh),
levels = isoline.get('levels')(mesh),
minmax, delta, step, steppow, steplog10, fixedDigits, i;
if (!mesh.numVertices || !mesh.numElements) {
return [];
}
minmax = util.getMinMaxValues(mesh.value, isoline.get('min')(mesh), isoline.get('max')(mesh), true);
mesh.minValue = minmax.min;
mesh.maxValue = minmax.max;
delta = mesh.maxValue - mesh.minValue;
if (delta <= 0) {
return [];
}
/* Determine values for which we need to generate isolines. */
if (Array.isArray(values)) {
/* if the caller specified values, use them. Each can either be a number
* or an object with `value` and optionally `level`. If it doesn't have
* level, the position is just the index in the array. */
values = values.map(function (val, idx) {
return {
value: val.value !== undefined ? val.value : val,
position: idx,
level: val.level
};
});
/* Remove any values that are outside of the data range. */
values = values.filter(function (val) {
return val.value >= mesh.minValue && val.value <= mesh.maxValue;
});
} else if (!spacing && !autofit) {
/* If no values or spacing are specified and autofit is falsy, then
* use uniform spacing across the value range. The max and min won't
* produce contours (since they are exact values), so there range is
* divided into `count + 1` sections to get `count` visible lines. */
values = Array(count);
for (i = 0; i < count; i += 1) {
values[i] = {
value: mesh.minValue + delta * (i + 1) / (count + 1),
position: i + 1
};
}
} else {
if (!spacing) {
/* If no spacing is specified, then this has a count with autofit.
* Generate at least 2/3rds as many lines as the count, but it could be
* 5/2 of that when adjusted to "nice values" (so between 2/3 and 5/3
* of the specified count). */
step = delta / (count * 2 / 3);
steplog10 = Math.floor(Math.log10(step));
fixedDigits = Math.max(0, -steplog10);
steppow = Math.pow(10, steplog10);
step /= steppow; // will now be in range [1, 10)
step = step >= 5 ? 5 : step >= 2 ? 2 : 1; // now 1, 2, or 5
spacing = step * steppow;
}
/* Generate the values based on a spacing. The `position` is used for
* figuring out level further on and is based so that 0 will be the
* maximum level. */
values = [];
for (i = Math.ceil(mesh.minValue / spacing); i <= Math.floor(mesh.maxValue / spacing); i += 1) {
values.push({value: i * spacing, position: i, fixedDigits: fixedDigits});
}
}
/* Mark levels for each value. These are intended for styling. All values
* will have a `value` and `position` attribute at this point. */
if (levels.length) {
values.forEach(function (val, idx) {
if (val.level === undefined) {
val.level = 0;
for (var i = 0, basis = levels[0]; i < levels.length && !(val.position % basis); i += 1, basis *= levels[i]) {
val.level = i + 1;
}
}
if (isoline.get('label')(val, val.position)) {
var label = isoline.get('labelText')(val, val.position);
if (label === undefined) {
if (val.fixedDigits !== undefined) {
label = '' + parseFloat(val.value.toFixed(val.fixedDigits));
} else {
label = '' + val.value;
}
}
if (label) {
val.label = label;
}
}
});
}
return values;
};
/**
* Add a new segment to a list of chains. Each chain is a list of vertices,
* each of which is an array of two values with the low/high mesh vertices
* for that chain vertex. There are then three possibilities: (a) The
* segment forms a new chain that doesn't attach to an existing chain. (b)
* One endpoint of the segment matches the endpoint of an existing chain, and
* it gets added to that chain. (c) Both endpoints of the segment match
* endpoints of two different chains, and those two chains are combined via
* the segment. A chain may represent a loop, in which case its two
* endpoints will match. This function does not join the loop.
*
* @param {array} chains An array of existing chains.
* @param {array} first One endpoint of the new segment. This is an array of
* two numbers defining the mesh vertices used for the endpoint.
* @param {array} last The second endpoint of the new segment.
* @returns {array} The modified chains array.
*/
this._addSegment = function (chains, first, last) {
var chain = [first, last],
idx = chains.length,
i, iter, check, checkFirst, checkLast, combine;
/* Add the segment as a new chain by itself. */
chains.push(chain);
for (iter = 0; iter < 2; iter += 1) {
/* Check if the new chain can attach to an existing chain */
for (i = idx - 1; i >= 0; i -= 1) {
check = chains[i];
checkFirst = check[0];
checkLast = check[check.length - 1];
/* The segment can be inserted at the start of this chain */
if (last[0] === checkFirst[0] && last[1] === checkFirst[1]) {
combine = chain.concat(check.slice(1));
/* The segment can be inserted at the end of this chain */
} else if (first[0] === checkLast[0] && first[1] === checkLast[1]) {
combine = check.concat(chain.slice(1));
/* These two conditions should never be required, as we generate
* segments with a consistent winding direction.
} else if (first[0] === checkFirst[0] && first[1] === checkFirst[1]) {
combine = chain.slice(1).reverse().concat(check);
} else if (last[0] === checkLast[0] && last[1] === checkLast[1]) {
combine = check.concat(chain.slice(0, chain.length - 1).reverse());
*/
/* The segment doesn't match this chain, so keep scanning chains */
} else {
continue;
}
/* The segment matched and `combine` contains the chain it has been
* merged with. */
chains.splice(idx, 1);
chains[i] = chain = combine;
idx = i;
first = chain[0];
last = chain[chain.length - 1];
break;
}
/* If we didn't combine the new chain to any existing chains, then don't
* check if the other end also joins an existing chain. */
if (i < 0) {
break;
}
}
return chains;
};
/**
* Given a vertex of the form [low vertex index, high vertex index], compute
* the coordinates of the vertex.
*
* @param {geo.meshFeature.meshInfo} mesh The normalized mesh.
* @param {geo.isolineFeature.valueEntry} value The value for which to
* generate the vertex.
* @param {number[]} vertex The low vertex index and high vertex index.
* @returns {geo.geoPosition} The calculated coordinate.
*/
this._chainVertex = function (mesh, value, vertex) {
var v0 = vertex[0], v1 = vertex[1],
v03 = v0 * 3, v13 = v1 * 3,
f = (value.value - mesh.value[v0]) / (mesh.value[v1] - mesh.value[v0]),
g = 1 - f;
return {
x: mesh.pos[v03] * g + mesh.pos[v13] * f,
y: mesh.pos[v03 + 1] * g + mesh.pos[v13 + 1] * f,
z: mesh.pos[v03 + 2] * g + mesh.pos[v13 + 2] * f
};
};
/**
* Generate the lines for associated with a particular value. This performs
* either marching triangles or marching squares.
*
* @param {geo.meshFeature.meshInfo} mesh The normalized mesh.
* @param {geo.isolineFeature.valueEntry} value The value for which to
* generate the isolines.
* @returns {geo.isolineFeature.line[]} An array of lines.
*/
this._isolinesForValue = function (mesh, value) {
var val = value.value,
lowhigh = Array(mesh.value.length),
chains = [],
i, v, pattern, lines;
/* Determine if each vertex is above or below the value. It is faster to
* use a for loop than map since it avoids function calls. */
for (i = lowhigh.length - 1; i >= 0; i -= 1) {
lowhigh[i] = mesh.value[i] <= val ? 0 : 1;
}
var vpe = mesh.verticesPerElement,
square = mesh.shape === 'square',
elem = mesh.elements,
elemLen = elem.length;
for (v = 0; v < elemLen; v += vpe) {
if (square) {
pattern = lowhigh[elem[v]] * 8 + lowhigh[elem[v + 1]] * 4 +
lowhigh[elem[v + 2]] * 2 + lowhigh[elem[v + 3]];
if (pattern === 0 || pattern === 15) {
continue;
}
} else {
pattern = 16 + lowhigh[elem[v]] * 4 + lowhigh[elem[v + 1]] * 2 +
lowhigh[elem[v + 2]];
if (pattern === 16 || pattern === 23) {
continue;
}
}
patternLineTable[pattern].forEach(function (lineEntry) {
chains = m_this._addSegment(
chains,
[elem[v + lineEntry.l0], elem[v + lineEntry.h0]],
[elem[v + lineEntry.l1], elem[v + lineEntry.h1]]
);
});
}
/* convert chains to lines */
lines = chains.map(function (chain) {
var line = [];
chain.forEach(function (vertex) {
var v = m_this._chainVertex(mesh, value, vertex);
if (!line.length || v.x !== line[line.length - 1].x ||
v.y !== line[line.length - 1].y) {
line.push(v);
}
});
line.closed = (line[0].x === line[line.length - 1].x &&
line[0].y === line[line.length - 1].y);
/* Add value, level, position, and label information to the line. */
line.value = value.value;
line.level = value.level;
line.position = value.position;
line.label = value.label;
return line;
}).filter(function (line) { return line.length > 1; });
return lines;
};
/**
* Update the timestamp to the next global timestamp value. Mark
* sub-features as modified, too.
*
* @returns {object} The results of the superclass modified function.
*/
this.modified = function () {
var result = s_modified();
if (m_lineFeature) {
m_lineFeature.modified();
}
if (m_labelFeature) {
m_labelFeature.modified();
}
return result;
};
/**
* Compute the positions for labels on each line. This can be called to
* recompute label positions without needign to recompute isolines, for
* instance when the zoom level changes. Label positions are computed in the
* map gcs coordinates, not interface gcs coordinates, since the interface
* gcs may not be linear with the display space.
*
* @returns {this}
*/
this.labelPositions = function () {
if (m_this.dataTime().timestamp() >= m_this.buildTime().timestamp()) {
m_this._build();
}
m_lastLabelPositions = null;
if (!m_labelFeature) {
return m_this;
}
if (!m_isolines || !m_isolines.hasLabels || !m_isolines.lines || !m_isolines.lines.length) {
m_labelFeature.data([]);
return m_this;
}
var isoline = m_this.isoline,
spacingFunc = isoline.get('labelSpacing'),
offsetFunc = isoline.get('labelOffset'),
labelViewport = isoline.get('labelViewport')(m_isolines.mesh),
gcs = m_this.gcs(),
map = m_this.layer().map(),
mapgcs = map.gcs(),
mapRotation = map.rotation(),
mapSize = map.size(),
labelData = [],
maxSpacing = 0;
m_isolines.lines.forEach(function (line, idx) {
if (!line.label) {
return;
}
var spacing = spacingFunc(line.value, line.value.position),
offset = offsetFunc(line.value, line.value.position) || 0,
dispCoor = map.gcsToDisplay(line, gcs),
totalDistance = 0,
dist, count, localSpacing, next, lineDistance, i, i2, f, g, pos,
mapCoor;
if (spacing <= 0 || isNaN(spacing)) {
return;
}
maxSpacing = Math.max(spacing, maxSpacing);
/* make offset in the range of [0, 1) with the default at 0.5 */
offset = (offset + 0.5) - Math.floor(offset + 0.5);
dist = dispCoor.map(function (pt1, coorIdx) {
if (!line.closed && coorIdx + 1 === dispCoor.length) {
return 0;
}
var val = Math.sqrt(util.distance2dSquared(pt1, dispCoor[coorIdx + 1 < dispCoor.length ? coorIdx + 1 : 0]));
totalDistance += val;
return val;
});
count = Math.floor(totalDistance / spacing);
if (!count) {
return;
}
/* If we have any labels, compute map coordinates of the line and use
* those for interpolating label positions */
mapCoor = transform.transformCoordinates(gcs, mapgcs, line);
localSpacing = totalDistance / count;
next = localSpacing * offset;
lineDistance = 0;
for (i = 0; i < dispCoor.length; i += 1) {
while (lineDistance + dist[i] >= next) {
i2 = i + 1 === dispCoor.length ? 0 : i + 1;
f = (next - lineDistance) / dist[i];
g = 1 - f;
next += localSpacing;
if (labelViewport > 0) {
pos = {
x: dispCoor[i].x * g + dispCoor[i2].x * f,
y: dispCoor[i].y * g + dispCoor[i2].y * f
};
if (pos.x < -labelViewport || pos.x > mapSize.width + labelViewport ||
pos.y < -labelViewport || pos.y > mapSize.height + labelViewport) {
continue;
}
}
labelData.push({
x: mapCoor[i].x * g + mapCoor[i2].x * f,
y: mapCoor[i].y * g + mapCoor[i2].y * f,
z: mapCoor[i].z * g + mapCoor[i2].z * f,
line: line,
rotation: Math.atan2(dispCoor[i].y - dispCoor[i2].y, dispCoor[i].x - dispCoor[i2].x) - mapRotation
});
}
lineDistance += dist[i];
}
});
m_labelFeature.gcs(mapgcs);
m_labelFeature.data(labelData);
m_labelFeature.style('renderThreshold', maxSpacing * 2);
m_lastLabelPositions = {
zoom: map.zoom(),
center: map.center(),
rotation: mapRotation,
size: mapSize,
labelViewport: labelViewport,
maxSpacing: maxSpacing,
labelAutoUpdate: isoline.get('labelAutoUpdate')(m_isolines.mesh)
};
return m_this;
};
/**
* Get the last map position that was used for generating labels.
*
* @returns {object} An object with the map `zoom` and `center` and the
* `labelViewport` used in generating labels. The object may have no
* properties if there are no labels.
*/
this.lastLabelPositions = function () {
return $.extend({}, m_lastLabelPositions);
};
/**
* On a pan event, if labels exist and are set to autoupdate, recalculate
* their positions and redraw them as needed. Labels are redrawn if the
* zoom level changes by at least 2 levels, or the map's center is moved
* enough that there is a chance that the viewport is nearing the extent of
* the generated labels. The viewport calculation is conservative, as the
* map could be rotated, changed size, or have other modifications.
*
* @returns {this}
*/
this._updateLabelPositions = function () {
var last = m_lastLabelPositions;
if (!last || !last.labelAutoUpdate) {
return m_this;
}
var map = m_this.layer().map(),
zoom = map.zoom(),
mapSize = map.size(),
update = !!(Math.abs(zoom - last.zoom) >= 2);
if (!update && last.labelViewport > 0) {
/* Distance in scaled pixels between the map's current center and the
* center when the labels were computed. */
var lastDelta = Math.sqrt(util.distance2dSquared(
map.gcsToDisplay(map.center()), map.gcsToDisplay(last.center))) *
Math.pow(2, last.zoom - zoom);
/* Half the viewport, less twice the maxSpacing, less any expansion of
* the map. */
var threshold = last.labelViewport / 2 - last.maxSpacing * 2 - Math.max(
mapSize.width - last.size.width, mapSize.height - last.size.height, 0);
update = update || (lastDelta >= threshold);
}
if (update) {
m_this.labelPositions().draw();
}
return m_this;
};
/**
* Build. Generate the isolines. Create a line feature if necessary and
* update it.
*
* @returns {this}
*/
this._build = function () {
m_isolines = m_this._createIsolines();
if (m_isolines && m_isolines.lines && m_isolines.lines.length) {
if (!m_lineFeature) {
m_lineFeature = m_this.layer().createFeature('line', {
selectionAPI: false,
gcs: m_this.gcs(),
visible: m_this.visible(undefined, true),
style: {
closed: function (d) { return d.closed; }
}
});
m_this.dependentFeatures([m_lineFeature]);
}
var style = m_this.style();
m_lineFeature.data(m_isolines.lines).style({
antialiasing: style.antialiasing,
lineCap: style.lineCap,
lineJoin: style.lineJoin,
miterLimit: style.miterLimit,
strokeWidth: style.strokeWidth,
strokeStyle: style.strokeStyle,
strokeColor: style.strokeColor,
strokeOffset: style.strokeOffset,
strokeOpacity: style.strokeOpacity
});
if (m_isolines.hasLabels) {
if (!m_labelFeature) {
if (!(registry.registries.features[m_this.layer().rendererName()] || {}).text) {
var renderer = registry.rendererForFeatures(['text']);
m_labelLayer = registry.createLayer('feature', m_this.layer().map(), {renderer: renderer});
m_this.layer().addChild(m_labelLayer);
m_this.layer().node().append(m_labelLayer.node());
}
m_labelFeature = (m_labelLayer || m_this.layer()).createFeature('text', {
selectionAPI: false,
gcs: m_this.gcs(),
visible: m_this.visible(undefined, true),
style: {
text: function (d) { return d.line.label; }
}
}).geoOn(geo_event.pan, m_this._updateLabelPositions);
}
textFeature.usedStyles.forEach(function (styleName) {
if (styleName !== 'visible') {
m_labelFeature.style(styleName, style[styleName]);
}
});
m_this.dependentFeatures([m_lineFeature, m_labelFeature]);
}
} else if (m_lineFeature) {
m_lineFeature.data([]);
}
m_this.buildTime().modified();
/* Update label positions after setting the build time. The labelPositions
* method will build if necessary, and this prevents it from looping. */
m_this.labelPositions();
return m_this;
};
/**
* Update. Rebuild if necessary.
*
* @returns {this}
*/
this._update = function () {
s_update.call(m_this);
if (m_this.dataTime().timestamp() >= m_this.buildTime().timestamp() ||
m_this.updateTime().timestamp() <= m_this.timestamp()) {
m_this._build();
}
m_this.updateTime().modified();
return m_this;
};
/**
* Redraw the object.
*
* @returns {object} The results of the superclass draw function.
*/
this.draw = function () {
var result = s_draw();
if (m_lineFeature) {
m_lineFeature.draw();
}
if (m_labelFeature) {
m_labelFeature.draw();
}
return result;
};
/**
* Destroy.
*/
this._exit = function () {
if (m_labelFeature) {
if (m_labelLayer || m_this.layer()) {
(m_labelLayer || m_this.layer()).deleteFeature(m_labelFeature);
}
if (m_labelLayer && m_this.layer()) {
m_this.layer().removeChild(m_labelLayer);
}
}
if (m_lineFeature && m_this.layer()) {
m_this.layer().deleteFeature(m_lineFeature);
}
m_labelFeature = null;
m_labelLayer = null;
m_lineFeature = null;
m_this.dependentFeatures([]);
s_exit();
};
/**
* Initialize.
*
* @param {geo.isolineFeature.spec} arg The isoline feature specification.
*/
this._init = function (arg) {
arg = arg || {};
s_init.call(m_this, arg);
var defaultStyle = $.extend(
{},
{
opacity: 1.0,
value: function (d, i) {
return m_this.position()(d, i).z;
},
rotateWithMap: true,
rotation: isolineFeature.rotationFunction(),
strokeWidth: function (v, vi, d, di) { return d.level + 0.5; },
strokeColor: {r: 0, g: 0, b: 0},
textStrokeColor: {r: 1, g: 1, b: 1, a: 0.75},
textStrokeWidth: 2,
fontSize: '12px'
},
arg.style === undefined ? {} : arg.style
);
m_this.style(defaultStyle);
m_this.isoline($.extend({}, {
count: 15,
autofit: true,
levels: [5, 5],
label: function (value) {
return value.level >= 1;
},
labelSpacing: 200,
labelViewport: 10000,
labelAutoUpdate: true
}, arg.mesh || {}, arg.contour || {}, arg.isoline || {}));
if (arg.mesh || arg.contour || arg.isoline) {
m_this.dataTime().modified();
}
};
return this;
};
/**
* Return a function that will rotate text labels in a specified orientation.
* The results of this are intended to be used as the value of the `rotation`
* style.
*
* @param {string} [mode='higher'] The rotation mode. `higher` orients the top
* of the text to high values. `lower` orients the top of the text to lower
* values. `map` orients the top of the text so it is aligned to the isoline
* and biased toward the top of the map. `screen` orients the top of the
* text so it is aligned to the isoline and biased toward the top of the
* display screen.
* @param {geo.map} [map] The parent map. Required for `screen` mode.
* @returns {function} A function for the rotation style.
*/
isolineFeature.rotationFunction = function (mode, map) {
var functionList = {
higher: function (d) {
return d.rotation;
},
lower: function (d) {
return d.rotation + Math.PI;
},
map: function (d) {
var r = d.rotation,
rt = util.wrapAngle(r, true);
if (rt > Math.PI / 2 || rt < -Math.PI / 2) {
r += Math.PI;
}
return r;
},
screen: function (d) {
var r = d.rotation,
rt = util.wrapAngle(r + map.rotation(), true);
if (rt > Math.PI / 2 || rt < -Math.PI / 2) {
r += Math.PI;
}
return r;
}
};
return functionList[mode] || functionList.higher;
};
inherit(isolineFeature, meshFeature);
module.exports = isolineFeature;