You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

851 lines
30 KiB
TypeScript

/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
import * as SVG from 'svg.js';
// tslint:disable-next-line: ordered-imports
import 'svg.draggable.js';
import 'svg.resize.js';
import 'svg.select.js';
import { CanvasController } from './canvasController';
import { Listener, Master } from './master';
import { DrawHandler, DrawHandlerImpl } from './drawHandler';
import { MergeHandler, MergeHandlerImpl } from './mergeHandler';
import { SplitHandler, SplitHandlerImpl } from './splitHandler';
import { GroupHandler, GroupHandlerImpl } from './groupHandler';
import { translateToSVG, translateFromSVG } from './shared';
import consts from './consts';
import {
CanvasModel,
Geometry,
Size,
UpdateReasons,
FocusData,
FrameZoom,
ActiveElement,
DrawData,
} from './canvasModel';
export interface CanvasView {
html(): HTMLDivElement;
}
interface ShapeDict {
[index: number]: SVG.Shape;
}
interface TextDict {
[index: number]: SVG.Text;
}
enum Mode {
IDLE = 'idle',
DRAG = 'drag',
RESIZE = 'resize',
DRAW = 'draw',
}
function selectize(value: boolean, shape: SVG.Element, geometry: Geometry): void {
if (value) {
(shape as any).selectize(value, {
deepSelect: true,
pointSize: consts.BASE_POINT_SIZE / geometry.scale,
rotationPoint: false,
pointType(cx: number, cy: number): SVG.Circle {
const circle: SVG.Circle = this.nested
.circle(this.options.pointSize)
.stroke('black')
.fill(shape.node.getAttribute('fill'))
.center(cx, cy)
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (3 * geometry.scale),
});
circle.node.addEventListener('mouseenter', (): void => {
circle.attr({
'stroke-width': circle.attr('stroke-width') * 2,
});
circle.addClass('cvat_canvas_selected_point');
});
circle.node.addEventListener('mouseleave', (): void => {
circle.attr({
'stroke-width': circle.attr('stroke-width') / 2,
});
circle.removeClass('cvat_canvas_selected_point');
});
return circle;
},
});
} else {
(shape as any).selectize(false, {
deepSelect: true,
});
}
}
function darker(color: string, percentage: number): string {
const R = Math.round(parseInt(color.slice(1, 3), 16) * (1 - percentage / 100));
const G = Math.round(parseInt(color.slice(3, 5), 16) * (1 - percentage / 100));
const B = Math.round(parseInt(color.slice(5, 7), 16) * (1 - percentage / 100));
const rHex = Math.max(0, R).toString(16);
const gHex = Math.max(0, G).toString(16);
const bHex = Math.max(0, B).toString(16);
return `#${rHex.length === 1 ? `0${rHex}` : rHex}`
+ `${gHex.length === 1 ? `0${gHex}` : gHex}`
+ `${bHex.length === 1 ? `0${bHex}` : bHex}`;
}
export class CanvasViewImpl implements CanvasView, Listener {
private loadingAnimation: SVGSVGElement;
private text: SVGSVGElement;
private adoptedText: SVG.Container;
private background: SVGSVGElement;
private grid: SVGSVGElement;
private content: SVGSVGElement;
private adoptedContent: SVG.Container;
private canvas: HTMLDivElement;
private gridPath: SVGPathElement;
private gridPattern: SVGPatternElement;
private controller: CanvasController;
private svgShapes: ShapeDict;
private svgTexts: TextDict;
private drawHandler: DrawHandler;
private mergeHandler: MergeHandler;
private splitHandler: SplitHandler;
private groupHandler: GroupHandler;
private activeElement: {
state: any;
attributeID: number;
};
private mode: Mode;
private onDrawDone(data: object): void {
if (data) {
const event: CustomEvent = new CustomEvent('canvas.drawn', {
bubbles: false,
cancelable: true,
detail: {
// eslint-disable-next-line new-cap
state: new this.controller.objectStateClass(data),
},
});
this.canvas.dispatchEvent(event);
} else {
const event: CustomEvent = new CustomEvent('canvas.canceled', {
bubbles: false,
cancelable: true,
});
this.canvas.dispatchEvent(event);
}
this.controller.draw({
enabled: false,
});
}
private onMergeDone(objects: any[]): void {
if (objects) {
const event: CustomEvent = new CustomEvent('canvas.merged', {
bubbles: false,
cancelable: true,
detail: {
states: objects,
},
});
this.canvas.dispatchEvent(event);
} else {
const event: CustomEvent = new CustomEvent('canvas.canceled', {
bubbles: false,
cancelable: true,
});
this.canvas.dispatchEvent(event);
}
}
private onSplitDone(object: any): void {
if (object) {
const event: CustomEvent = new CustomEvent('canvas.splitted', {
bubbles: false,
cancelable: true,
detail: {
state: object,
},
});
this.canvas.dispatchEvent(event);
} else {
const event: CustomEvent = new CustomEvent('canvas.canceled', {
bubbles: false,
cancelable: true,
});
this.canvas.dispatchEvent(event);
}
}
private onGroupDone(objects: any[], reset: boolean): void {
if (objects) {
const event: CustomEvent = new CustomEvent('canvas.groupped', {
bubbles: false,
cancelable: true,
detail: {
states: objects,
reset,
},
});
this.canvas.dispatchEvent(event);
} else {
const event: CustomEvent = new CustomEvent('canvas.canceled', {
bubbles: false,
cancelable: true,
});
this.canvas.dispatchEvent(event);
}
}
public constructor(model: CanvasModel & Master, controller: CanvasController) {
this.controller = controller;
this.svgShapes = {};
this.svgTexts = {};
this.activeElement = null;
this.mode = Mode.IDLE;
// Create HTML elements
this.loadingAnimation = window.document
.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.text = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.adoptedText = (SVG.adopt((this.text as any as HTMLElement)) as SVG.Container);
this.background = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.grid = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.gridPath = window.document.createElementNS('http://www.w3.org/2000/svg', 'path');
this.gridPattern = window.document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
this.content = window.document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.adoptedContent = (SVG.adopt((this.content as any as HTMLElement)) as SVG.Container);
this.canvas = window.document.createElement('div');
const loadingCircle: SVGCircleElement = window.document
.createElementNS('http://www.w3.org/2000/svg', 'circle');
const gridDefs: SVGDefsElement = window.document
.createElementNS('http://www.w3.org/2000/svg', 'defs');
const gridRect: SVGRectElement = window.document
.createElementNS('http://www.w3.org/2000/svg', 'rect');
// Setup loading animation
this.loadingAnimation.setAttribute('id', 'cvat_canvas_loading_animation');
loadingCircle.setAttribute('id', 'cvat_canvas_loading_circle');
loadingCircle.setAttribute('r', '30');
loadingCircle.setAttribute('cx', '50%');
loadingCircle.setAttribute('cy', '50%');
// Setup grid
this.grid.setAttribute('id', 'cvat_canvas_grid');
this.grid.setAttribute('version', '2');
this.gridPath.setAttribute('d', 'M 1000 0 L 0 0 0 1000');
this.gridPath.setAttribute('fill', 'none');
this.gridPath.setAttribute('stroke-width', '1.5');
this.gridPattern.setAttribute('id', 'cvat_canvas_grid_pattern');
this.gridPattern.setAttribute('width', '100');
this.gridPattern.setAttribute('height', '100');
this.gridPattern.setAttribute('patternUnits', 'userSpaceOnUse');
gridRect.setAttribute('width', '100%');
gridRect.setAttribute('height', '100%');
gridRect.setAttribute('fill', 'url(#cvat_canvas_grid_pattern)');
// Setup content
this.text.setAttribute('id', 'cvat_canvas_text_content');
this.background.setAttribute('id', 'cvat_canvas_background');
this.content.setAttribute('id', 'cvat_canvas_content');
// Setup wrappers
this.canvas.setAttribute('id', 'cvat_canvas_wrapper');
// Unite created HTML elements together
this.loadingAnimation.appendChild(loadingCircle);
this.grid.appendChild(gridDefs);
this.grid.appendChild(gridRect);
gridDefs.appendChild(this.gridPattern);
this.gridPattern.appendChild(this.gridPath);
this.canvas.appendChild(this.loadingAnimation);
this.canvas.appendChild(this.text);
this.canvas.appendChild(this.background);
this.canvas.appendChild(this.grid);
this.canvas.appendChild(this.content);
// A little hack to get size after first mounting
// http://www.backalleycoder.com/2012/04/25/i-want-a-damnodeinserted/
const self = this;
const canvasFirstMounted = (event: AnimationEvent): void => {
if (event.animationName === 'loadingAnimation') {
const { geometry } = this.controller;
geometry.canvas = {
height: self.canvas.clientHeight,
width: self.canvas.clientWidth,
};
this.controller.geometry = geometry;
self.canvas.removeEventListener('animationstart', canvasFirstMounted);
}
};
this.canvas.addEventListener('animationstart', canvasFirstMounted);
// Setup API handlers
this.drawHandler = new DrawHandlerImpl(
this.onDrawDone.bind(this),
this.adoptedContent,
this.background,
);
this.mergeHandler = new MergeHandlerImpl(
this.onMergeDone.bind(this),
);
this.splitHandler = new SplitHandlerImpl(
this.onSplitDone.bind(this),
);
this.groupHandler = new GroupHandlerImpl(
this.onGroupDone.bind(this),
);
// Setup event handlers
this.content.addEventListener('dblclick', (): void => {
self.controller.fit();
});
this.content.addEventListener('mousedown', (event): void => {
if ((event.which === 1 && this.mode === Mode.IDLE) || (event.which === 2)) {
self.controller.enableDrag(event.clientX, event.clientY);
}
});
this.content.addEventListener('mousemove', (event): void => {
self.controller.drag(event.clientX, event.clientY);
});
window.document.addEventListener('mouseup', (event): void => {
if (event.which === 1 || event.which === 2) {
self.controller.disableDrag();
}
});
this.content.addEventListener('wheel', (event): void => {
const point = translateToSVG(self.background, [event.clientX, event.clientY]);
self.controller.zoom(point[0], point[1], event.deltaY > 0 ? -1 : 1);
event.preventDefault();
});
this.content.addEventListener('mousemove', (e): void => {
if (this.mode !== Mode.IDLE) return;
const [x, y] = translateToSVG(this.background, [e.clientX, e.clientY]);
const event: CustomEvent = new CustomEvent('canvas.moved', {
bubbles: false,
cancelable: true,
detail: {
x,
y,
states: this.controller.objects,
},
});
this.canvas.dispatchEvent(event);
});
this.content.oncontextmenu = (): boolean => false;
model.subscribe(this);
}
public notify(model: CanvasModel & Master, reason: UpdateReasons): void {
function transform(geometry: Geometry): void {
// Transform canvas
for (const obj of [this.background, this.grid, this.loadingAnimation, this.content]) {
obj.style.transform = `scale(${geometry.scale}) rotate(${geometry.angle}deg)`;
}
// Transform grid
this.gridPath.setAttribute('stroke-width', `${consts.BASE_STROKE_WIDTH / (2 * geometry.scale)}px`);
// Transform all shape points
for (const element of window.document.getElementsByClassName('svg_select_points')) {
element.setAttribute(
'stroke-width',
`${consts.BASE_STROKE_WIDTH / (3 * geometry.scale)}`,
);
element.setAttribute(
'r',
`${consts.BASE_POINT_SIZE / (2 * geometry.scale)}`,
);
}
for (const element of
window.document.getElementsByClassName('cvat_canvas_selected_point')) {
element.setAttribute(
'stroke-width',
`${+element.getAttribute('stroke-width') * 2}`,
);
}
// Transform all drawn shapes
for (const key in this.svgShapes) {
if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)) {
const object = this.svgShapes[key];
if (object.attr('stroke-width')) {
object.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / (geometry.scale),
});
}
}
}
// Transform all text
for (const key in this.svgShapes) {
if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)
&& Object.prototype.hasOwnProperty.call(this.svgTexts, key)) {
this.updateTextPosition(
this.svgTexts[key],
this.svgShapes[key],
);
}
}
// Transform handlers
this.drawHandler.transform(geometry);
}
function resize(geometry: Geometry): void {
for (const obj of [this.background, this.grid, this.loadingAnimation]) {
obj.style.width = `${geometry.image.width}px`;
obj.style.height = `${geometry.image.height}px`;
}
for (const obj of [this.content, this.text]) {
obj.style.width = `${geometry.image.width + geometry.offset * 2}px`;
obj.style.height = `${geometry.image.height + geometry.offset * 2}px`;
}
}
function move(geometry: Geometry): void {
for (const obj of [this.background, this.grid, this.loadingAnimation]) {
obj.style.top = `${geometry.top}px`;
obj.style.left = `${geometry.left}px`;
}
for (const obj of [this.content, this.text]) {
obj.style.top = `${geometry.top - geometry.offset}px`;
obj.style.left = `${geometry.left - geometry.offset}px`;
}
// Transform handlers
this.drawHandler.transform(geometry);
}
function computeFocus(focusData: FocusData, geometry: Geometry): void {
// This computation cann't be done in the model because of lack of data
const object = this.svgShapes[focusData.clientID];
if (!object) {
return;
}
// First of all, compute and apply scale
let scale = null;
const bbox: SVG.BBox = object.node.getBBox();
if ((geometry.angle / 90) % 2) {
// 90, 270, ..
scale = Math.min(Math.max(Math.min(
geometry.canvas.width / bbox.height,
geometry.canvas.height / bbox.width,
), FrameZoom.MIN), FrameZoom.MAX);
} else {
scale = Math.min(Math.max(Math.min(
geometry.canvas.width / bbox.width,
geometry.canvas.height / bbox.height,
), FrameZoom.MIN), FrameZoom.MAX);
}
transform.call(this, Object.assign({}, geometry, {
scale,
}));
const [x, y] = translateFromSVG(this.content, [
bbox.x + bbox.width / 2,
bbox.y + bbox.height / 2,
]);
const [cx, cy] = [
this.canvas.clientWidth / 2 + this.canvas.offsetLeft,
this.canvas.clientHeight / 2 + this.canvas.offsetTop,
];
const dragged = Object.assign({}, geometry, {
top: geometry.top + cy - y,
left: geometry.left + cx - x,
scale,
});
this.controller.geometry = dragged;
move.call(this, dragged);
}
function setupObjects(objects: any[], geometry: Geometry): void {
const ctm = this.content.getScreenCTM()
.inverse().multiply(this.background.getScreenCTM());
this.deactivate();
// TODO: Compute difference
// Instead of simple clearing let's remove all objects properly
for (const id of Object.keys(this.svgShapes)) {
if (id in this.svgTexts) {
this.svgTexts[id].remove();
}
this.svgShapes[id].remove();
}
this.svgTexts = {};
this.svgShapes = {};
this.addObjects(ctm, objects, geometry);
// TODO: Update objects
// TODO: Delete objects
this.mergeHandler.updateObjects(objects);
this.groupHandler.updateObjects(objects);
this.splitHandler.updateObjects(objects);
}
const { geometry } = this.controller;
if (reason === UpdateReasons.IMAGE) {
if (!model.image.length) {
this.loadingAnimation.classList.remove('cvat_canvas_hidden');
} else {
this.loadingAnimation.classList.add('cvat_canvas_hidden');
this.background.style.backgroundImage = `url("${model.image}")`;
move.call(this, geometry);
resize.call(this, geometry);
transform.call(this, geometry);
}
} else if (reason === UpdateReasons.ZOOM || reason === UpdateReasons.FIT) {
move.call(this, geometry);
transform.call(this, geometry);
} else if (reason === UpdateReasons.MOVE) {
move.call(this, geometry);
} else if (reason === UpdateReasons.OBJECTS) {
setupObjects.call(this, this.controller.objects, geometry);
const event: CustomEvent = new CustomEvent('canvas.setup');
this.canvas.dispatchEvent(event);
} else if (reason === UpdateReasons.GRID) {
const size: Size = geometry.grid;
this.gridPattern.setAttribute('width', `${size.width}`);
this.gridPattern.setAttribute('height', `${size.height}`);
} else if (reason === UpdateReasons.FOCUS) {
computeFocus.call(this, this.controller.focusData, geometry);
} else if (reason === UpdateReasons.ACTIVATE) {
this.activate(geometry, this.controller.activeElement);
} else if (reason === UpdateReasons.DRAW) {
const data: DrawData = this.controller.drawData;
if (data.enabled) {
this.mode = Mode.DRAW;
this.deactivate();
} else {
this.mode = Mode.IDLE;
}
this.drawHandler.draw(data, geometry);
}
}
public html(): HTMLDivElement {
return this.canvas;
}
private addObjects(ctm: SVGMatrix, states: any[], geometry: Geometry): void {
for (const state of states) {
if (state.objectType === 'tag') {
this.addTag(state, geometry);
} else {
const points: number[] = (state.points as number[]);
const translatedPoints: number[] = [];
for (let i = 0; i <= points.length - 1; i += 2) {
let point: SVGPoint = this.background.createSVGPoint();
point.x = points[i];
point.y = points[i + 1];
point = point.matrixTransform(ctm);
translatedPoints.push(point.x, point.y);
}
// TODO: Use enums after typification cvat-core
if (state.shapeType === 'rectangle') {
this.svgShapes[state.clientID] = this
.addRect(translatedPoints, state, geometry);
} else {
const stringified = translatedPoints.reduce(
(acc: string, val: number, idx: number): string => {
if (idx % 2) {
return `${acc}${val} `;
}
return `${acc}${val},`;
}, '',
);
if (state.shapeType === 'polygon') {
this.svgShapes[state.clientID] = this
.addPolygon(stringified, state, geometry);
} else if (state.shapeType === 'polyline') {
this.svgShapes[state.clientID] = this
.addPolyline(stringified, state, geometry);
} else if (state.shapeType === 'points') {
this.svgShapes[state.clientID] = this
.addPoints(stringified, state, geometry);
}
}
// TODO: Use enums after typification cvat-core
if (state.visibility === 'all') {
this.svgTexts[state.clientID] = this.addText(state);
this.updateTextPosition(
this.svgTexts[state.clientID],
this.svgShapes[state.clientID],
);
}
}
}
}
private deactivate(): void {
if (this.activeElement) {
const { state } = this.activeElement;
const shape = this.svgShapes[this.activeElement.state.clientID];
(shape as any).draggable(false);
if (state.shapeType !== 'points') {
selectize(false, shape, null);
}
(shape as any).resize(false);
// Hide text only if it is hidden by settings
const text = this.svgTexts[state.clientID];
if (text && state.visibility === 'shape') {
text.remove();
delete this.svgTexts[state.clientID];
}
this.activeElement = null;
}
}
private activate(geometry: Geometry, activeElement: ActiveElement): void {
// Check if other element have been already activated
if (this.activeElement) {
// Check if it is the same element
if (this.activeElement.state.clientID === activeElement.clientID) {
return;
}
// Deactivate previous element
this.deactivate();
}
const state = this.controller.objects
.filter((el): boolean => el.clientID === activeElement.clientID)[0];
this.activeElement = {
attributeID: activeElement.attributeID,
state,
};
const shape = this.svgShapes[activeElement.clientID];
let text = this.svgTexts[activeElement.clientID];
// Draw text if it's hidden by default
if (!text && state.visibility === 'shape') {
text = this.addText(state);
this.svgTexts[state.clientID] = text;
this.updateTextPosition(
text,
shape,
);
}
const self = this;
this.content.append(shape.node);
(shape as any).draggable().on('dragstart', (): void => {
this.mode = Mode.DRAG;
if (text) {
text.addClass('cvat_canvas_hidden');
}
}).on('dragend', (): void => {
this.mode = Mode.IDLE;
if (text) {
text.removeClass('cvat_canvas_hidden');
self.updateTextPosition(
text,
shape,
);
}
});
if (state.shapeType !== 'points') {
selectize(true, shape, geometry);
}
(shape as any).resize().on('resizestart', (): void => {
this.mode = Mode.RESIZE;
if (text) {
text.addClass('cvat_canvas_hidden');
}
}).on('resizedone', (): void => {
this.mode = Mode.IDLE;
if (text) {
text.removeClass('cvat_canvas_hidden');
self.updateTextPosition(
text,
shape,
);
}
});
}
// Update text position after corresponding box has been moved, resized, etc.
private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void {
let box = (shape.node as any).getBBox();
// Translate the whole box to the client coordinate system
const [x1, y1, x2, y2]: number[] = translateFromSVG(this.content, [
box.x,
box.y,
box.x + box.width,
box.y + box.height,
]);
box = {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.max(x1, x2) - Math.min(x1, x2),
height: Math.max(y1, y2) - Math.min(y1, y2),
};
// Find the best place for a text
let [clientX, clientY]: number[] = [box.x + box.width, box.y];
if (clientX + (text.node as any as SVGTextElement)
.getBBox().width + consts.TEXT_MARGIN > this.canvas.offsetWidth) {
([clientX, clientY] = [box.x, box.y]);
}
// Translate back to text SVG
const [x, y]: number[] = translateToSVG(this.text, [
clientX + consts.TEXT_MARGIN,
clientY,
]);
// Finally draw a text
text.move(x, y);
for (const tspan of (text.lines() as any).members) {
tspan.attr('x', text.attr('x'));
}
}
private addText(state: any): SVG.Text {
const { label, clientID, attributes } = state;
const attrNames = label.attributes.reduce((acc: any, val: any): void => {
acc[val.id] = val.name;
return acc;
}, {});
return this.adoptedText.text((block): void => {
block.tspan(`${label.name} ${clientID}`).style('text-transform', 'uppercase');
for (const attrID of Object.keys(attributes)) {
block.tspan(`${attrNames[attrID]}: ${attributes[attrID]}`).attr({
attrID,
dy: '1em',
x: 0,
});
}
}).move(0, 0).addClass('cvat_canvas_text');
}
private addRect(points: number[], state: any, geometry: Geometry): SVG.Rect {
const [xtl, ytl, xbr, ybr] = points;
return this.adoptedContent.rect().size(xbr - xtl, ybr - ytl).attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50),
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
zOrder: state.zOrder,
}).move(xtl, ytl)
.addClass('cvat_canvas_shape');
}
private addPolygon(points: string, state: any, geometry: Geometry): SVG.Polygon {
return this.adoptedContent.polygon(points).attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50),
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
zOrder: state.zOrder,
}).addClass('cvat_canvas_shape');
}
private addPolyline(points: string, state: any, geometry: Geometry): SVG.PolyLine {
return this.adoptedContent.polyline(points).attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
fill: state.color,
'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50),
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
zOrder: state.zOrder,
}).addClass('cvat_canvas_shape');
}
private addPoints(points: string, state: any, geometry: Geometry): SVG.PolyLine {
const shape = this.adoptedContent.polyline(points).attr({
clientID: state.clientID,
'color-rendering': 'optimizeQuality',
fill: state.color,
'shape-rendering': 'geometricprecision',
zOrder: state.zOrder,
}).addClass('cvat_canvas_shape');
selectize(true, shape, geometry);
shape.remove = function remove(): void {
this.selectize(false, shape);
shape.constructor.prototype.remove.call(shape);
}.bind(this);
shape.attr('fill', 'none');
return shape;
}
/* eslint-disable-next-line */
private addTag(state: any, geometry: Geometry): void {
console.log(state, geometry);
}
}