add getScaleArray() method test two dimension scale icons test two dimension scale text add example of icon and label scaling use smaller icon and larger interval test two dimensional scale icons test two dimensional scale icons
606 lines
17 KiB
JavaScript
606 lines
17 KiB
JavaScript
/**
|
|
* @module ol/render/canvas/TextBuilder
|
|
*/
|
|
import CanvasBuilder from './Builder.js';
|
|
import CanvasInstruction from './Instruction.js';
|
|
import GeometryType from '../../geom/GeometryType.js';
|
|
import TextPlacement from '../../style/TextPlacement.js';
|
|
import {asColorLike} from '../../colorlike.js';
|
|
import {
|
|
defaultFillStyle,
|
|
defaultFont,
|
|
defaultLineCap,
|
|
defaultLineDash,
|
|
defaultLineDashOffset,
|
|
defaultLineJoin,
|
|
defaultLineWidth,
|
|
defaultMiterLimit,
|
|
defaultPadding,
|
|
defaultStrokeStyle,
|
|
defaultTextAlign,
|
|
defaultTextBaseline,
|
|
registerFont,
|
|
} from '../canvas.js';
|
|
import {getUid} from '../../util.js';
|
|
import {intersects} from '../../extent.js';
|
|
import {matchingChunk} from '../../geom/flat/straightchunk.js';
|
|
/**
|
|
* @const
|
|
* @enum {number}
|
|
*/
|
|
export const TEXT_ALIGN = {
|
|
'left': 0,
|
|
'end': 0,
|
|
'center': 0.5,
|
|
'right': 1,
|
|
'start': 1,
|
|
'top': 0,
|
|
'middle': 0.5,
|
|
'hanging': 0.2,
|
|
'alphabetic': 0.8,
|
|
'ideographic': 0.8,
|
|
'bottom': 1,
|
|
};
|
|
|
|
class CanvasTextBuilder extends CanvasBuilder {
|
|
/**
|
|
* @param {number} tolerance Tolerance.
|
|
* @param {import("../../extent.js").Extent} maxExtent Maximum extent.
|
|
* @param {number} resolution Resolution.
|
|
* @param {number} pixelRatio Pixel ratio.
|
|
*/
|
|
constructor(tolerance, maxExtent, resolution, pixelRatio) {
|
|
super(tolerance, maxExtent, resolution, pixelRatio);
|
|
|
|
/**
|
|
* @private
|
|
* @type {import("../canvas.js").DeclutterGroups}
|
|
*/
|
|
this.declutterGroups_;
|
|
|
|
/**
|
|
* @private
|
|
* @type {Array<HTMLCanvasElement>}
|
|
*/
|
|
this.labels_ = null;
|
|
|
|
/**
|
|
* @private
|
|
* @type {string}
|
|
*/
|
|
this.text_ = '';
|
|
|
|
/**
|
|
* @private
|
|
* @type {number}
|
|
*/
|
|
this.textOffsetX_ = 0;
|
|
|
|
/**
|
|
* @private
|
|
* @type {number}
|
|
*/
|
|
this.textOffsetY_ = 0;
|
|
|
|
/**
|
|
* @private
|
|
* @type {boolean|undefined}
|
|
*/
|
|
this.textRotateWithView_ = undefined;
|
|
|
|
/**
|
|
* @private
|
|
* @type {number}
|
|
*/
|
|
this.textRotation_ = 0;
|
|
|
|
/**
|
|
* @private
|
|
* @type {?import("../canvas.js").FillState}
|
|
*/
|
|
this.textFillState_ = null;
|
|
|
|
/**
|
|
* @type {!Object<string, import("../canvas.js").FillState>}
|
|
*/
|
|
this.fillStates = {};
|
|
|
|
/**
|
|
* @private
|
|
* @type {?import("../canvas.js").StrokeState}
|
|
*/
|
|
this.textStrokeState_ = null;
|
|
|
|
/**
|
|
* @type {!Object<string, import("../canvas.js").StrokeState>}
|
|
*/
|
|
this.strokeStates = {};
|
|
|
|
/**
|
|
* @private
|
|
* @type {import("../canvas.js").TextState}
|
|
*/
|
|
this.textState_ = /** @type {import("../canvas.js").TextState} */ ({});
|
|
|
|
/**
|
|
* @type {!Object<string, import("../canvas.js").TextState>}
|
|
*/
|
|
this.textStates = {};
|
|
|
|
/**
|
|
* @private
|
|
* @type {string}
|
|
*/
|
|
this.textKey_ = '';
|
|
|
|
/**
|
|
* @private
|
|
* @type {string}
|
|
*/
|
|
this.fillKey_ = '';
|
|
|
|
/**
|
|
* @private
|
|
* @type {string}
|
|
*/
|
|
this.strokeKey_ = '';
|
|
}
|
|
|
|
/**
|
|
* @return {import("./Builder.js").SerializableInstructions} the serializable instructions.
|
|
*/
|
|
finish() {
|
|
const instructions = super.finish();
|
|
instructions.textStates = this.textStates;
|
|
instructions.fillStates = this.fillStates;
|
|
instructions.strokeStates = this.strokeStates;
|
|
return instructions;
|
|
}
|
|
|
|
/**
|
|
* @param {import("../../geom/SimpleGeometry.js").default|import("../Feature.js").default} geometry Geometry.
|
|
* @param {import("../../Feature.js").FeatureLike} feature Feature.
|
|
*/
|
|
drawText(geometry, feature) {
|
|
const fillState = this.textFillState_;
|
|
const strokeState = this.textStrokeState_;
|
|
const textState = this.textState_;
|
|
if (this.text_ === '' || !textState || (!fillState && !strokeState)) {
|
|
return;
|
|
}
|
|
|
|
let begin = this.coordinates.length;
|
|
|
|
const geometryType = geometry.getType();
|
|
let flatCoordinates = null;
|
|
let end = 2;
|
|
let stride = 2;
|
|
let i, ii;
|
|
|
|
if (textState.placement === TextPlacement.LINE) {
|
|
if (!intersects(this.getBufferedMaxExtent(), geometry.getExtent())) {
|
|
return;
|
|
}
|
|
let ends;
|
|
flatCoordinates = geometry.getFlatCoordinates();
|
|
stride = geometry.getStride();
|
|
if (geometryType == GeometryType.LINE_STRING) {
|
|
ends = [flatCoordinates.length];
|
|
} else if (geometryType == GeometryType.MULTI_LINE_STRING) {
|
|
ends = /** @type {import("../../geom/MultiLineString.js").default} */ (geometry).getEnds();
|
|
} else if (geometryType == GeometryType.POLYGON) {
|
|
ends = /** @type {import("../../geom/Polygon.js").default} */ (geometry)
|
|
.getEnds()
|
|
.slice(0, 1);
|
|
} else if (geometryType == GeometryType.MULTI_POLYGON) {
|
|
const endss = /** @type {import("../../geom/MultiPolygon.js").default} */ (geometry).getEndss();
|
|
ends = [];
|
|
for (i = 0, ii = endss.length; i < ii; ++i) {
|
|
ends.push(endss[i][0]);
|
|
}
|
|
}
|
|
this.beginGeometry(geometry, feature);
|
|
const textAlign = textState.textAlign;
|
|
let flatOffset = 0;
|
|
let flatEnd;
|
|
for (let o = 0, oo = ends.length; o < oo; ++o) {
|
|
if (textAlign == undefined) {
|
|
const range = matchingChunk(
|
|
textState.maxAngle,
|
|
flatCoordinates,
|
|
flatOffset,
|
|
ends[o],
|
|
stride
|
|
);
|
|
flatOffset = range[0];
|
|
flatEnd = range[1];
|
|
} else {
|
|
flatEnd = ends[o];
|
|
}
|
|
for (i = flatOffset; i < flatEnd; i += stride) {
|
|
this.coordinates.push(flatCoordinates[i], flatCoordinates[i + 1]);
|
|
}
|
|
end = this.coordinates.length;
|
|
flatOffset = ends[o];
|
|
const declutterGroup = this.declutterGroups_
|
|
? o === 0
|
|
? this.declutterGroups_[0]
|
|
: [].concat(this.declutterGroups_[0])
|
|
: null;
|
|
this.drawChars_(begin, end, declutterGroup);
|
|
begin = end;
|
|
}
|
|
this.endGeometry(feature);
|
|
} else {
|
|
let geometryWidths = null;
|
|
if (!textState.overflow) {
|
|
geometryWidths = [];
|
|
}
|
|
switch (geometryType) {
|
|
case GeometryType.POINT:
|
|
case GeometryType.MULTI_POINT:
|
|
flatCoordinates = /** @type {import("../../geom/MultiPoint.js").default} */ (geometry).getFlatCoordinates();
|
|
end = flatCoordinates.length;
|
|
break;
|
|
case GeometryType.LINE_STRING:
|
|
flatCoordinates = /** @type {import("../../geom/LineString.js").default} */ (geometry).getFlatMidpoint();
|
|
break;
|
|
case GeometryType.CIRCLE:
|
|
flatCoordinates = /** @type {import("../../geom/Circle.js").default} */ (geometry).getCenter();
|
|
break;
|
|
case GeometryType.MULTI_LINE_STRING:
|
|
flatCoordinates = /** @type {import("../../geom/MultiLineString.js").default} */ (geometry).getFlatMidpoints();
|
|
end = flatCoordinates.length;
|
|
break;
|
|
case GeometryType.POLYGON:
|
|
flatCoordinates = /** @type {import("../../geom/Polygon.js").default} */ (geometry).getFlatInteriorPoint();
|
|
if (!textState.overflow) {
|
|
geometryWidths.push(flatCoordinates[2] / this.resolution);
|
|
}
|
|
stride = 3;
|
|
break;
|
|
case GeometryType.MULTI_POLYGON:
|
|
const interiorPoints = /** @type {import("../../geom/MultiPolygon.js").default} */ (geometry).getFlatInteriorPoints();
|
|
flatCoordinates = [];
|
|
for (i = 0, ii = interiorPoints.length; i < ii; i += 3) {
|
|
if (!textState.overflow) {
|
|
geometryWidths.push(interiorPoints[i + 2] / this.resolution);
|
|
}
|
|
flatCoordinates.push(interiorPoints[i], interiorPoints[i + 1]);
|
|
}
|
|
end = flatCoordinates.length;
|
|
if (end == 0) {
|
|
return;
|
|
}
|
|
break;
|
|
default:
|
|
}
|
|
end = this.appendFlatCoordinates(
|
|
flatCoordinates,
|
|
0,
|
|
end,
|
|
stride,
|
|
false,
|
|
false
|
|
);
|
|
|
|
this.saveTextStates_();
|
|
|
|
if (textState.backgroundFill || textState.backgroundStroke) {
|
|
this.setFillStrokeStyle(
|
|
textState.backgroundFill,
|
|
textState.backgroundStroke
|
|
);
|
|
if (textState.backgroundFill) {
|
|
this.updateFillStyle(this.state, this.createFill);
|
|
this.hitDetectionInstructions.push(this.createFill(this.state));
|
|
}
|
|
if (textState.backgroundStroke) {
|
|
this.updateStrokeStyle(this.state, this.applyStroke);
|
|
this.hitDetectionInstructions.push(this.createStroke(this.state));
|
|
}
|
|
}
|
|
|
|
this.beginGeometry(geometry, feature);
|
|
|
|
// adjust padding for negative scale
|
|
let padding = textState.padding;
|
|
if (
|
|
padding != defaultPadding &&
|
|
(textState.scale[0] < 0 || textState.scale[1] < 0)
|
|
) {
|
|
let p0 = textState.padding[0];
|
|
let p1 = textState.padding[1];
|
|
let p2 = textState.padding[2];
|
|
let p3 = textState.padding[3];
|
|
if (textState.scale[0] < 0) {
|
|
p1 = -p1;
|
|
p3 = -p3;
|
|
}
|
|
if (textState.scale[1] < 0) {
|
|
p0 = -p0;
|
|
p2 = -p2;
|
|
}
|
|
padding = [p0, p1, p2, p3];
|
|
}
|
|
|
|
// The image is unknown at this stage so we pass null; it will be computed at render time.
|
|
// For clarity, we pass NaN for offsetX, offsetY, width and height, which will be computed at
|
|
// render time.
|
|
const pixelRatio = this.pixelRatio;
|
|
this.instructions.push([
|
|
CanvasInstruction.DRAW_IMAGE,
|
|
begin,
|
|
end,
|
|
null,
|
|
NaN,
|
|
NaN,
|
|
this.declutterGroups_,
|
|
NaN,
|
|
1,
|
|
0,
|
|
0,
|
|
this.textRotateWithView_,
|
|
this.textRotation_,
|
|
[1, 1],
|
|
NaN,
|
|
padding == defaultPadding
|
|
? defaultPadding
|
|
: padding.map(function (p) {
|
|
return p * pixelRatio;
|
|
}),
|
|
!!textState.backgroundFill,
|
|
!!textState.backgroundStroke,
|
|
this.text_,
|
|
this.textKey_,
|
|
this.strokeKey_,
|
|
this.fillKey_,
|
|
this.textOffsetX_,
|
|
this.textOffsetY_,
|
|
geometryWidths,
|
|
]);
|
|
const scale = 1 / pixelRatio;
|
|
this.hitDetectionInstructions.push([
|
|
CanvasInstruction.DRAW_IMAGE,
|
|
begin,
|
|
end,
|
|
null,
|
|
NaN,
|
|
NaN,
|
|
this.declutterGroups_,
|
|
NaN,
|
|
1,
|
|
0,
|
|
0,
|
|
this.textRotateWithView_,
|
|
this.textRotation_,
|
|
[scale, scale],
|
|
NaN,
|
|
padding,
|
|
!!textState.backgroundFill,
|
|
!!textState.backgroundStroke,
|
|
this.text_,
|
|
this.textKey_,
|
|
this.strokeKey_,
|
|
this.fillKey_,
|
|
this.textOffsetX_,
|
|
this.textOffsetY_,
|
|
geometryWidths,
|
|
]);
|
|
|
|
this.endGeometry(feature);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
saveTextStates_() {
|
|
const strokeState = this.textStrokeState_;
|
|
const textState = this.textState_;
|
|
const fillState = this.textFillState_;
|
|
|
|
const strokeKey = this.strokeKey_;
|
|
if (strokeState) {
|
|
if (!(strokeKey in this.strokeStates)) {
|
|
this.strokeStates[strokeKey] = {
|
|
strokeStyle: strokeState.strokeStyle,
|
|
lineCap: strokeState.lineCap,
|
|
lineDashOffset: strokeState.lineDashOffset,
|
|
lineWidth: strokeState.lineWidth,
|
|
lineJoin: strokeState.lineJoin,
|
|
miterLimit: strokeState.miterLimit,
|
|
lineDash: strokeState.lineDash,
|
|
};
|
|
}
|
|
}
|
|
const textKey = this.textKey_;
|
|
if (!(textKey in this.textStates)) {
|
|
this.textStates[textKey] = {
|
|
font: textState.font,
|
|
textAlign: textState.textAlign || defaultTextAlign,
|
|
textBaseline: textState.textBaseline || defaultTextBaseline,
|
|
scale: textState.scale,
|
|
};
|
|
}
|
|
const fillKey = this.fillKey_;
|
|
if (fillState) {
|
|
if (!(fillKey in this.fillStates)) {
|
|
this.fillStates[fillKey] = {
|
|
fillStyle: fillState.fillStyle,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {number} begin Begin.
|
|
* @param {number} end End.
|
|
* @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group.
|
|
*/
|
|
drawChars_(begin, end, declutterGroup) {
|
|
const strokeState = this.textStrokeState_;
|
|
const textState = this.textState_;
|
|
|
|
const strokeKey = this.strokeKey_;
|
|
const textKey = this.textKey_;
|
|
const fillKey = this.fillKey_;
|
|
this.saveTextStates_();
|
|
|
|
const pixelRatio = this.pixelRatio;
|
|
const baseline = TEXT_ALIGN[textState.textBaseline];
|
|
|
|
const offsetY = this.textOffsetY_ * pixelRatio;
|
|
const text = this.text_;
|
|
const strokeWidth = strokeState
|
|
? (strokeState.lineWidth * Math.abs(textState.scale[0])) / 2
|
|
: 0;
|
|
|
|
this.instructions.push([
|
|
CanvasInstruction.DRAW_CHARS,
|
|
begin,
|
|
end,
|
|
baseline,
|
|
declutterGroup,
|
|
textState.overflow,
|
|
fillKey,
|
|
textState.maxAngle,
|
|
pixelRatio,
|
|
offsetY,
|
|
strokeKey,
|
|
strokeWidth * pixelRatio,
|
|
text,
|
|
textKey,
|
|
1,
|
|
]);
|
|
this.hitDetectionInstructions.push([
|
|
CanvasInstruction.DRAW_CHARS,
|
|
begin,
|
|
end,
|
|
baseline,
|
|
declutterGroup,
|
|
textState.overflow,
|
|
fillKey,
|
|
textState.maxAngle,
|
|
1,
|
|
offsetY,
|
|
strokeKey,
|
|
strokeWidth,
|
|
text,
|
|
textKey,
|
|
1 / pixelRatio,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param {import("../../style/Text.js").default} textStyle Text style.
|
|
* @param {import("../canvas.js").DeclutterGroups} declutterGroups Declutter.
|
|
*/
|
|
setTextStyle(textStyle, declutterGroups) {
|
|
let textState, fillState, strokeState;
|
|
if (!textStyle) {
|
|
this.text_ = '';
|
|
} else {
|
|
this.declutterGroups_ = declutterGroups;
|
|
|
|
const textFillStyle = textStyle.getFill();
|
|
if (!textFillStyle) {
|
|
fillState = null;
|
|
this.textFillState_ = fillState;
|
|
} else {
|
|
fillState = this.textFillState_;
|
|
if (!fillState) {
|
|
fillState = /** @type {import("../canvas.js").FillState} */ ({});
|
|
this.textFillState_ = fillState;
|
|
}
|
|
fillState.fillStyle = asColorLike(
|
|
textFillStyle.getColor() || defaultFillStyle
|
|
);
|
|
}
|
|
|
|
const textStrokeStyle = textStyle.getStroke();
|
|
if (!textStrokeStyle) {
|
|
strokeState = null;
|
|
this.textStrokeState_ = strokeState;
|
|
} else {
|
|
strokeState = this.textStrokeState_;
|
|
if (!strokeState) {
|
|
strokeState = /** @type {import("../canvas.js").StrokeState} */ ({});
|
|
this.textStrokeState_ = strokeState;
|
|
}
|
|
const lineDash = textStrokeStyle.getLineDash();
|
|
const lineDashOffset = textStrokeStyle.getLineDashOffset();
|
|
const lineWidth = textStrokeStyle.getWidth();
|
|
const miterLimit = textStrokeStyle.getMiterLimit();
|
|
strokeState.lineCap = textStrokeStyle.getLineCap() || defaultLineCap;
|
|
strokeState.lineDash = lineDash ? lineDash.slice() : defaultLineDash;
|
|
strokeState.lineDashOffset =
|
|
lineDashOffset === undefined ? defaultLineDashOffset : lineDashOffset;
|
|
strokeState.lineJoin = textStrokeStyle.getLineJoin() || defaultLineJoin;
|
|
strokeState.lineWidth =
|
|
lineWidth === undefined ? defaultLineWidth : lineWidth;
|
|
strokeState.miterLimit =
|
|
miterLimit === undefined ? defaultMiterLimit : miterLimit;
|
|
strokeState.strokeStyle = asColorLike(
|
|
textStrokeStyle.getColor() || defaultStrokeStyle
|
|
);
|
|
}
|
|
|
|
textState = this.textState_;
|
|
const font = textStyle.getFont() || defaultFont;
|
|
registerFont(font);
|
|
const textScale = textStyle.getScaleArray();
|
|
textState.overflow = textStyle.getOverflow();
|
|
textState.font = font;
|
|
textState.maxAngle = textStyle.getMaxAngle();
|
|
textState.placement = textStyle.getPlacement();
|
|
textState.textAlign = textStyle.getTextAlign();
|
|
textState.textBaseline =
|
|
textStyle.getTextBaseline() || defaultTextBaseline;
|
|
textState.backgroundFill = textStyle.getBackgroundFill();
|
|
textState.backgroundStroke = textStyle.getBackgroundStroke();
|
|
textState.padding = textStyle.getPadding() || defaultPadding;
|
|
textState.scale = textScale === undefined ? [1, 1] : textScale;
|
|
|
|
const textOffsetX = textStyle.getOffsetX();
|
|
const textOffsetY = textStyle.getOffsetY();
|
|
const textRotateWithView = textStyle.getRotateWithView();
|
|
const textRotation = textStyle.getRotation();
|
|
this.text_ = textStyle.getText() || '';
|
|
this.textOffsetX_ = textOffsetX === undefined ? 0 : textOffsetX;
|
|
this.textOffsetY_ = textOffsetY === undefined ? 0 : textOffsetY;
|
|
this.textRotateWithView_ =
|
|
textRotateWithView === undefined ? false : textRotateWithView;
|
|
this.textRotation_ = textRotation === undefined ? 0 : textRotation;
|
|
|
|
this.strokeKey_ = strokeState
|
|
? (typeof strokeState.strokeStyle == 'string'
|
|
? strokeState.strokeStyle
|
|
: getUid(strokeState.strokeStyle)) +
|
|
strokeState.lineCap +
|
|
strokeState.lineDashOffset +
|
|
'|' +
|
|
strokeState.lineWidth +
|
|
strokeState.lineJoin +
|
|
strokeState.miterLimit +
|
|
'[' +
|
|
strokeState.lineDash.join() +
|
|
']'
|
|
: '';
|
|
this.textKey_ =
|
|
textState.font +
|
|
textState.scale +
|
|
(textState.textAlign || '?') +
|
|
(textState.textBaseline || '?');
|
|
this.fillKey_ = fillState
|
|
? typeof fillState.fillStyle == 'string'
|
|
? fillState.fillStyle
|
|
: '|' + getUid(fillState.fillStyle)
|
|
: '';
|
|
}
|
|
}
|
|
}
|
|
|
|
export default CanvasTextBuilder;
|