Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: drawing annotation deletion #337

Merged
merged 108 commits into from
Aug 30, 2017
Merged
Show file tree
Hide file tree
Changes from 97 commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
12cc4ed
Fix: thread save incorrectly rejects when no dialogue exists
Jul 26, 2017
1872d82
Fix: error attempting to set dialogue threadnum when no dialogue exists
Jul 26, 2017
6db4184
Merge branch 'master' into branch/rebaseToRemote
Minh-Ng Jul 26, 2017
48af9ac
Merge branch 'master' into branch/rebaseToRemote
Minh-Ng Jul 27, 2017
533030c
Update: drawing annotations now rescale correctly
Jul 27, 2017
7f661ea
Fix: annotator is now loaded with the correct initial scale
Jul 28, 2017
7230510
Fix: update annotator tests for constructor change
Jul 28, 2017
b90659a
Update: drawingannotation rescaling
Jul 28, 2017
000b503
Merge branch 'master' into branch/rebaseToRemote
Jul 28, 2017
48b5589
Merge remote-tracking branch 'upstream/master' into feature/drawingAn…
Jul 28, 2017
8c88200
Update: tests for updating local annotations on annotationthreads
Jul 28, 2017
f3b557c
Merge branch 'branch/rebaseToRemote' of https://github.com/MinhHNguye…
Jul 28, 2017
ed403b8
Fix: typo
Jul 28, 2017
22d4faa
Update: merge initial scaling changes
Jul 28, 2017
4be971e
Merge branch 'branch/rebaseToRemote' into feature/drawingAnnotationSc…
Jul 28, 2017
51b0201
Update: fix tests
Jul 29, 2017
0757c0b
Fix: clean up anonymous functions
Jul 29, 2017
e4a097b
Fix: clean up anonymous functions
Jul 29, 2017
1075f78
Update: merge from master
Jul 31, 2017
f25c34c
Update: tests for docdrawingthread
Jul 31, 2017
70d619c
Update: change page change method
Aug 1, 2017
833a42a
Fix: various cleanup for PR
Aug 1, 2017
fe3587d
Fix: removing fixes coming in a different PR
Aug 1, 2017
924cd25
Fix: sentenced to 1 year of unit tests
Aug 1, 2017
3549473
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 1, 2017
1857b90
Update: docdrawingthread documentation
Aug 1, 2017
cb87a9e
Merge branch 'feature/drawingAnnotationScaling' of https://github.com…
Aug 1, 2017
5402f45
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 1, 2017
67e250f
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 1, 2017
bf29d9f
Update: merge from upstream master
Aug 2, 2017
7bd6fdb
Merge branch 'feature/drawingAnnotationScaling' of https://github.com…
Aug 2, 2017
74c85fc
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 2, 2017
10d1616
Update: undo redo drawing container
Aug 3, 2017
18f0444
Merge branch 'master' into feature/drawingAnnotationScaling
pramodsum Aug 4, 2017
60e259b
Update: resolve merge conflicts with master
Aug 7, 2017
034e37a
Fix: pull request feedback
Aug 7, 2017
8edb906
Update: remove todo
Aug 7, 2017
f79244b
New: drawing annotation undo and redo container
Aug 7, 2017
1fea19d
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 7, 2017
a1909fe
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 8, 2017
bb56ac4
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 9, 2017
d225a4a
Update: undo and redo grey out when applicable
Aug 9, 2017
6d0b4cc
Fix: update tests with new fn calls
Aug 9, 2017
b216523
Update: merge upstream master
Aug 9, 2017
bd25a6e
Chore: update variable name
Aug 9, 2017
08f44c1
Merge branch 'feature/drawingAnnotationScaling' of https://github.com…
Aug 9, 2017
41a5d51
Chore: actually update the draw states variable
Aug 9, 2017
5d05d52
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 10, 2017
06e4f24
Merge branch 'master' into feature/undoredo
Minh-Ng Aug 10, 2017
abdc1ac
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 10, 2017
55817f4
Update: drawing annotation svgs
Aug 11, 2017
1e31e26
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 11, 2017
c827c3e
Update: remove draw from drawstates
Aug 11, 2017
bf550bd
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 11, 2017
e44a151
Update: resolve conflicts from upstream
Aug 11, 2017
52642bf
Update: handle page change with two toggles
Aug 11, 2017
746d291
Update: merge from scaling pr
Aug 11, 2017
ee25fa4
Update: merge refactor from master
Aug 11, 2017
cffba79
Update: resolve merge
Aug 12, 2017
7f31e8c
Update: merge conflicts again please
Aug 12, 2017
0046949
Merge branch 'feature/drawingAnnotationScaling' into feature/undoredo
Aug 12, 2017
41287af
Fix: bind cleanup listeners on drawing thread and fix resize
Aug 12, 2017
1df105b
Fix: remove global test variable
Aug 12, 2017
94bf934
Merge branch 'feature/drawingAnnotationScaling' into feature/undoredo
Aug 12, 2017
2a39df7
Fix: scale annotations in redo stack
Aug 12, 2017
455549f
Update: drawing method cleanup
Aug 12, 2017
98d8e3b
Merge branch 'feature/undoredo' of https://github.com/MinhHNguyen/box…
Aug 12, 2017
fb0f5cb
Update: remove unused class
Aug 12, 2017
d8f9f80
Update: undo redo unit tests
Aug 13, 2017
6ac7878
Merge branch 'master' into feature/undoredo
Minh-Ng Aug 14, 2017
bad8368
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 14, 2017
2509bd6
Merge remote-tracking branch 'upstream/master' into feature/undoredo
Aug 14, 2017
08c24bc
Merge branch 'feature/undoredo' of https://github.com/MinhHNguyen/box…
Aug 14, 2017
94e92c0
Merge branch 'master' into feature/drawingAnnotationScaling
Minh-Ng Aug 14, 2017
ab8358b
Update: remove isTypeEnabled
Aug 14, 2017
ae4e82c
Update: merge from master
Aug 15, 2017
73aaa87
Merge branch 'feature/drawingAnnotationScaling' into feature/undoredo
Aug 15, 2017
cdf8b6b
Update: fix tests again
Aug 15, 2017
15e0e72
Merge branch 'feature/drawingAnnotationScaling' into feature/undoredo
Aug 15, 2017
2612395
Merge branch 'master' into feature/undoredo
Minh-Ng Aug 15, 2017
5bcc4a4
Update: resolve merge conflicts
Aug 15, 2017
d02ac11
Update: added test for DocDrawingThreadShow
Aug 15, 2017
c3d3ee5
Merge branch 'master' into feature/undoredo
Minh-Ng Aug 15, 2017
82012d1
Update: PR changes
Aug 15, 2017
a8a9be5
Update: custom thread cleanup for drawing annotationevent
Aug 15, 2017
e6e77fd
Merge branch 'master' into feature/undoredo
Minh-Ng Aug 15, 2017
53aed0e
Merge branch 'feature/undoredo' of https://github.com/MinhHNguyen/box…
Aug 15, 2017
45ce77b
New: drawing deletion
Aug 17, 2017
91bffcc
Update: resolve merge conflict
Aug 17, 2017
b369165
Update: updates
Aug 18, 2017
8c8e158
Update: resolve merge from master
Aug 21, 2017
303bb2d
Merge remote-tracking branch 'upstream/master' into feature/drawingDe…
Aug 23, 2017
d2eddee
Update: separate drawing methods
Aug 24, 2017
f9d083d
Merge remote-tracking branch 'upstream/master' into feature/drawingDe…
Aug 24, 2017
f85d2e8
Update: tests
Aug 24, 2017
75b021e
Merge branch 'master' into feature/drawingDeletion
Aug 25, 2017
c7b2b35
Update: complete tests
Aug 25, 2017
9d1ac3d
Update: Change how controllers are instantiated
Aug 29, 2017
b8fdc3d
Update: resolve merge conflict in BoxAnnotations
Aug 29, 2017
9402fa2
Merge remote-tracking branch 'upstream/master' into feature/drawingDe…
Aug 29, 2017
48e4bfb
Update: bind draw selection to annotated element
Aug 29, 2017
9268715
Merge branch 'master' into feature/drawingDeletion
Minh-Ng Aug 29, 2017
9314a41
Update: resolve merge conflict from master
Aug 29, 2017
d5f99a6
Merge branch 'master' into feature/drawingDeletion
Minh-Ng Aug 29, 2017
34ff020
Merge remote-tracking branch 'upstream/master' into feature/drawingDe…
Aug 29, 2017
d0c4427
Merge branch 'feature/drawingDeletion' of https://github.com/MinhHNgu…
Aug 29, 2017
4d94bf4
Update: pr updates
Aug 30, 2017
beb8150
Merge branch 'master' into feature/drawingDeletion
Minh-Ng Aug 30, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions src/lib/annotations/AnnotationController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import EventEmitter from 'events';

class AnnotationController extends EventEmitter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be clearer to label this AnnotationModeController since it only applies to annotation types that would enter a 'mode'. AKA not highlight annotations

/** @property {Array} - The array of annotation threads */
threads = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So would we be storing all the annotation threads both in the annotator and the controller then? Seems redundant to have it stored in 2 places

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal is to have each controller manage their respective type of threads. Right now we are at an in-between state where highlight and point annotation doesn't use a controller so we still need to store threads in the annotator and controller. In addition, the threadmap is in use in other functions that I did not want to modify.

The good news is that these data structures simply store a reference to the thread so the memory overhead isn't that bad. The downside is that the thread reference needs to be removed from both (this is accomplished by listening to 'threaddeleted').


/** @property {Array} - The array of annotation handlers */
handlers = [];

/**
* [constructor]
*
* @return {AnnotationController} Annotation controller instance
*/
constructor() {
super();

this.handleAnnotationEvent = this.handleAnnotationEvent.bind(this);
}

/**
* Register the annotator and any information associated with the annotator
*
* @public
* @param {Annotator} annotator - The annotator to be associated with the controller
* @return {void}
*/
registerAnnotator(annotator) {
// TODO (@minhnguyen): remove the need to register an annotator. Ideally, the annotator should know about the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// controller and the controller does not know about the annotator.
this.annotator = annotator;
}

/**
* Bind the mode listeners and store each handler for future unbinding
*
* @public
* @return {void}
*/
bindModeListeners() {
const handlers = this.setupAndGetHandlers();
handlers.forEach((handler) => {
const eventNames = handler.type.split(' ');
eventNames.forEach((eventName) => handler.eventObj.addEventListener(eventName, handler.func));
this.handlers.push(handler);
});
}

/**
* Unbind the previously bound mode listeners
*
* @public
* @return {void}
*/
unbindModeListeners() {
while (this.handlers.length > 0) {
const handler = this.handlers.pop();
const eventNames = handler.type.split(' ');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just save the event types as an array so you can just loop through them instead of parsing a string into an array

eventNames.forEach((eventName) => {
handler.eventObj.removeEventListener(eventName, handler.func);
});
}
}

/**
* Register a thread with the controller so that the controller can keep track of relevant threads
*
* @public
* @param {AnnotationThread} thread - The thread to register with the controller
* @return {void}
*/
registerThread(thread) {
this.threads.push(thread);
}

/**
* Unregister a previously registered thread
*
* @public
* @param {AnnotationThread} thread - The thread to unregister with the controller
* @return {void}
*/
unregisterThread(thread) {
this.threads = this.threads.filter((item) => item !== thread);
}

/**
* Binds custom event listeners for a thread.
*
* @protected
* @param {AnnotationThread} thread - Thread to bind events to
* @return {void}
*/
bindCustomListenersOnThread(thread) {
if (!thread) {
return;
}

// TODO (@minhnguyen): Move annotator.bindCustomListenersOnThread logic to AnnotationController
this.annotator.bindCustomListenersOnThread(thread);
thread.addListener('annotationevent', (data) => {
this.handleAnnotationEvent(thread, data);
});
}

/**
* Unbinds custom event listeners for the thread.
*
* @protected
* @param {AnnotationThread} thread - Thread to unbind events from
* @return {void}
*/
unbindCustomListenersOnThread(thread) {
if (!thread) {
return;
}

thread.removeAllListeners('threaddeleted');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure you can call this once without args and it will remove all listeners for the thread.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you're right about this, but there is a comment in annotator regarding calling unbindCustomListenersOnThread on 'threadcleanup':

// Thread should be cleaned up, unbind listeners - we don't do
// in threaddeleted listener since thread may still need to respond
// to error messages

thread.removeAllListeners('threadcleanup');
thread.removeAllListeners('annotationsaved');
thread.removeAllListeners('annotationevent');
}

/**
* Set up and return the necessary handlers for the annotation mode
*
* @protected
* @return {Array} An array where each element is an object containing the object that will emit the event,
* the type of events to listen for, and the callback
*/
setupAndGetHandlers() {}

/**
* Handle an annotation event.
*
* @protected
* @param {AnnotationThread} thread - The thread that emitted the event
* @param {Object} data - Extra data related to the annotation event
* @return {void}
*/
/* eslint-disable no-unused-vars */
handleAnnotationEvent(thread, data = {}) {}
/* eslint-enable no-unused-vars */
}

export default AnnotationController;
4 changes: 3 additions & 1 deletion src/lib/annotations/AnnotationThread.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ class AnnotationThread extends EventEmitter {

// If this annotation was the last one in the thread, destroy the thread
} else if (this.annotations.length === 0 || annotatorUtil.isPlainHighlight(this.annotations)) {
if (this.isMobile) {
if (this.isMobile && this.dialog) {
this.dialog.removeAnnotation(annotationID);
this.dialog.hideMobileDialog();
}
Expand Down Expand Up @@ -411,6 +411,8 @@ class AnnotationThread extends EventEmitter {
this.dialog.addAnnotation(savedAnnotation);
this.dialog.removeAnnotation(tempAnnotation.annotationID);
}

this.emit('annotationsaved');
}

/**
Expand Down
133 changes: 41 additions & 92 deletions src/lib/annotations/Annotator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import EventEmitter from 'events';
import autobind from 'autobind-decorator';
import AnnotationService from './AnnotationService';
import DrawingController from './drawing/DrawingController';
import * as annotatorUtil from './annotatorUtil';
import { ICON_CLOSE } from '../icons/icons';
import './Annotator.scss';
Expand Down Expand Up @@ -84,7 +85,10 @@ class Annotator extends EventEmitter {
Object.keys(this.modeButtons).forEach((type) => {
const handler = this.getAnnotationModeClickHandler(type);
const buttonEl = this.container.querySelector(this.modeButtons[type].selector);
buttonEl.removeEventListener('click', handler);

if (buttonEl) {
buttonEl.removeEventListener('click', handler);
}
});

this.unbindDOMListeners();
Expand Down Expand Up @@ -167,6 +171,11 @@ class Annotator extends EventEmitter {

const handler = this.getAnnotationModeClickHandler(currentMode);
annotateButtonEl.addEventListener('click', handler);

// TODO (@minhnguyen): Implement controller for point mode annotation and remove this check
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if (mode.controller && mode.controller.constructor.name === DrawingController.name) {
mode.controller.registerAnnotator(this);
}
}
}

Expand Down Expand Up @@ -536,6 +545,17 @@ class Annotator extends EventEmitter {
// Bind events on valid annotation thread
const thread = this.createAnnotationThread(annotations, firstAnnotation.location, firstAnnotation.type);
this.bindCustomListenersOnThread(thread);

const modeData = this.modeButtons[firstAnnotation.type];
if (
modeData &&
modeData.controller &&
modeData.controller.constructor.name === DrawingController.name
) {
modeData.controller.bindCustomListenersOnThread(thread);
modeData.controller.registerThread(thread);
this.addThreadToMap(thread);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remind me why we need to bind the thread so early and not on mode entry?

Copy link
Contributor Author

@Minh-Ng Minh-Ng Aug 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This particular binding is done on AnnotationThreads fetched from the server. This binding allows the annotator to react to the deletion of saved AnnotationThreads.

}
});

this.emit('annotationsfetched');
Expand Down Expand Up @@ -667,6 +687,7 @@ class Annotator extends EventEmitter {
unbindCustomListenersOnThread(thread) {
thread.removeAllListeners('threaddeleted');
thread.removeAllListeners('threadcleanup');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm right about removeAllListeners then it applies here as well.

thread.removeAllListeners('annotationsaved');
thread.removeAllListeners('annotationevent');
}

Expand Down Expand Up @@ -694,98 +715,12 @@ class Annotator extends EventEmitter {
}
);
} else if (mode === TYPES.draw) {
const drawingThread = this.createAnnotationThread([], {}, TYPES.draw);
this.bindCustomListenersOnThread(drawingThread);

/* eslint-disable require-jsdoc */
const locationFunction = (event) => this.getLocationFromEvent(event, TYPES.point);
/* eslint-enable require-jsdoc */

const postButtonEl = this.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_DRAW_POST);
const undoButtonEl = this.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_DRAW_UNDO);
const redoButtonEl = this.getAnnotateButton(SELECTOR_ANNOTATION_BUTTON_DRAW_REDO);

// NOTE (@minhnguyen): Move this logic to a new controller class
const that = this;
drawingThread.addListener('annotationevent', (data = {}) => {
switch (data.type) {
case 'drawcommit':
drawingThread.removeAllListeners('annotationevent');
break;
case 'pagechanged':
drawingThread.saveAnnotation(TYPES.draw);
that.unbindModeListeners();
that.bindModeListeners(TYPES.draw);
break;
case 'availableactions':
if (data.undo === 1) {
annotatorUtil.enableElement(undoButtonEl);
} else if (data.undo === 0) {
annotatorUtil.disableElement(undoButtonEl);
}

if (data.redo === 1) {
annotatorUtil.enableElement(redoButtonEl);
} else if (data.redo === 0) {
annotatorUtil.disableElement(redoButtonEl);
}
break;
default:
}
});

handlers.push(
{
type: 'mousemove',
func: annotatorUtil.eventToLocationHandler(locationFunction, drawingThread.handleMove),
eventObj: this.annotatedElement
},
{
type: 'mousedown',
func: annotatorUtil.eventToLocationHandler(locationFunction, drawingThread.handleStart),
eventObj: this.annotatedElement
},
{
type: 'mouseup',
func: annotatorUtil.eventToLocationHandler(locationFunction, drawingThread.handleStop),
eventObj: this.annotatedElement
}
);

if (postButtonEl) {
handlers.push({
type: 'click',
func: () => {
drawingThread.saveAnnotation(mode);
this.toggleAnnotationHandler(mode);
},
eventObj: postButtonEl
});
}

if (undoButtonEl) {
handlers.push({
type: 'click',
func: () => {
drawingThread.undo();
},
eventObj: undoButtonEl
});
}

if (redoButtonEl) {
handlers.push({
type: 'click',
func: () => {
drawingThread.redo();
},
eventObj: redoButtonEl
});
}
const controller = this.modeButtons[TYPES.draw].controller;
controller.bindModeListeners();
}

handlers.forEach((handler) => {
handler.eventObj.addEventListener(handler.type, handler.func);
handler.eventObj.addEventListener(handler.type, handler.func, false);
this.annotationModeHandlers.push(handler);
});
}
Expand Down Expand Up @@ -836,12 +771,26 @@ class Annotator extends EventEmitter {
* Unbinds event listeners for annotation modes.
*
* @protected
* @param {string} mode - Annotation mode to be unbound
* @return {void}
*/
unbindModeListeners() {
unbindModeListeners(mode) {
while (this.annotationModeHandlers.length > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the difference between removing these listeners vs. calling CONTROLLERS[mode]. unbindModeListeners()? Does this part just remove point annotation listeners (I assume until we create a modeController for point annotation mode as well)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part delegates the unbinding to the controller rather than the annotator. Meaning once a point annotation controller is in (and preferably highlight as well) this call just needs to be a call to controller[mode].unbindModeListeners()

const handler = this.annotationModeHandlers.pop();
handler.eventObj.removeEventListener(handler.type, handler.func);
const eventNames = handler.type.split(' ');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these for different events then the unbind func on line 55 of annotationController?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was initially checking out a way to bind the same listener to multiple events ie. 'mousedown', 'touchstart' by simply adding it to the string in bindModeListeners.

This is done in AnnotationController (see drawingcontroller line 119 for an example) and doesn't need to be in annotator. I'll remove this.

eventNames.forEach((eventName) => {
handler.eventObj.removeEventListener(eventName, handler.func);
});
}

if (
mode &&
this.modeButtons &&
this.modeButtons[mode] &&
this.modeButtons[mode].controller &&
this.modeButtons[mode].controller.constructor.name === DrawingController.name
) {
this.modeButtons[mode].controller.unbindModeListeners();
}
}

Expand Down
21 changes: 21 additions & 0 deletions src/lib/annotations/Annotator.scss
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,27 @@ $avatar-color-9: #f22c44;
width: 100%;
}

//------------------------------------------------------------------------------
// Draw annotation mode
//------------------------------------------------------------------------------
.bp-annotation-draw-boundary {
animation: dash 1s linear infinite;
fill: none;
stroke: rgb(0, 0, 0);
stroke-dasharray: 5;
stroke-width: 3px;
}

@keyframes dash {
from {
stroke-dashoffset: 10;
}

to {
stroke-dashoffset: 0;
}
}

//------------------------------------------------------------------------------
// Annotation mode
//------------------------------------------------------------------------------
Expand Down
Loading