• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5
6(function(exports) {
7  /**
8   * Orientation of a line.
9   * @enum {boolean}
10   */
11  var Orientation = {
12    VERTICAL: false,
13    HORIZONTAL: true
14  }
15
16  /**
17   * Map from keysetId to layout.
18   * @type {Map<String,KeysetLayout>}
19   * @private
20   */
21  var layouts = {};
22
23  /**
24   * Container for storing a keyset's layout.
25   */
26  var KeysetLayout = function() {
27    this.keys = [];
28  }
29
30  KeysetLayout.prototype = {
31    /**
32     * All keys in the keyset.
33     * @type {Array<Key>}
34     */
35    keys: undefined,
36
37    /**
38     * Spacial partitioning of all keys in the keyset.
39     * @type {DecisionNode}
40     */
41    tree: undefined,
42
43    /**
44     * Add a key to the keyset.
45     */
46    add: function(key) {
47      this.keys.push(key);
48    },
49
50    /**
51     * Regenerates a decision tree using the keys in the keyset.
52     */
53    regenerateTree: function() {
54      // Split using horizontal lines first, as keyboards tend to be
55      // row-centric.
56      var splits = findSplits(this.keys, Orientation.HORIZONTAL);
57      this.tree = createBinaryTree(0, splits.length - 1, splits);
58      if (this.tree)
59        this.tree.populate(this.keys);
60    },
61
62    /**
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.
67     */
68    findClosestKey: function(x, y) {
69      var closestNode = this.tree.findClosestNode(x, y);
70      var key = closestNode.data;
71      if (!key)
72        return;
73      // Ignore touches that aren't close.
74      return key.distanceTo(x,y) <= MAX_TOUCH_FUZZ_DISTANCE ?
75          key.key : null;
76    },
77
78    /**
79     * Returns the position data of all keys in this keyset.
80     * @return {Array<Map>}
81     */
82    getLayout: function() {
83      return this.keys.map(function(key) {
84        return key.toMap();
85      });
86    },
87  };
88
89  /**
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.
96   */
97  var Key = function(key) {
98    this.key = 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;
105  }
106
107  Key.prototype = {
108    /**
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.
112     * @return {number}.
113     */
114    distanceTo: function (x, y) {
115      return Math.abs(this.intersect(new Line(x))) +
116          Math.abs(this.intersect(new Line(y, true)));
117    },
118
119    /**
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
123     *     does not.
124     */
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);
129    },
130
131    /**
132     * Returns the Key as a map.
133     * @return {Map<String,number>}
134     */
135    toMap: function() {
136      return {
137        'x': this.left,
138        'y': this.top,
139        'width': this.right - this.left,
140        'height': this.bottom - this.bottom,
141      }
142    },
143  };
144
145  /**
146   * Object representing the line y = c or x = c.
147   * @param {number} The x or y coordinate of the intersection line depending on
148   *    on orientation.
149   * @param {Orientation} orientation The orientation of the line.
150   */
151  var Line = function(c, orientation) {
152    this.c = c;
153    this.rotated = orientation;
154  };
155
156  Line.prototype = {
157    /**
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.
163     */
164    testPoint: function (x, y) {
165      var c = this.rotated ? y : x;
166      return this.c == c ? 0 : c - this.c;
167    },
168
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);
173    },
174  };
175
176  /**
177   * A node used to split 2D space.
178   * @param {Line} The line to split the space with.
179   */
180  var DecisionNode = function(line) {
181    this.decision = line;
182  };
183
184  DecisionNode.prototype = {
185    /**
186     * The test whether to proceed in the left or right branch.
187     * @type {Line}
188     */
189    decision: undefined,
190
191    /**
192     * The branch for nodes that failed the decision test.
193     * @type {?DecisionNode}
194     */
195    fail: undefined,
196
197    /**
198     * The branch for nodes that passed the decision test.
199     * @type {?DecisionNode}
200     */
201    pass: undefined,
202
203    /**
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}
208     */
209    findClosestNode: function(x, y) {
210      return this.search(function(node){
211        return node.decision.testPoint(x,y) >= 0;
212      });
213    },
214
215    /**
216     * Populates the decision tree with elements.
217     * @param {Array{Key}} The child elements.
218     */
219    populate: function(data) {
220      if (!data.length)
221        return;
222      var pass = [];
223      var fail = [];
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.
227        if (result >= 0)
228          pass.push(data[i]);
229        if (result <= 0)
230          fail.push(data[i]);
231      }
232      var currentRotation = this.decision.rotated;
233      /**
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.
237       */
238      var updateBranch = function(array) {
239        if (array.length == 1) {
240          return new LeafNode(array[0]);
241        } else {
242          var splits = findSplits(array, !currentRotation)
243          var tree = createBinaryTree(0, splits.length - 1, splits);
244          tree.populate(array);
245          return tree;
246        }
247      };
248      // All elements that passed the decision test.
249      if (pass.length > 0) {
250        if (this.pass)
251          this.pass.populate(pass);
252        else
253          this.pass = updateBranch(pass);
254      }
255      // All elements that failed the decision test.
256      if (fail.length > 0) {
257        if (this.fail)
258          this.fail.populate(fail);
259        else
260          this.fail = updateBranch(fail);
261      }
262    },
263
264    /**
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
269     *    search parameters.
270     */
271    search: function(searchFn) {
272      if (searchFn(this)) {
273        return this.pass ? this.pass.search(searchFn) : this;
274      }
275      return this.fail ? this.fail.search(searchFn) : this;
276    },
277
278    /**
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.
282     */
283    test: function(key) {
284      return this.decision.testKey(key)
285    },
286  };
287
288  /**
289   * Structure representing a leaf in the decision tree. It contains a single
290   * data point.
291   */
292  var LeafNode = function(data) {
293    this.data = data;
294  };
295
296  LeafNode.prototype = {
297    search: function() {
298      return this;
299    },
300  };
301
302  /**
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}
308   */
309  var createBinaryTree = function(start, end, nodes) {
310    if (start > end)
311      return;
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);
316    return root;
317  };
318
319  /**
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.
324   */
325  var findSplits = function(allKeys, orientation) {
326    /**
327     * Returns the minimum edge on the key.
328     * @param {Key} The key.
329     * @return {number}
330     */
331    var getMin = function(key) {
332      return orientation == Orientation.HORIZONTAL ? key.top : key.left;
333    };
334
335    /**
336     * Returns the maximum edge on the key.
337     * @param {Key} The key.
338     */
339    var getMax = function(key) {
340      return orientation == Orientation.HORIZONTAL ? key.bottom : key.right;
341    };
342
343    /**
344     * Returns a duplicate free version of array.
345     * @param {Array} array A sorted array.
346     * @return {Array} Sorted array without duplicates.
347     */
348    var unique = function(array) {
349      var result = [];
350      for (var i = 0; i< array.length; i++) {
351        if (i == 0 || result[result.length -1] != array[i])
352            result.push(array[i]);
353      }
354      return result;
355    };
356
357    /**
358     * Creates an array of zeroes.
359     * @param {number} length The length of the array.
360     * @return {Array{number}}
361     */
362    var zeroes = function(length) {
363      var array = new Array(length);
364      for (var i = 0; i < length; i++) {
365        array[i] = 0;
366      }
367      return array;
368    }
369    // All edges of keys.
370    var edges = [];
371    for (var i = 0; i < allKeys.length; i++) {
372      var key = allKeys[i];
373      var min = getMin(key);
374      var max = getMax(key);
375      edges.push(min);
376      edges.push(max);
377    }
378    // Array.sort() sorts lexicographically by default.
379    edges.sort(function(a, b) {
380      return a - b;
381    });
382    edges = unique(edges);
383    // Container for projection sum from edge i to edge i + 1.
384    var intervalWeight = zeroes(edges.length);
385
386    for (var i = 0; i < allKeys.length; i++) {
387      var key = allKeys[i];
388      var min = getMin(key);
389      var max = getMax(key);
390      var index =
391          exports.binarySearch(edges, 0, edges.length - 1, function(index) {
392        var edge = edges[index];
393        return edge == min ? 0 : min - edge;
394      });
395      if (index < 0 || min != edges[index]) {
396        console.error("Unable to split keys.");
397        return;
398      }
399      // Key can span multiple edges.
400      for (var j = index; j < edges.length && edges[j] < max; j++) {
401        intervalWeight[j] ++;
402      }
403    }
404
405    var splits = [];
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));
411      }
412    }
413    return splits;
414  }
415
416  /**
417   * Caches the layout of current keysets.
418   */
419  function recordKeysets_() {
420    layouts = {};
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++) {
427        var row = rows[j];
428        var nodes = row.children;
429        for (var k = 0 ; k < nodes.length; k++) {
430          layout.add(new Key(nodes[k]));
431        }
432      }
433      layout.regenerateTree();
434      layouts[keyset.id] = layout;
435    }
436  };
437
438  /**
439   * Returns the layout of the keyset.
440   * @param{!String} id The id of the keyset.
441   * @private
442   */
443  var getKeysetLayout_ = function(id) {
444    return layouts[id];
445  };
446
447  exports.getKeysetLayout = getKeysetLayout_;
448  exports.recordKeysets = recordKeysets_;
449})(this);
450