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

Feature: drawing annotation deletion #337

merged 108 commits into from
Aug 30, 2017

Conversation

Minh-Ng
Copy link
Contributor

@Minh-Ng Minh-Ng commented Aug 25, 2017

Unfortunately I didn't have the foresight to split selection and deletion into separate pull requests, but ~900 lines are tests.

  • I'm proposing the ability to pass a controller into the annotation object (see BaseViewer.js for an example). This will make more sense with custom annotations in the sense that only a controller needs to be accounted for within the official library.
  • The controller will be able to take over some of the responsibilities currently done in annotator. Annotator should be able to be made more generic as any annotation type specific logic should be delegated to a controller. Eventually the controller should not need to know about the annotator. Instead should emit events and the annotator should observe these events.
  • Deletion is currently done upon selecting a drawing twice. Next up is to make UI for selection and deletion. An additional improvement is to use two canvases for hit detection so that it can be done in O(1) time, allowing selection to be done on hover without a huge loss of performance.

Minh Nguyen and others added 30 commits July 25, 2017 21:36
@@ -3,6 +3,8 @@ import EventEmitter from 'events';
import debounce from 'lodash.debounce';
import fullscreen from '../Fullscreen';
import RepStatus from '../RepStatus';
// TODO (@minhnguyen): Import controller from annotations npm module once the library is separate
import DrawingController from '../annotations/drawing/DrawingController';
Copy link
Contributor

Choose a reason for hiding this comment

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

I really want to avoid calling annotation specific code in BaseViewer. Is it possible to move this into Annotator or BoxAnnotations?

Copy link
Contributor

Choose a reason for hiding this comment

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

Unsure if it makes sense to allow external controllers. Any other annotation type that gets added would be added through the annotations codebase, so any needed controllers would also be added in the annotations codebase.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, I'll move it into annotator as discussed.

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

) {
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.

@@ -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.

while (this.annotationModeHandlers.length > 0) {
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.

@@ -436,7 +437,9 @@ class DocAnnotator extends Annotator {

if (this.hasTouch && this.isMobile) {
document.addEventListener('selectionchange', this.onSelectionChange);
document.addEventListener('touchstart', this.drawingSelectionHandler);
Copy link
Contributor

Choose a reason for hiding this comment

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

can we add these to the annotated element 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.

I'm not quite sure. I will test it out but it might be the case that mobile browsers are functioning a bit differently.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just tested it out, looks like highlight selection needs to be bound to document but the drawing selection can be bound to annotated element.

* @param {Object} data - Extra data related to the annotation event
* @return {void}
*/
handleAnnotationEvent(thread, data = {}) {
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 on the comments here, nice.

}

const eventBoundary = {
minX: +location.x - DRAW_BORDER_OFFSET,
Copy link
Contributor

Choose a reason for hiding this comment

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

What do these +'s do?

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.

Sometimes location.x/y are strings (I suspect this is when they are returned from getLocation) and + (unary plus operator) ensures they are parsed as numbers. We can also use parseFloat() here if that is more clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It looks like +number is actually the fastest. https://jsperf.com/number-vs-plus-vs-toint-vs-tofloat/16

return;
}

// Randomly select a thread in case there are multiple
Copy link
Contributor

Choose a reason for hiding this comment

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

o_0

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 will change with better hit detection 👍 (using a hitmap canvas)

}

/**
* Select the indicated drawing thread. Deletes a drawing thread upon the second consecutive selection
Copy link
Contributor

Choose a reason for hiding this comment

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

This will change once we introduce UI, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, with the UI, a selection should show the GUI. A deletion will be done by clicking delete on the GUI option.

*/
updateUndoRedoButtonEls(undoCount, redoCount) {
if (this.undoButtonEl) {
if (undoCount === 1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can these numbers be > 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, in which case we don't need to do anything because they should already be enabled. In theory, the undo and redo counts will always start at 0. When they hit 1 they will be enabled and stay enabled for values > 1. If they drop to 0 they will return to the starting state (of being disabled).

@@ -3,6 +3,8 @@ import EventEmitter from 'events';
import debounce from 'lodash.debounce';
import fullscreen from '../Fullscreen';
import RepStatus from '../RepStatus';
// TODO (@minhnguyen): Import controller from annotations npm module once the library is separate
import DrawingController from '../annotations/drawing/DrawingController';
Copy link
Contributor

Choose a reason for hiding this comment

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

Unsure if it makes sense to allow external controllers. Any other annotation type that gets added would be added through the annotations codebase, so any needed controllers would also be added in the annotations codebase.

* @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.

👍

// Get handlers
handlers.push(
{
type: 'mousemove touchmove',
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe store this as type: ['mousemove', 'touchmove']

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


class AnnotationController extends EventEmitter {
/** @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').

@@ -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.

👍

@@ -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

@Minh-Ng
Copy link
Contributor Author

Minh-Ng commented Aug 29, 2017

  • Changed controller names
  • Controllers instantiated inside of BoxAnnotations and loaded through annotatorConf
  • Handler array uses an array of event names rather than one string being parsed
  • Bind selection handler to annotator.annotatedElement

Copy link
Contributor

@pramodsum pramodsum left a comment

Choose a reason for hiding this comment

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

🎉

const handlers = this.setupAndGetHandlers();
handlers.forEach((handler) => {
const types = handler.type instanceof Array ? handler.type : [handler.type];
types.forEach((eventName) => handler.eventObj.addEventListener(eventName, handler.func));
Copy link
Contributor

Choose a reason for hiding this comment

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

new line before forEach and maybe style it like this

types.forEach((eventName) => {
...
});

while (this.handlers.length > 0) {
const handler = this.handlers.pop();
const types = handler.type instanceof Array ? handler.type : [handler.type];
types.forEach((eventName) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

new line before forEach

@@ -167,6 +170,11 @@ class Annotator extends EventEmitter {

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

const { CONTROLLERS } = this.options.annotator;
Copy link
Contributor

Choose a reason for hiding this comment

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

What if this.options.annotator is null?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also might make sense to store each valid mode controller as this.modeControllersso you're not constantly checking if this.options.annotator and CONTROLLERS exist

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.options.annotator can't be null because of the isModeAnnotatable check above

* @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()

annotatorConfig.CONTROLLERS = {};
annotatorConfig.TYPE.forEach((type) => {
if (type in ANNOTATOR_TYPE_CONTROLLERS) {
annotatorConfig.CONTROLLERS[type] = new ANNOTATOR_TYPE_CONTROLLERS[type]();
Copy link
Contributor

Choose a reason for hiding this comment

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

annotatorConfig.CONTROLLERS[type] = new ANNOTATOR_TYPE_CONTROLLERS[type].CONSTRUCTOR();

* @param {Object} location - The location information of the pointer
* @return {void}
*/
handleMove(location) {
if (this.drawingFlag !== DRAW_STATES.drawing) {
if (this.drawingFlag !== DRAW_STATES.drawing || !location) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this not have a valid location at some point?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would not have a valid location upon moving the mouse on the side of the document.

*
* @return {Object} The object with the boundaries and the path
*/
getAABB() {
Copy link
Contributor

Choose a reason for hiding this comment

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

getAxisAlignedBoundingBox() or getAlignedBoundingBox()

}
);

if (this.postButtonEl) {
Copy link
Contributor

Choose a reason for hiding this comment

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

move these into their own helper method and just call it for each button. May even make sense to move it to the base class!

addButtonHandlers(buttonEl, handler) {
  if (buttonEl) {
    const buttonHandler = {
      type: 'click,
      func: handler, 
      eventObj: buttonEl
    };
    handlers.push(buttonHandler);
}

};

// Get the threads that correspond to the point that was clicked on
const intersectingThreads = this.threads
Copy link
Contributor

Choose a reason for hiding this comment

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

is drawController.threads an array?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is an array by default (AnnotationModeController) and an RTree in DrawingModeController


// Calculate the bounding rectangle
const [x, y, width, height] = this.getRectangularBoundary();
// Clear the drawn thread and destroy it
Copy link
Contributor

Choose a reason for hiding this comment

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

new line before comment

@Minh-Ng Minh-Ng merged commit 34b9da1 into box:master Aug 30, 2017
@Minh-Ng Minh-Ng deleted the feature/drawingDeletion branch August 30, 2017 23:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants