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); 454