| OLD | NEW |
| (Empty) |
| 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 {{style: {left: number, top: number, width: number, | |
| 92 * height: number}}} key The key to cache. | |
| 93 * left: The x-coordinate of the left edge of the key. | |
| 94 * top: The y-coordinate of the top edge of the key. | |
| 95 * width: The width of the key in px. | |
| 96 * height: The height of the key in px. | |
| 97 * @constructor | |
| 98 */ | |
| 99 var Key = function(key) { | |
| 100 this.key = key; | |
| 101 var style = key.style; | |
| 102 this.top = parseFloat(style.top) - KEY_PADDING_TOP; | |
| 103 this.left = parseFloat(style.left); | |
| 104 this.right = this.left + parseFloat(style.width); | |
| 105 this.bottom = this.top + parseFloat(style.height) + KEY_PADDING_TOP | |
| 106 + KEY_PADDING_BOTTOM; | |
| 107 } | |
| 108 | |
| 109 Key.prototype = { | |
| 110 /** | |
| 111 * Manhattan distance from the the provided point to the key. | |
| 112 * @param {number} x The x-coordinate of the point. | |
| 113 * @param {number} y The y-coordinate of the point. | |
| 114 * @return {number} | |
| 115 */ | |
| 116 distanceTo: function(x, y) { | |
| 117 return Math.abs(this.intersect(new Line(x))) + | |
| 118 Math.abs(this.intersect(new Line(y, true))); | |
| 119 }, | |
| 120 | |
| 121 /** | |
| 122 * Checks whether the key intersects with the line provided. | |
| 123 * @param {!Line} line The line. | |
| 124 * @return {number} Zero if it intersects, signed manhattan distance if it | |
| 125 * does not. | |
| 126 */ | |
| 127 intersect: function(line) { | |
| 128 var min = line.rotated ? this.top : this.left; | |
| 129 var max = line.rotated ? this.bottom : this.right; | |
| 130 return (line.c > max) ? line.c - max : Math.min(0, line.c - min); | |
| 131 }, | |
| 132 | |
| 133 /** | |
| 134 * Returns the Key as a map. | |
| 135 * @return {Map<String,number>} | |
| 136 */ | |
| 137 toMap: function() { | |
| 138 return { | |
| 139 'x': this.left, | |
| 140 'y': this.top, | |
| 141 'width': this.right - this.left, | |
| 142 'height': this.bottom - this.bottom, | |
| 143 } | |
| 144 }, | |
| 145 }; | |
| 146 | |
| 147 /** | |
| 148 * Object representing the line y = c or x = c. | |
| 149 * @param {number} c The x or y coordinate of the intersection line depending | |
| 150 * on orientation. | |
| 151 * @param {Orientation} orientation The orientation of the line. | |
| 152 * @constructor | |
| 153 */ | |
| 154 var Line = function(c, orientation) { | |
| 155 this.c = c; | |
| 156 this.rotated = orientation; | |
| 157 }; | |
| 158 | |
| 159 Line.prototype = { | |
| 160 /** | |
| 161 * The position of the provided point in relation to the line. | |
| 162 * @param {number} x The x-coordinate of the point. | |
| 163 * @param {number} y The y-coordinate of the point. | |
| 164 * @return {number} Zero if they intersect, negative if the point is before | |
| 165 * the line, positive if it's after. | |
| 166 */ | |
| 167 testPoint: function(x, y) { | |
| 168 var c = this.rotated ? y : x; | |
| 169 return this.c == c ? 0 : c - this.c; | |
| 170 }, | |
| 171 | |
| 172 test: function(key) { | |
| 173 // Key already provides an intersect method. If the key is to the right of | |
| 174 // the line, then the line is to the left of the key. | |
| 175 return -1 * key.intersect(this); | |
| 176 }, | |
| 177 }; | |
| 178 | |
| 179 /** | |
| 180 * A node used to split 2D space. | |
| 181 * @param {Line} line The line to split the space with. | |
| 182 * @constructor | |
| 183 */ | |
| 184 var DecisionNode = function(line) { | |
| 185 this.decision = line; | |
| 186 }; | |
| 187 | |
| 188 DecisionNode.prototype = { | |
| 189 /** | |
| 190 * The test whether to proceed in the left or right branch. | |
| 191 * @type {Line} | |
| 192 */ | |
| 193 decision: undefined, | |
| 194 | |
| 195 /** | |
| 196 * The branch for nodes that failed the decision test. | |
| 197 * @type {?DecisionNode} | |
| 198 */ | |
| 199 fail: undefined, | |
| 200 | |
| 201 /** | |
| 202 * The branch for nodes that passed the decision test. | |
| 203 * @type {?DecisionNode} | |
| 204 */ | |
| 205 pass: undefined, | |
| 206 | |
| 207 /** | |
| 208 * Finds the node closest to the point provided. | |
| 209 * @param {number} x The x-coordinate. | |
| 210 * @param {number} y The y-coordinate. | |
| 211 * @return {DecisionNode | LeafNode} | |
| 212 */ | |
| 213 findClosestNode: function(x, y) { | |
| 214 return this.search(function(node) { | |
| 215 return node.decision.testPoint(x, y) >= 0; | |
| 216 }); | |
| 217 }, | |
| 218 | |
| 219 /** | |
| 220 * Populates the decision tree with elements. | |
| 221 * @param {Array{Key}} The child elements. | |
| 222 */ | |
| 223 populate: function(data) { | |
| 224 if (!data.length) | |
| 225 return; | |
| 226 var pass = []; | |
| 227 var fail = []; | |
| 228 for (var i = 0; i < data.length; i++) { | |
| 229 var result = this.decision.test(data[i]); | |
| 230 // Add to both branches if result == 0. | |
| 231 if (result >= 0) | |
| 232 pass.push(data[i]); | |
| 233 if (result <= 0) | |
| 234 fail.push(data[i]); | |
| 235 } | |
| 236 var currentRotation = this.decision.rotated; | |
| 237 /** | |
| 238 * Splits the tree further such that each leaf has exactly one data point. | |
| 239 * @param {Array} array The data points. | |
| 240 * @return {DecisionNode | LeafNode} The new branch for the tree. | |
| 241 */ | |
| 242 var updateBranch = function(array) { | |
| 243 if (array.length == 1) { | |
| 244 return new LeafNode(array[0]); | |
| 245 } else { | |
| 246 var splits = findSplits(array, !currentRotation); | |
| 247 var tree = createBinaryTree(0, splits.length - 1, splits); | |
| 248 tree.populate(array); | |
| 249 return tree; | |
| 250 } | |
| 251 }; | |
| 252 // All elements that passed the decision test. | |
| 253 if (pass.length > 0) { | |
| 254 if (this.pass) | |
| 255 this.pass.populate(pass); | |
| 256 else | |
| 257 this.pass = updateBranch(pass); | |
| 258 } | |
| 259 // All elements that failed the decision test. | |
| 260 if (fail.length > 0) { | |
| 261 if (this.fail) | |
| 262 this.fail.populate(fail); | |
| 263 else | |
| 264 this.fail = updateBranch(fail); | |
| 265 } | |
| 266 }, | |
| 267 | |
| 268 /** | |
| 269 * Searches for the first leaf that matches the search function. | |
| 270 * @param {Function<DecisionNode>: Boolean} searchFn The function used to | |
| 271 * determine whether to search in the left or right subtree. | |
| 272 * @return {DecisionNode | LeafNode} The node that most closely matches the | |
| 273 * search parameters. | |
| 274 */ | |
| 275 search: function(searchFn) { | |
| 276 if (searchFn(this)) { | |
| 277 return this.pass ? this.pass.search(searchFn) : this; | |
| 278 } | |
| 279 return this.fail ? this.fail.search(searchFn) : this; | |
| 280 }, | |
| 281 | |
| 282 /** | |
| 283 * Tests whether the key belongs in the left or right branch of this node. | |
| 284 * @param {Key} key The key being tested. | |
| 285 * @return {boolean} Whether it belongs in the right branch. | |
| 286 */ | |
| 287 test: function(key) { | |
| 288 return this.decision.testKey(key); | |
| 289 }, | |
| 290 }; | |
| 291 | |
| 292 /** | |
| 293 * Structure representing a leaf in the decision tree. It contains a single | |
| 294 * data point. | |
| 295 */ | |
| 296 var LeafNode = function(data) { | |
| 297 this.data = data; | |
| 298 }; | |
| 299 | |
| 300 LeafNode.prototype = { | |
| 301 search: function() { | |
| 302 return this; | |
| 303 }, | |
| 304 }; | |
| 305 | |
| 306 /** | |
| 307 * Converts the array to a binary tree. | |
| 308 * @param {number} start The start index. | |
| 309 * @param {number} end The end index. | |
| 310 * @param {Array} nodes The array to convert. | |
| 311 * @return {DecisionNode} | |
| 312 */ | |
| 313 var createBinaryTree = function(start, end, nodes) { | |
| 314 if (start > end) | |
| 315 return; | |
| 316 var midpoint = Math.floor((end + start)/2); | |
| 317 var root = new DecisionNode(nodes[midpoint]); | |
| 318 root.fail = createBinaryTree(start, midpoint - 1, nodes); | |
| 319 root.pass = createBinaryTree(midpoint + 1, end, nodes); | |
| 320 return root; | |
| 321 }; | |
| 322 | |
| 323 /** | |
| 324 * Calculates the optimum split points on the specified axis. | |
| 325 * @param {Array.<Keys>} allKeys All keys in the keyset. | |
| 326 * @param {Orientation} orientation Whether to split on the y-axis instead. | |
| 327 * @return {Array.<Line>} The optimum split points. | |
| 328 */ | |
| 329 var findSplits = function(allKeys, orientation) { | |
| 330 /** | |
| 331 * Returns the minimum edge on the key. | |
| 332 * @param {Key} key The key. | |
| 333 * @return {number} | |
| 334 */ | |
| 335 var getMin = function(key) { | |
| 336 return orientation == Orientation.HORIZONTAL ? key.top : key.left; | |
| 337 }; | |
| 338 | |
| 339 /** | |
| 340 * Returns the maximum edge on the key. | |
| 341 * @param {Key} key The key. | |
| 342 */ | |
| 343 var getMax = function(key) { | |
| 344 return orientation == Orientation.HORIZONTAL ? key.bottom : key.right; | |
| 345 }; | |
| 346 | |
| 347 /** | |
| 348 * Returns a duplicate free version of array. | |
| 349 * @param {Array} array A sorted array. | |
| 350 * @return {Array} Sorted array without duplicates. | |
| 351 */ | |
| 352 var unique = function(array) { | |
| 353 var result = []; | |
| 354 for (var i = 0; i< array.length; i++) { | |
| 355 if (i == 0 || result[result.length -1] != array[i]) | |
| 356 result.push(array[i]); | |
| 357 } | |
| 358 return result; | |
| 359 }; | |
| 360 | |
| 361 /** | |
| 362 * Creates an array of zeroes. | |
| 363 * @param {number} length The length of the array. | |
| 364 * @return {Array{number}} | |
| 365 */ | |
| 366 var zeroes = function(length) { | |
| 367 var array = new Array(length); | |
| 368 for (var i = 0; i < length; i++) { | |
| 369 array[i] = 0; | |
| 370 } | |
| 371 return array; | |
| 372 } | |
| 373 // All edges of keys. | |
| 374 var edges = []; | |
| 375 for (var i = 0; i < allKeys.length; i++) { | |
| 376 var key = allKeys[i]; | |
| 377 var min = getMin(key); | |
| 378 var max = getMax(key); | |
| 379 edges.push(min); | |
| 380 edges.push(max); | |
| 381 } | |
| 382 // Array.sort() sorts lexicographically by default. | |
| 383 edges.sort(function(a, b) { | |
| 384 return a - b; | |
| 385 }); | |
| 386 edges = unique(edges); | |
| 387 // Container for projection sum from edge i to edge i + 1. | |
| 388 var intervalWeight = zeroes(edges.length); | |
| 389 | |
| 390 for (var i = 0; i < allKeys.length; i++) { | |
| 391 var key = allKeys[i]; | |
| 392 var min = getMin(key); | |
| 393 var max = getMax(key); | |
| 394 var index = | |
| 395 exports.binarySearch(edges, 0, edges.length - 1, function(index) { | |
| 396 var edge = edges[index]; | |
| 397 return edge == min ? 0 : min - edge; | |
| 398 }); | |
| 399 if (index < 0 || min != edges[index]) { | |
| 400 console.error("Unable to split keys."); | |
| 401 return; | |
| 402 } | |
| 403 // Key can span multiple edges. | |
| 404 for (var j = index; j < edges.length && edges[j] < max; j++) { | |
| 405 intervalWeight[j] ++; | |
| 406 } | |
| 407 } | |
| 408 | |
| 409 var splits = []; | |
| 410 // Min and max are bad splits. | |
| 411 for (var i = 1; i < intervalWeight.length - 1; i++) { | |
| 412 if (intervalWeight[i] < intervalWeight[i - 1]) { | |
| 413 var mid = Math.abs((edges[i] + edges[i+1]) / 2) | |
| 414 splits.push(new Line(mid, orientation)); | |
| 415 } | |
| 416 } | |
| 417 return splits; | |
| 418 } | |
| 419 | |
| 420 /** | |
| 421 * Caches the layout of current keysets. | |
| 422 */ | |
| 423 function recordKeysets_() { | |
| 424 layouts = {}; | |
| 425 var keysets = $('keyboard').querySelectorAll('kb-keyset').array(); | |
| 426 for (var i = 0; i < keysets.length; i++) { | |
| 427 var keyset = keysets[i]; | |
| 428 var layout = new KeysetLayout(); | |
| 429 var rows = keyset.querySelectorAll('kb-row').array(); | |
| 430 for (var j = 0; j < rows.length; j++) { | |
| 431 var row = rows[j]; | |
| 432 var nodes = row.children; | |
| 433 for (var k = 0 ; k < nodes.length; k++) { | |
| 434 layout.add(new Key(nodes[k])); | |
| 435 } | |
| 436 } | |
| 437 layout.regenerateTree(); | |
| 438 layouts[keyset.id] = layout; | |
| 439 } | |
| 440 }; | |
| 441 | |
| 442 /** | |
| 443 * Returns the layout of the keyset. | |
| 444 * @param{!String} id The id of the keyset. | |
| 445 * @private | |
| 446 */ | |
| 447 var getKeysetLayout_ = function(id) { | |
| 448 return layouts[id]; | |
| 449 }; | |
| 450 | |
| 451 exports.getKeysetLayout = getKeysetLayout_; | |
| 452 exports.recordKeysets = recordKeysets_; | |
| 453 })(this); | |
| OLD | NEW |