blob: 51cbd3983164826160094237f88fa335dba1ae49 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
import ExtensionAPI from '../../core/ExtensionAPI';
import GlobalModel from '../../model/Global';
import ComponentView from '../../view/Component';
import { ThumbnailModel } from './ThumbnailModel';
import { NullUndefined, RoamOptionMixin, RoamPayload } from '../../util/types';
import BoundingRect from 'zrender/src/core/BoundingRect';
import * as matrix from 'zrender/src/core/matrix';
import RoamController, { RoamEventParams } from '../helper/RoamController';
import tokens from '../../visual/tokens';
import { createBoxLayoutReference, getBoxLayoutParams, getLayoutRect } from '../../util/layout';
import { expandOrShrinkRect, Rect, Group, traverseUpdateZ, retrieveZInfo } from '../../util/graphic';
import { RectShape } from 'zrender/src/graphic/shape/Rect';
import { applyTransform } from 'zrender/src/core/vector';
import View, {
VIEW_COORD_SYS_TRANS_RAW, viewCoordSysCopyTrans,
viewCoordSysSetBoundingRect, viewCoordSysSetViewRect,
viewCoordSysCopyOverallMatrix
} from '../../coord/View';
import { bind, defaults, extend } from 'zrender/src/core/util';
import type ComponentModel from '../../model/Component';
import { ThumbnailBridgeRendered } from './ThumbnailBridgeImpl';
export class ThumbnailView extends ComponentView {
static type = 'thumbnail' as const;
type = ThumbnailView.type;
private _api: ExtensionAPI;
private _model: ThumbnailModel;
private _bgRect: Rect;
private _windowRect: Rect;
private _contentRect: BoundingRect;
private _targetGroup: Group;
private _transThisToTarget: matrix.MatrixArray;
private _roamController: RoamController;
private _coordSys: View;
private _bridgeRendered: ThumbnailBridgeRendered | NullUndefined;
// The version of rendering result.
// `render`/`updateContent`/`updateWindow` can be called separatedly and the order
// is not guaranteed. Use version to ensure the consistency.
private _renderVersion: number;
render(thumbnailModel: ThumbnailModel, ecModel: GlobalModel, api: ExtensionAPI): void {
this._api = api;
this._model = thumbnailModel;
if (!this._coordSys) {
this._coordSys = new View();
}
if (!this._isEnabled()) {
this._clear();
return;
}
this._renderVersion = api.getECUpdateCycleVersion();
const group = this.group;
group.removeAll();
const itemStyleModel = thumbnailModel.getModel('itemStyle');
const itemStyle = itemStyleModel.getItemStyle();
if (itemStyle.fill == null) {
itemStyle.fill = ecModel.get('backgroundColor') || tokens.color.neutral00;
}
const refContainer = createBoxLayoutReference(thumbnailModel, api).refContainer;
const boxRect = getLayoutRect(
getBoxLayoutParams(thumbnailModel, true),
refContainer
);
const boxBorderWidth = itemStyle.lineWidth || 0;
const contentRect = this._contentRect = expandOrShrinkRect(
boxRect.clone(), boxBorderWidth / 2, true, true
);
const contentGroup = new Group();
group.add(contentGroup);
contentGroup.setClipPath(new Rect({shape: contentRect.plain()}));
const targetGroup = this._targetGroup = new Group();
contentGroup.add(targetGroup);
// Draw border and background and shadow of thumbnail box.
const borderShape: RectShape = boxRect.plain();
borderShape.r = itemStyleModel.getShallow('borderRadius', true) as (number | number[]);
group.add(this._bgRect = new Rect({
style: itemStyle,
shape: borderShape,
silent: false, // Prevent from hovering on the lower elements.
cursor: 'grab',
}));
const windowStyleModel = thumbnailModel.getModel('windowStyle');
const windowR = windowStyleModel.getShallow('borderRadius', true) as (number | number[]);
contentGroup.add(this._windowRect = new Rect({
shape: {x: 0, y: 0, width: 0, height: 0, r: windowR},
style: windowStyleModel.getItemStyle(),
silent: false, // Prevent from hovering on the lower elements.
cursor: 'grab',
}));
this._dealRenderContent();
this._dealUpdateWindow();
updateZ(thumbnailModel, this);
}
/**
* Can be called asynchronously directly.
* This method should be idempotent.
*/
renderContent(bridgeRendered: ThumbnailBridgeRendered): void {
this._bridgeRendered = bridgeRendered;
if (this._isEnabled()) {
this._dealRenderContent();
this._dealUpdateWindow();
updateZ(this._model, this);
}
}
private _dealRenderContent(): void {
const bridgeRendered = this._bridgeRendered;
if (!bridgeRendered || bridgeRendered.renderVersion !== this._renderVersion) {
return;
}
const targetGroup = this._targetGroup;
const coordSys = this._coordSys;
const contentRect = this._contentRect;
targetGroup.removeAll();
if (!bridgeRendered) {
return;
}
const bridgeGroup = bridgeRendered.group;
const bridgeRect = bridgeGroup.getBoundingRect();
targetGroup.add(bridgeGroup);
this._bgRect.z2 = bridgeRendered.z2Range.min - 10;
viewCoordSysSetBoundingRect(coordSys, bridgeRect.x, bridgeRect.y, bridgeRect.width, bridgeRect.height);
// Use `getLayoutRect` is just to find an appropriate rect in thumbnail.
const viewRect = getLayoutRect(
{
left: 'center',
top: 'center',
aspect: bridgeRect.width / bridgeRect.height
},
contentRect
);
viewCoordSysSetViewRect(coordSys, viewRect.x, viewRect.y, viewRect.width, viewRect.height);
viewCoordSysCopyTrans(bridgeGroup, coordSys, VIEW_COORD_SYS_TRANS_RAW);
bridgeGroup.dirty();
this._windowRect.z2 = bridgeRendered.z2Range.max + 10;
this._resetRoamController(bridgeRendered.roamType);
}
/**
* Can be called from action handler directly.
* This method should be idempotent.
*/
updateWindow(param: Pick<ThumbnailBridgeRendered, 'targetTrans' | 'renderVersion'>): void {
const bridgeRendered = this._bridgeRendered;
if (bridgeRendered && bridgeRendered.renderVersion === param.renderVersion) {
bridgeRendered.targetTrans = param.targetTrans;
}
if (this._isEnabled()) {
this._dealUpdateWindow();
}
}
private _dealUpdateWindow(): void {
const bridgeRendered = this._bridgeRendered;
if (!bridgeRendered || bridgeRendered.renderVersion !== this._renderVersion) {
return;
}
const invTargetTrans = matrix.invert([], bridgeRendered.targetTrans);
const transTargetToThis = matrix.mul([], viewCoordSysCopyOverallMatrix(null, this._coordSys), invTargetTrans);
this._transThisToTarget = matrix.invert([], transTargetToThis);
let viewportRect = bridgeRendered.viewportRect;
if (!viewportRect) {
viewportRect = new BoundingRect(0, 0, this._api.getWidth(), this._api.getHeight());
}
else {
viewportRect = viewportRect.clone();
}
viewportRect.applyTransform(transTargetToThis);
const windowRect = this._windowRect;
const r = windowRect.shape.r;
windowRect.setShape(defaults({r}, viewportRect));
}
private _resetRoamController(
roamType: RoamOptionMixin['roam'],
): void {
const api = this._api;
let roamController = this._roamController;
if (!roamController) {
roamController = this._roamController = new RoamController(api.getZr());
}
if (!roamType || !this._isEnabled()) {
roamController.disable();
return;
}
roamController.enable(roamType, {
api: api,
zInfo: {component: this._model},
triggerInfo: {
roamTrigger: null,
isInSelf: (e, x, y) => this._contentRect.contain(x, y)
}
});
roamController
.off('pan')
.off('zoom')
.on('pan', bind(this._onPan, this))
.on('zoom', bind(this._onZoom, this));
// PENDING: Currently roaming animation (when roaming args are changed;
// e.g., `center` and `zoom` are changed) is not supported in thumbnail.
// Animation should be able to be interrupted on pan and zoom, and synchronize
// back to the target. See VIEW_COORD_SYS_ANIMATION.
}
private _onPan(event: RoamEventParams['pan']): void {
const trans = this._transThisToTarget;
if (!this._isEnabled() || !trans) {
return;
}
const oldOffset = applyTransform([], [event.oldX, event.oldY], trans);
const newOffset = applyTransform([], [event.oldX - event.dx, event.oldY - event.dy], trans);
this._api.dispatchAction(makeRoamPayload(this._model.getTarget().baseMapProvider, {
dx: newOffset[0] - oldOffset[0],
dy: newOffset[1] - oldOffset[1],
}));
}
private _onZoom(event: RoamEventParams['zoom']): void {
const trans = this._transThisToTarget;
if (!this._isEnabled() || !trans) {
return;
}
const offset = applyTransform([], [event.originX, event.originY], trans);
this._api.dispatchAction(makeRoamPayload(this._model.getTarget().baseMapProvider, {
zoom: 1 / event.scale,
originX: offset[0],
originY: offset[1],
}));
}
/**
* This method is also responsible for check enable in asynchronous situation,
* e.g., in event listeners that is supposed to be outdated but not be removed.
*/
private _isEnabled(): boolean {
const thumbnailModel = this._model;
if (!thumbnailModel || !thumbnailModel.shouldShow()) {
return false;
}
const baseMapProvider = thumbnailModel.getTarget().baseMapProvider;
if (!baseMapProvider) {
return false;
}
return true;
}
private _clear(): void {
this.group.removeAll();
this._bridgeRendered = null;
if (this._roamController) {
this._roamController.disable();
}
}
remove() {
this._clear();
}
dispose() {
this._clear();
}
}
function makeRoamPayload(baseMapProvider: ComponentModel, params: Partial<RoamPayload>): RoamPayload {
const type = baseMapProvider.mainType === 'series'
? `${baseMapProvider.subType}Roam` // e.g. 'graphRoam'
: `${baseMapProvider.mainType}Roam`; // e.g., 'geoRoam'
const payload = {type} as RoamPayload;
payload[`${baseMapProvider.mainType}Id`] = baseMapProvider.id;
extend(payload, params);
return payload;
}
function updateZ(thumbnailModel: ThumbnailModel, thumbnailView: ThumbnailView): void {
const zInfo = retrieveZInfo(thumbnailModel);
traverseUpdateZ(thumbnailView.group, zInfo.z, zInfo.zlevel);
}