import { extend, isNil } from '../core/util';
import { escapeSpecialChars } from '../core/util/strings';
import Coordinate from '../geo/Coordinate';
import TextMarker, { TextMarkerOptionsType } from './TextMarker';
import { isFunctionDefinition, interpolated } from '@maptalks/function-type';
import { TextSymbol, VectorMarkerSymbol } from '../symbol';
import { GeometryEditOptionsType } from './ext/Geometry.Edit';
/**
* @property {Object} [options=null] - textbox's options, also including options of [Marker]{@link Marker#options}
* @property {Boolean} [options.textStyle] - the default text style of text
* @property {Boolean} [options.textStyle.wrap=true] - whether to autowrap text in the textbox
* @property {Boolean} [options.textStyle.padding=[12, 8]] - text padding in the box
* @property {Boolean} [options.textStyle.verticalAlignment=middle] - text's vertical alignment
* @property {Boolean} [options.textStyle.horizontalAlignment=true] - text's horizontal alignment
* @property {Boolean} [options.boxSymbol=null] - box symbol of textbox
* @memberOf TextBox
* @instance
*/
const options: TextBoxOptionsType = {
'textStyle': {
'wrap': true,
'padding': [12, 8],
'verticalAlignment': 'middle',
'horizontalAlignment': 'middle'
},
'boxSymbol': null
};
/**
* @classdesc
* Represents point type geometry for text boxes.
* A TextBox is used to draw a box with text inside on a particular coordinate.
* @category geometry
* @extends TextMarker
* @mixes TextEditable
* @example
* var textbox = new maptalks.TextBox('This is a textbox',
[0, 0], 200, 90,
{
'draggable' : true,
'textStyle' : {
'wrap' : true,
'padding' : [12, 8],
'verticalAlignment' : 'top',
'horizontalAlignment' : 'right',
'symbol' : {
'textFaceName' : 'monospace',
'textFill' : '#34495e',
'textHaloFill' : '#fff',
'textHaloRadius' : 4,
'textSize' : 18,
'textWeight' : 'bold'
}
},
'boxSymbol': {
// box's symbol
'markerType' : 'square',
'markerFill' : 'rgb(135,196,240)',
'markerFillOpacity' : 0.9,
'markerLineColor' : '#34495e',
'markerLineWidth' : 1
}
});
*/
class TextBox extends TextMarker {
options: TextBoxOptionsType;
//@internal
_width: number
//@internal
_height: number
//@internal
_oldWidth: any
//@internal
_oldHeight: any
/**
* @param {String} content - TextBox's text content
* @param {Coordinate} coordinates - coordinates
* @param {Number} width - width in pixel
* @param {Number} height - height in pixel
* @param {Object} [options=null] - construct options defined in [TextBox]{@link TextBox#options}
*/
constructor(content: string, coordinates: Coordinate | Array, width: number, height: number, options: TextBoxOptionsType = {}) {
super(coordinates, options);
this._content = escapeSpecialChars(content);
this._width = isNil(width) ? 100 : width;
this._height = isNil(height) ? 40 : height;
if (options.boxSymbol) {
this.setBoxSymbol(options.boxSymbol);
}
if (options.textStyle) {
this.setTextStyle(options.textStyle);
}
this._refresh();
}
/**
* 获取文本框得宽度
* @english
* Get textbox's width
* @return {Number}
*/
getWidth(): number {
return this._width;
}
/**
* 设置文本框得宽度
* @english
* Set new width to textbox
* @param {Number} width
* returns {TextBox} this
*/
setWidth(width: number) {
this._width = width;
this._refresh();
return this;
}
/**
* 获取文本框高度
* @english
* Get textbox's height
* @return {Number}
*/
getHeight(): number {
return this._height;
}
/**
* 设置文本框高度
* @english
* Set new height to textbox
* @param {Number} height
* returns {TextBox} this
*/
setHeight(height: number) {
this._height = height;
this._refresh();
return this;
}
/**
* 获取文本框边框样式
* @english
* Get textbox's boxSymbol
* @return {Object} boxsymbol
*/
getBoxSymbol(): VectorMarkerSymbol {
return extend({}, this.options.boxSymbol);
}
/**
* 设置文本框边框样式
* @english
* Set a new box symbol to textbox
* @param {Object} symbol
* returns {TextBox} this
*/
setBoxSymbol(symbol: VectorMarkerSymbol) {
this.options.boxSymbol = symbol ? extend({}, symbol) : symbol;
if (this.getSymbol()) {
this._refresh();
}
return this;
}
/**
* 获取文本框文本样式
* @english
* Get textbox's text style
* @return {Object}
*/
getTextStyle(): TextStyle | null {
if (!this.options.textStyle) {
return null;
}
return extend({}, this.options.textStyle);
}
/**
* 设置文本框文本样式
* @english
* Set a new text style to the textbox
* @param {Object} style new text style
* returns {TextBox} this
*/
setTextStyle(style: TextStyle) {
this.options.textStyle = style ? extend({}, style) : style;
if (this.getSymbol()) {
this._refresh();
}
return this;
}
static fromJSON(json: { [key: string]: any }): TextBox {
const feature = json['feature'];
const textBox = new TextBox(json['content'], feature['geometry']['coordinates'], json['width'], json['height'], json['options']);
textBox.setProperties(feature['properties']);
textBox.setId(feature['id']);
if (json['symbol']) {
textBox.setSymbol(json['symbol']);
}
return textBox;
}
//@internal
_toJSON(options: any) {
return {
'feature': this.toGeoJSON(options),
'width': this.getWidth(),
'height': this.getHeight(),
'subType': 'TextBox',
'content': this._content
};
}
//@internal
_refresh(): void {
const textStyle = this.getTextStyle() || {},
padding = textStyle['padding'] || [12, 8];
let maxWidth, maxHeight;
if (isFunctionDefinition(this._width)) {
maxWidth = JSON.parse(JSON.stringify(this._width));
const stops = maxWidth.stops;
if (stops) {
for (let i = 0; i < stops.length; i++) {
stops[i][1] = stops[i][1] - 2 * padding[0];
}
}
} else {
maxWidth = this._width - 2 * padding[0];
}
if (isFunctionDefinition(this._height)) {
maxHeight = JSON.parse(JSON.stringify(this._height));
const stops = maxHeight.stops;
if (stops) {
for (let i = 0; i < stops.length; i++) {
stops[i][1] = stops[i][1] - 2 * padding[1];
}
}
} else {
maxHeight = this._height - 2 * padding[1];
}
const symbol = extend({},
textStyle.symbol || this._getDefaultTextSymbol(),
this.options.boxSymbol || this._getDefaultBoxSymbol(),
{
'textName': this._content,
'markerWidth': this._width,
'markerHeight': this._height,
'textHorizontalAlignment': 'middle',
'textVerticalAlignment': 'middle',
'textMaxWidth': maxWidth,
'textMaxHeight': maxHeight
});
if (textStyle['wrap'] && !symbol['textWrapWidth']) {
symbol['textWrapWidth'] = maxWidth;
}
// function-type markerWidth and markerHeight doesn't support left/right horizontalAlignment and top/bottom verticalAlignment now
const hAlign = textStyle['horizontalAlignment'];
symbol['textDx'] = symbol['markerDx'] || 0;
let offsetX;
if (isFunctionDefinition(this._width)) {
offsetX = JSON.parse(JSON.stringify(this._width));
const stops = offsetX.stops;
if (stops) {
for (let i = 0; i < stops.length; i++) {
stops[i][1] = stops[i][1] / 2 - padding[0];
if (hAlign === 'left') {
stops[i][1] *= -1;
}
}
}
} else {
offsetX = symbol['markerWidth'] / 2 - padding[0];
if (hAlign === 'left') {
offsetX *= -1;
}
}
if (hAlign === 'left') {
symbol['textHorizontalAlignment'] = 'right';
symbol['textDx'] = offsetX;
} else if (hAlign === 'right') {
symbol['textHorizontalAlignment'] = 'left';
symbol['textDx'] = offsetX;
}
const vAlign = textStyle['verticalAlignment'];
symbol['textDy'] = symbol['markerDy'] || 0;
let offsetY;
if (isFunctionDefinition(this._height)) {
offsetY = JSON.parse(JSON.stringify(this._height));
const stops = offsetY.stops;
if (stops) {
for (let i = 0; i < stops.length; i++) {
stops[i][1] = stops[i][1] / 2 - padding[1];
if (vAlign === 'top') {
stops[i][1] *= -1;
}
}
}
} else {
offsetY = symbol['markerHeight'] / 2 - padding[1];
if (vAlign === 'top') {
offsetY *= -1;
}
}
if (vAlign === 'top') {
symbol['textVerticalAlignment'] = 'bottom';
symbol['textDy'] = offsetY;
} else if (vAlign === 'bottom') {
symbol['textVerticalAlignment'] = 'top';
symbol['textDy'] = offsetY;
}
this._refreshing = true;
this.updateSymbol(symbol);
delete this._refreshing;
}
startEdit(opts: GeometryEditOptionsType): this {
const symbol = this._getCompiledSymbol();
if (isFunctionDefinition(this._width)) {
const markerWidth = symbol['markerWidth'];
this._oldWidth = this._width;
this.setWidth(markerWidth);
}
if (isFunctionDefinition(this._height)) {
const markerHeight = symbol['markerHeight'];
this._oldHeight = this._height;
this.setHeight(markerHeight);
}
return super.startEdit(opts);
}
endEdit(): this {
const map = this.getMap();
const zoom = map && map.getZoom();
if (this._oldWidth) {
const markerWidth = this._width;
const widthFn = interpolated(this._oldWidth);
const oldExpectedWidth = widthFn(zoom);
const scale = markerWidth / oldExpectedWidth;
const stops = this._oldWidth.stops;
for (let i = 0; i < stops.length; i++) {
stops[i][1] *= scale;
}
this.setWidth(this._oldWidth);
delete this._oldWidth;
}
if (this._oldHeight) {
const markerHeight = this._height;
const heightFn = interpolated(this._oldHeight);
const oldExpectedHeight = heightFn(zoom);
const scale = markerHeight / oldExpectedHeight;
const stops = this._oldHeight.stops;
for (let i = 0; i < stops.length; i++) {
stops[i][1] *= scale;
}
this.setHeight(this._oldHeight);
delete this._oldHeight;
}
return super.endEdit();
}
}
TextBox.mergeOptions(options);
TextBox.registerJSONType('TextBox');
export default TextBox;
type TextStyle = {
wrap?: boolean;
padding?: [number, number];
verticalAlignment?: 'top' | 'middle' | 'bottom';
horizontalAlignment?: 'left' | 'middle' | 'right';
symbol?: TextSymbol;
}
export type TextBoxOptionsType = TextMarkerOptionsType & {
boxSymbol?: VectorMarkerSymbol;
textStyle?: TextStyle
}