1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
8 * Orientation of a line.
17 * Map from keysetId to layout.
18 * @type {Map<String,KeysetLayout>}
24 * Container for storing a keyset's layout.
26 var KeysetLayout = function() {
30 KeysetLayout
.prototype = {
32 * All keys in the keyset.
38 * Spacial partitioning of all keys in the keyset.
39 * @type {DecisionNode}
44 * Add a key to the keyset.
51 * Regenerates a decision tree using the keys in the keyset.
53 regenerateTree: function() {
54 // Split using horizontal lines first, as keyboards tend to be
56 var splits
= findSplits(this.keys
, Orientation
.HORIZONTAL
);
57 this.tree
= createBinaryTree(0, splits
.length
- 1, splits
);
59 this.tree
.populate(this.keys
);
63 * Searches the tree for the key closest to the point provided.
64 * @param {number} x The x-coordinate.
65 * @param {number} y The y-coordinate.
66 * @return {?kb-key} The key, or null if none found.
68 findClosestKey: function(x
, y
) {
69 var closestNode
= this.tree
.findClosestNode(x
, y
);
70 var key
= closestNode
.data
;
73 // Ignore touches that aren't close.
74 return key
.distanceTo(x
,y
) <= MAX_TOUCH_FUZZ_DISTANCE
?
79 * Returns the position data of all keys in this keyset.
80 * @return {Array<Map>}
82 getLayout: function() {
83 return this.keys
.map(function(key
) {
90 * Container for caching a key's data.
91 * @param {Object} key The key to cache.
92 * @param {number} left The x-coordinate of the left edge of the key.
93 * @param {number} top The y-coordinate of the top edge of the key.
94 * @param {number} width The width of the key in px.
95 * @param {number} height The height of the key in px.
97 var Key = function(key
) {
99 var style
= key
.style
;
100 this.top
= parseFloat(style
.top
) - KEY_PADDING_TOP
;
101 this.left
= parseFloat(style
.left
);
102 this.right
= this.left
+ parseFloat(style
.width
);
103 this.bottom
= this.top
+ parseFloat(style
.height
) + KEY_PADDING_TOP
104 + KEY_PADDING_BOTTOM
;
109 * Manhattan distance from the the provided point to the key.
110 * @param {number} x The x-coordinate of the point.
111 * @param {number} y The y-coordinate of the point.
114 distanceTo: function (x
, y
) {
115 return Math
.abs(this.intersect(new Line(x
))) +
116 Math
.abs(this.intersect(new Line(y
, true)));
120 * Checks whether the key intersects with the line provided.
121 * @param {!Line} line The line.
122 * @return {number} Zero if it intersects, signed manhattan distance if it
125 intersect: function(line
) {
126 var min
= line
.rotated
? this.top
: this.left
;
127 var max
= line
.rotated
? this.bottom
: this.right
;
128 return (line
.c
> max
) ? line
.c
- max
: Math
.min(0, line
.c
- min
);
132 * Returns the Key as a map.
133 * @return {Map<String,number>}
139 'width': this.right
- this.left
,
140 'height': this.bottom
- this.bottom
,
146 * Object representing the line y = c or x = c.
147 * @param {number} The x or y coordinate of the intersection line depending on
149 * @param {Orientation} orientation The orientation of the line.
151 var Line = function(c
, orientation
) {
153 this.rotated
= orientation
;
158 * The position of the provided point in relation to the line.
159 * @param {number} x The x-coordinate of the point.
160 * @param {number} y The y-coordinate of the point.
161 * @return {number} Zero if they intersect, negative if the point is before
162 * the line, positive if it's after.
164 testPoint: function (x
, y
) {
165 var c
= this.rotated
? y
: x
;
166 return this.c
== c
? 0 : c
- this.c
;
169 test: function (key
) {
170 // Key already provides an intersect method. If the key is to the right of
171 // the line, then the line is to the left of the key.
172 return -1 * key
.intersect(this);
177 * A node used to split 2D space.
178 * @param {Line} The line to split the space with.
180 var DecisionNode = function(line
) {
181 this.decision
= line
;
184 DecisionNode
.prototype = {
186 * The test whether to proceed in the left or right branch.
192 * The branch for nodes that failed the decision test.
193 * @type {?DecisionNode}
198 * The branch for nodes that passed the decision test.
199 * @type {?DecisionNode}
204 * Finds the node closest to the point provided.
205 * @param {number} x The x-coordinate.
206 * @param {number} y The y-coordinate.
207 * @return {DecisionNode | LeafNode}
209 findClosestNode: function(x
, y
) {
210 return this.search(function(node
){
211 return node
.decision
.testPoint(x
,y
) >= 0;
216 * Populates the decision tree with elements.
217 * @param {Array{Key}} The child elements.
219 populate: function(data
) {
224 for (var i
= 0; i
< data
.length
; i
++) {
225 var result
= this.decision
.test(data
[i
]);
226 // Add to both branches if result == 0.
232 var currentRotation
= this.decision
.rotated
;
234 * Splits the tree further such that each leaf has exactly one data point.
235 * @param {Array} array The data points.
236 * @return {DecisionNode | LeafNode} The new branch for the tree.
238 var updateBranch = function(array
) {
239 if (array
.length
== 1) {
240 return new LeafNode(array
[0]);
242 var splits
= findSplits(array
, !currentRotation
)
243 var tree
= createBinaryTree(0, splits
.length
- 1, splits
);
244 tree
.populate(array
);
248 // All elements that passed the decision test.
249 if (pass
.length
> 0) {
251 this.pass
.populate(pass
);
253 this.pass
= updateBranch(pass
);
255 // All elements that failed the decision test.
256 if (fail
.length
> 0) {
258 this.fail
.populate(fail
);
260 this.fail
= updateBranch(fail
);
265 * Searches for the first leaf that matches the search function.
266 * @param {Function<DecisionNode>: Boolean} searchFn The function used to
267 * determine whether to search in the left or right subtree.
268 * @param {DecisionNode | LeafNode} The node that most closely matches the
271 search: function(searchFn
) {
272 if (searchFn(this)) {
273 return this.pass
? this.pass
.search(searchFn
) : this;
275 return this.fail
? this.fail
.search(searchFn
) : this;
279 * Tests whether the key belongs in the left or right branch of this node.
280 * @param {Key} key The key being tested.
281 * @return {boolean} Whether it belongs in the right branch.
283 test: function(key
) {
284 return this.decision
.testKey(key
)
289 * Structure representing a leaf in the decision tree. It contains a single
292 var LeafNode = function(data
) {
296 LeafNode
.prototype = {
303 * Converts the array to a binary tree.
304 * @param {number} start The start index.
305 * @param {number} end The end index.
306 * @param {Array} nodes The array to convert.
307 * @return {DecisionNode}
309 var createBinaryTree = function(start
, end
, nodes
) {
312 var midpoint
= Math
.floor((end
+ start
)/2);
313 var root
= new DecisionNode(nodes
[midpoint
]);
314 root
.fail
= createBinaryTree(start
, midpoint
- 1, nodes
);
315 root
.pass
= createBinaryTree(midpoint
+ 1, end
, nodes
);
320 * Calculates the optimum split points on the specified axis.
321 * @param {Array<Keys>} allKeys All keys in the keyset.
322 * @param {Partition=} axis Whether to split on the y-axis instead.
323 * @return {Array<Line>} The optimum split points.
325 var findSplits = function(allKeys
, orientation
) {
327 * Returns the minimum edge on the key.
328 * @param {Key} The key.
331 var getMin = function(key
) {
332 return orientation
== Orientation
.HORIZONTAL
? key
.top
: key
.left
;
336 * Returns the maximum edge on the key.
337 * @param {Key} The key.
339 var getMax = function(key
) {
340 return orientation
== Orientation
.HORIZONTAL
? key
.bottom
: key
.right
;
344 * Returns a duplicate free version of array.
345 * @param {Array} array A sorted array.
346 * @return {Array} Sorted array without duplicates.
348 var unique = function(array
) {
350 for (var i
= 0; i
< array
.length
; i
++) {
351 if (i
== 0 || result
[result
.length
-1] != array
[i
])
352 result
.push(array
[i
]);
358 * Creates an array of zeroes.
359 * @param {number} length The length of the array.
360 * @return {Array{number}}
362 var zeroes = function(length
) {
363 var array
= new Array(length
);
364 for (var i
= 0; i
< length
; i
++) {
369 // All edges of keys.
371 for (var i
= 0; i
< allKeys
.length
; i
++) {
372 var key
= allKeys
[i
];
373 var min
= getMin(key
);
374 var max
= getMax(key
);
378 // Array.sort() sorts lexicographically by default.
379 edges
.sort(function(a
, b
) {
382 edges
= unique(edges
);
383 // Container for projection sum from edge i to edge i + 1.
384 var intervalWeight
= zeroes(edges
.length
);
386 for (var i
= 0; i
< allKeys
.length
; i
++) {
387 var key
= allKeys
[i
];
388 var min
= getMin(key
);
389 var max
= getMax(key
);
391 exports
.binarySearch(edges
, 0, edges
.length
- 1, function(index
) {
392 var edge
= edges
[index
];
393 return edge
== min
? 0 : min
- edge
;
395 if (index
< 0 || min
!= edges
[index
]) {
396 console
.error("Unable to split keys.");
399 // Key can span multiple edges.
400 for (var j
= index
; j
< edges
.length
&& edges
[j
] < max
; j
++) {
401 intervalWeight
[j
] ++;
406 // Min and max are bad splits.
407 for (var i
= 1; i
< intervalWeight
.length
- 1; i
++) {
408 if (intervalWeight
[i
] < intervalWeight
[i
- 1]) {
409 var mid
= Math
.abs((edges
[i
] + edges
[i
+1]) / 2)
410 splits
.push(new Line(mid
, orientation
));
417 * Caches the layout of current keysets.
419 function recordKeysets_() {
421 var keysets
= $('keyboard').querySelectorAll('kb-keyset').array();
422 for (var i
= 0; i
< keysets
.length
; i
++) {
423 var keyset
= keysets
[i
];
424 var layout
= new KeysetLayout();
425 var rows
= keyset
.querySelectorAll('kb-row').array();
426 for (var j
= 0; j
< rows
.length
; j
++) {
428 var nodes
= row
.children
;
429 for (var k
= 0 ; k
< nodes
.length
; k
++) {
430 layout
.add(new Key(nodes
[k
]));
433 layout
.regenerateTree();
434 layouts
[keyset
.id
] = layout
;
439 * Returns the layout of the keyset.
440 * @param{!String} id The id of the keyset.
443 var getKeysetLayout_ = function(id
) {
447 exports
.getKeysetLayout
= getKeysetLayout_
;
448 exports
.recordKeysets
= recordKeysets_
;