Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
5c13e4ad
Commit
5c13e4ad
authored
Sep 29, 2020
by
Lyza Danger Gardner
Committed by
Lyza Gardner
Oct 07, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Migrate algorithm for building buckets from points
parent
91029821
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
215 additions
and
15 deletions
+215
-15
bucket-bar-js.js
src/annotator/plugin/bucket-bar-js.js
+135
-13
bucket-bar.coffee
src/annotator/plugin/bucket-bar.coffee
+7
-2
bucket-bar-js-test.js
src/annotator/plugin/test/bucket-bar-js-test.js
+73
-0
No files found.
src/annotator/plugin/bucket-bar-js.js
View file @
5c13e4ad
...
...
@@ -4,11 +4,38 @@ import { getBoundingClientRect } from '../highlighter';
* @typedef {import('../../types/annotator').Anchor} Anchor
*/
/**
* A structured Array representing either the top (`startOrEnd` = 1) or the
* bottom (`startOrEnd` = -1) of an anchor's highlight bounding box.
*
* @typedef {[pixelPosition: number, startOrEnd: (-1 | 1), anchor: Anchor]} PositionPoint
*/
/**
* An object containing information about anchor highlight positions
*
* @typedef PositionPoints
* @prop {Anchor[]} above - Anchors that are offscreen above
* @prop {Anchor[]} below - Anchors that are offscreen below
* @prop {PositionPoint[]} points - PositionPoints for on-screen anchor
* highlights. Each highlight box has 2 PositionPoints (one for top edge
* and one for bottom edge).
*/
/**
* @typedef BucketInfo
* @prop {Array<Anchor[]>} buckets
* @prop {number[]} index - Array of (pixel) positions of each bucket
*/
// FIXME: Temporary duplication of size constants between here and BucketBar
const
BUCKET_SIZE
=
16
;
// Regular bucket size
const
BUCKET_NAV_SIZE
=
BUCKET_SIZE
+
6
;
// Bucket plus arrow (up/down)
const
BUCKET_TOP_THRESHOLD
=
115
+
BUCKET_NAV_SIZE
;
// Toolbar
// TODO!! This is an option in the plugin right now
const
BUCKET_GAP_SIZE
=
60
;
/**
* Find the closest valid anchor in `anchors` that is offscreen in the direction
* indicated.
...
...
@@ -59,19 +86,6 @@ export function findClosestOffscreenAnchor(anchors, direction) {
return
closestAnchor
;
}
/**
* A structured Array representing either the top or the bottom of an anchor's
* highlight-box position.
* @typedef {[number, (-1 | 1), Anchor]} PositionPoint
*/
/**
* @typedef PositionPoints
* @prop {Anchor[]} above - Anchors that are offscreen above
* @prop {Anchor[]} below - Anchors that are offscreen below
* @prop {PositionPoint[]} points - Points representing the tops and bottoms
* of on-screen anchor highlight boxes
*/
/**
* Construct an Array of points representing the positional tops and bottoms
* of current anchor highlights. Each anchor whose highlight(s)' bounding
...
...
@@ -124,3 +138,111 @@ export function constructPositionPoints(anchors) {
points
,
};
}
/**
* Take a sorted set of `points` representing top and bottom positions of anchor
* highlights and group them into a collection of "buckets".
*
* @param {PositionPoint[]} points
* @return {BucketInfo}
*/
export
function
buildBuckets
(
points
)
{
const
buckets
=
/** @type {Array<Anchor[]>} */
(
new
Array
());
const
bucketPositions
=
/** @type {number[]} */
(
new
Array
());
// Anchors that are part of the currently-being-built bucket, and a correspon-
// ding count of unclosed top edges seen for that anchor
const
current
=
/** @type {{anchors: Anchor[], counts: number[] }} */
({
anchors
:
new
Array
(),
counts
:
new
Array
(),
});
points
.
forEach
((
point
,
index
)
=>
{
const
[
position
,
delta
,
anchor
]
=
point
;
// Does this point represent the top or the bottom of an anchor's highlight
// box?
const
positionType
=
delta
>
0
?
'start'
:
'end'
;
// See if this point's anchor is already in our working set of open anchors
const
anchorIndex
=
current
.
anchors
.
indexOf
(
anchor
);
if
(
positionType
===
'start'
)
{
if
(
anchorIndex
===
-
1
)
{
// Add an entry for this anchor to our current set of "open" anchors
current
.
anchors
.
unshift
(
anchor
);
current
.
counts
.
unshift
(
1
);
}
else
{
// Increment the number of times we've seen a start/top edge for this
// anchor
current
.
counts
[
anchorIndex
]
++
;
}
}
else
{
// positionType = 'end'
// This is the bottom/end of an anchor that we should have already seen
// a top edge for. Decrement the count, representing that we've found an
// end point to balance a previously-seen start point
current
.
counts
[
anchorIndex
]
--
;
if
(
current
.
counts
[
anchorIndex
]
===
0
)
{
// All start points for this anchor have been balanced by end point(s)
// So we can remove this anchor from our collection of open anchors
current
.
anchors
.
splice
(
anchorIndex
,
1
);
current
.
counts
.
splice
(
anchorIndex
,
1
);
}
}
// For each point, we'll either:
// * create a new bucket: Add a new bucket (w/corresponding bucket position)
// and add the working anchors to the new bucket. This, of course, has
// the effect of making the buckets collection larger. OR:
// * merge buckets: In most cases, merge the anchors from the last bucket
// into the penultimate (previous) bucket and remove the last bucket (and
// its corresponding `bucketPosition` entry). Also add the working anchors
// to the previous bucket. Note that this decreases the size of the
// buckets collection.
// The ultimate set of buckets is defined by the pattern of creating and
// merging/removing buckets as we iterate over points.
const
isFirstOrLastPoint
=
bucketPositions
.
length
===
0
||
index
===
points
.
length
-
1
;
const
isLargeGap
=
bucketPositions
.
length
&&
position
-
bucketPositions
[
bucketPositions
.
length
-
1
]
>
BUCKET_GAP_SIZE
;
if
(
current
.
anchors
.
length
===
0
||
isFirstOrLastPoint
||
isLargeGap
)
{
// Create a new bucket, because:
// - There are no more open/working anchors, OR
// - This is the first or last point, OR
// - There's been a large dimensional gap since the last bucket's position
buckets
.
push
(
current
.
anchors
.
slice
());
// Each bucket gets a corresponding entry in `bucketPositions` for the
// pixel position of its eventual indicator in the bucket bar
bucketPositions
.
push
(
position
);
}
else
{
// Merge buckets
// We will remove 2 (usually) or 1 (if there is only one) bucket
// from the buckets collection, and re-add 1 merged bucket (always)
// Always pop off the last bucket
const
ultimateBucket
=
buckets
.
pop
()
||
new
Array
();
// If there is a previous/penultimate bucket, pop that off, as well
let
penultimateBucket
=
new
Array
();
if
(
buckets
[
buckets
.
length
-
1
]?.
length
)
{
penultimateBucket
=
buckets
.
pop
()
||
new
Array
();
// Because we're removing two buckets but only re-adding one below,
// we'll end up with a misalignment in the `bucketPositions` collection.
// Remove the last entry here, as it corresponds to the ultimate bucket,
// which won't be re-added in its present form
bucketPositions
.
pop
();
}
// Create a merged bucket from the anchors in the penultimate bucket
// (when available), ultimate bucket and current working anchors
const
activeBucket
=
Array
.
from
(
new
Set
([...
penultimateBucket
,
...
ultimateBucket
,
...
current
.
anchors
])
);
// Push the now-merged bucket onto the buckets collection
buckets
.
push
(
activeBucket
);
}
});
return
{
buckets
,
index
:
bucketPositions
};
}
src/annotator/plugin/bucket-bar.coffee
View file @
5c13e4ad
...
...
@@ -3,7 +3,7 @@ $ = require('jquery')
scrollIntoView
=
require
(
'scroll-into-view'
)
{
findClosestOffscreenAnchor
,
constructPositionPoints
}
=
require
(
'./bucket-bar-js'
)
{
findClosestOffscreenAnchor
,
constructPositionPoints
,
buildBuckets
}
=
require
(
'./bucket-bar-js'
)
highlighter
=
require
(
'../highlighter'
)
...
...
@@ -82,6 +82,7 @@ module.exports = class BucketBar extends Delegator
_update
:
->
{
above
,
below
,
points
}
=
constructPositionPoints
(
@
annotator
.
anchors
)
fakeBuckets
=
buildBuckets
(
points
)
# Accumulate the overlapping anchors into buckets.
# The algorithm goes like this:
...
...
@@ -134,8 +135,8 @@ module.exports = class BucketBar extends Delegator
else
last
=
buckets
[
buckets
.
length
-
1
]
toMerge
=
[]
last
.
push
a0
for
a0
in
carry
.
anchors
when
a0
not
in
last
last
.
push
a0
for
a0
in
toMerge
when
a0
not
in
last
last
.
push
a0
for
a0
in
carry
.
anchors
when
a0
not
in
last
{
buckets
,
index
,
carry
}
,
...
...
@@ -146,6 +147,10 @@ module.exports = class BucketBar extends Delegator
counts
:
[]
latest
:
0
@
buckets
.
forEach
(
bucket
,
bi
)
=>
console
.
assert
(
@
index
[
bi
]
is
fakeBuckets
.
index
[
bi
],
'index positions are equal'
)
bucket
.
forEach
(
anchor
,
ai
)
=>
console
.
assert
(
anchor
is
fakeBuckets
.
buckets
[
bi
][
ai
],
'anchors are the same'
,
anchor
.
annotation
.
$tag
,
fakeBuckets
.
buckets
[
bi
][
ai
].
annotation
.
$tag
)
# Scroll up
@
buckets
.
unshift
[],
above
,
[]
@
index
.
unshift
0
,
BUCKET_TOP_THRESHOLD
-
1
,
BUCKET_TOP_THRESHOLD
...
...
src/annotator/plugin/test/bucket-bar-js-test.js
View file @
5c13e4ad
import
{
findClosestOffscreenAnchor
,
constructPositionPoints
,
buildBuckets
,
}
from
'../bucket-bar-js'
;
import
{
$imports
}
from
'../bucket-bar-js'
;
...
...
@@ -199,4 +200,76 @@ describe('annotator/plugin/bucket-bar-js', () => {
}
});
});
describe
(
'buildBuckets'
,
()
=>
{
it
(
'should return empty buckets if points array is empty'
,
()
=>
{
const
bucketInfo
=
buildBuckets
([]);
assert
.
isArray
(
bucketInfo
.
buckets
);
assert
.
isEmpty
(
bucketInfo
.
buckets
);
assert
.
isEmpty
(
bucketInfo
.
index
);
});
it
(
'should group overlapping anchor highlights into shared buckets'
,
()
=>
{
const
anchors
=
[{},
{},
{},
{}];
const
points
=
[];
// Represents points for 4 anchors that all have a top of 150px and bottom
// of 200
anchors
.
forEach
(
anchor
=>
{
points
.
push
([
150
,
1
,
anchor
]);
points
.
push
([
200
,
-
1
,
anchor
]);
});
const
buckets
=
buildBuckets
(
points
);
assert
.
equal
(
buckets
.
buckets
.
length
,
2
);
assert
.
isEmpty
(
buckets
.
buckets
[
1
]);
// All anchors are in a single bucket
assert
.
deepEqual
(
buckets
.
buckets
[
0
],
anchors
);
// Because this is the first bucket, it will be aligned top
assert
.
equal
(
buckets
.
index
[
0
],
150
);
});
it
(
'should group nearby anchor highlights into shared buckets'
,
()
=>
{
let
increment
=
25
;
const
anchors
=
[{},
{},
{},
{}];
const
points
=
[];
// Represents points for 4 anchors that all have different start and
// end positions, but only differing by 25px
anchors
.
forEach
(
anchor
=>
{
points
.
push
([
150
+
increment
,
1
,
anchor
]);
points
.
push
([
200
+
increment
,
-
1
,
anchor
]);
increment
+=
25
;
});
const
buckets
=
buildBuckets
(
points
);
assert
.
equal
(
buckets
.
buckets
.
length
,
2
);
assert
.
isEmpty
(
buckets
.
buckets
[
1
]);
// All anchors are in a single bucket
assert
.
deepEqual
(
buckets
.
buckets
[
0
],
anchors
);
// Because this is the first bucket, it will be aligned top
assert
.
equal
(
buckets
.
index
[
0
],
175
);
});
it
(
'should put anchors that are not near each other in separate buckets'
,
()
=>
{
let
position
=
100
;
const
anchors
=
[{},
{},
{},
{}];
const
points
=
[];
// Represents points for 4 anchors that all have different start and
// end positions, but only differing by 25px
anchors
.
forEach
(
anchor
=>
{
points
.
push
([
position
,
1
,
anchor
]);
points
.
push
([
position
+
20
,
-
1
,
anchor
]);
position
+=
100
;
});
const
buckets
=
buildBuckets
(
points
);
assert
.
equal
(
buckets
.
buckets
.
length
,
8
);
// Legacy of previous implementation, shrug?
assert
.
isEmpty
(
buckets
.
buckets
[
1
]);
assert
.
isEmpty
(
buckets
.
buckets
[
3
]);
assert
.
isEmpty
(
buckets
.
buckets
[
5
]);
assert
.
isEmpty
(
buckets
.
buckets
[
7
]);
assert
.
deepEqual
(
buckets
.
buckets
[
0
],
[
anchors
[
0
]]);
assert
.
deepEqual
(
buckets
.
buckets
[
2
],
[
anchors
[
1
]]);
assert
.
deepEqual
(
buckets
.
buckets
[
4
],
[
anchors
[
2
]]);
assert
.
deepEqual
(
buckets
.
buckets
[
6
],
[
anchors
[
3
]]);
});
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment