| OLD | NEW |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** @fileoverview Logic for panning a braille display within a line of braille | 5 /** @fileoverview Logic for panning a braille display within a line of braille |
| 6 * content that might not fit on a single display. | 6 * content that might not fit on a single display. |
| 7 */ | 7 */ |
| 8 | 8 |
| 9 goog.provide('cvox.PanStrategy'); | 9 goog.provide('cvox.PanStrategy'); |
| 10 | 10 |
| 11 /** | 11 /** |
| 12 * @constructor | 12 * @constructor |
| 13 * | 13 * |
| 14 * A stateful class that keeps track of the current 'viewport' of a braille | 14 * A stateful class that keeps track of the current 'viewport' of a braille |
| 15 * display in a line of content. | 15 * display in a line of content. |
| 16 */ | 16 */ |
| 17 cvox.PanStrategy = function() { | 17 cvox.PanStrategy = function() { |
| 18 /** | 18 /** |
| 19 * @type {number} | 19 * @type {{rows: number, columns: number}} |
| 20 * @private | 20 * @private |
| 21 */ | 21 */ |
| 22 this.displaySize_ = 0; | 22 this.displaySize_ = {rows: 1, columns: 40}; |
| 23 |
| 23 /** | 24 /** |
| 24 * @type {number} | 25 * Start and end are both inclusive. |
| 25 * @private | |
| 26 */ | |
| 27 this.contentLength_ = 0; | |
| 28 /** | |
| 29 * Points before which it is desirable to break content if it doesn't fit | |
| 30 * on the display. | |
| 31 * @type {!Array<number>} | |
| 32 * @private | |
| 33 */ | |
| 34 this.breakPoints_ = []; | |
| 35 /** | |
| 36 * @type {!cvox.PanStrategy.Range} | 26 * @type {!cvox.PanStrategy.Range} |
| 37 * @private | 27 * @private |
| 38 */ | 28 */ |
| 39 this.viewPort_ = {start: 0, end: 0}; | 29 this.viewPort_ = {firstRow: 0, lastRow: 0}; |
| 30 |
| 31 /** |
| 32 * The ArrayBuffer holding the braille cells after it's been processed to |
| 33 * wrap words that are cut off by the column boundaries. |
| 34 * @type {!ArrayBuffer} |
| 35 * @private |
| 36 */ |
| 37 this.wrappedBuffer_ = new ArrayBuffer(0); |
| 38 |
| 39 /** |
| 40 * The original text that corresponds with the braille buffers. There is only |
| 41 * one textBuffer that correlates with both fixed and wrapped buffers. |
| 42 * @type {string} |
| 43 * @private |
| 44 */ |
| 45 this.textBuffer_ = ''; |
| 46 |
| 47 /** |
| 48 * The ArrayBuffer holding the original braille cells, without being |
| 49 * processed to wrap words. |
| 50 * @type {!ArrayBuffer} |
| 51 * @private |
| 52 */ |
| 53 this.fixedBuffer_ = new ArrayBuffer(0); |
| 54 |
| 55 /** |
| 56 * The updated mapping from braille cells to text characters for the wrapped |
| 57 * buffer. |
| 58 * @type {Array<number>} |
| 59 * @private |
| 60 */ |
| 61 this.wrappedBrailleToText_ = []; |
| 62 |
| 63 /** |
| 64 * The original mapping from braille cells to text characters. |
| 65 * @type {Array<number>} |
| 66 * @private |
| 67 */ |
| 68 this.fixedBrailleToText_ = []; |
| 69 |
| 70 /** |
| 71 * Indicates whether the pan strategy is wrapped or fixed. It is wrapped when |
| 72 * true. |
| 73 * @type {boolean} |
| 74 * @private |
| 75 */ |
| 76 this.panStrategyWrapped_ = false; |
| 77 |
| 40 }; | 78 }; |
| 41 | 79 |
| 42 /** | 80 /** |
| 43 * A range used to represent the viewport with inclusive start and xclusive | 81 * A range used to represent the viewport with inclusive start and xclusive |
| 44 * end position. | 82 * end position. |
| 45 * @typedef {{start: number, end: number}} | 83 * @typedef {{firstRow: number, lastRow: number}} |
| 46 */ | 84 */ |
| 47 cvox.PanStrategy.Range; | 85 cvox.PanStrategy.Range; |
| 48 | 86 |
| 49 cvox.PanStrategy.prototype = { | 87 cvox.PanStrategy.prototype = { |
| 50 /** | 88 /** |
| 51 * Gets the current viewport which is never larger than the current | 89 * Gets the current viewport which is never larger than the current |
| 52 * display size and whose end points are always within the limits of | 90 * display size and whose end points are always within the limits of |
| 53 * the current content. | 91 * the current content. |
| 54 * @type {!cvox.PanStrategy.Range} | 92 * @type {!cvox.PanStrategy.Range} |
| 55 */ | 93 */ |
| 56 get viewPort() { | 94 get viewPort() { |
| 57 return this.viewPort_; | 95 return this.viewPort_; |
| 58 }, | 96 }, |
| 59 | 97 |
| 60 /** | 98 /** |
| 99 * Gets the current displaySize. |
| 100 * @type {{rows: number, columns: number}} |
| 101 */ |
| 102 get displaySize() { |
| 103 return this.displaySize_; |
| 104 }, |
| 105 |
| 106 /** |
| 107 * @return {{brailleOffset: number, textOffset: number}} The offset of |
| 108 * braille and text indices of the current slice. |
| 109 */ |
| 110 get offsetsForSlices() { |
| 111 return {brailleOffset: this.viewPort_.firstRow * this.displaySize_.columns, |
| 112 textOffset: this.brailleToText[this.viewPort_.firstRow * |
| 113 this.displaySize_.columns]}; |
| 114 }, |
| 115 |
| 116 /** |
| 117 * @return {number} The number of lines in the fixedBuffer. |
| 118 */ |
| 119 get fixedLineCount() { |
| 120 return Math.ceil(this.fixedBuffer_.byteLength / this.displaySize_.columns); |
| 121 }, |
| 122 |
| 123 /** |
| 124 * @return {number} The number of lines in the wrappedBuffer. |
| 125 */ |
| 126 get wrappedLineCount() { |
| 127 return Math.ceil(this.wrappedBuffer_.byteLength / |
| 128 this.displaySize_.columns); |
| 129 }, |
| 130 |
| 131 /** |
| 132 * @return {Array<number>} The map of Braille cells to the first index of the |
| 133 * corresponding text character. |
| 134 */ |
| 135 get brailleToText() { |
| 136 if (this.panStrategyWrapped_) |
| 137 return this.wrappedBrailleToText_; |
| 138 else |
| 139 return this.fixedBrailleToText_; |
| 140 }, |
| 141 |
| 142 /** |
| 143 * @return {ArrayBuffer} Buffer of the slice of braille cells within the |
| 144 * bounds of the viewport. |
| 145 */ |
| 146 getCurrentBrailleViewportContents: function() { |
| 147 var buf = this.panStrategyWrapped_ ? |
| 148 this.wrappedBuffer_ : this.fixedBuffer_; |
| 149 return buf.slice(this.viewPort_.firstRow * this.displaySize_.columns, |
| 150 (this.viewPort_.lastRow + 1) * this.displaySize_.columns); |
| 151 }, |
| 152 |
| 153 /** |
| 154 * @return {string} String of the slice of text letters corresponding with |
| 155 * the current braille slice. |
| 156 */ |
| 157 getCurrentTextViewportContents: function() { |
| 158 var brailleToText = this.brailleToText; |
| 159 // Index of last braille character in slice. |
| 160 var index = (this.viewPort_.lastRow + 1) * this.displaySize_.columns - 1; |
| 161 // Index of first text character that the last braille character points to. |
| 162 var end = brailleToText[index]; |
| 163 // Increment index until brailleToText[index] points to a different char. |
| 164 // This is the cutoff point, as substring cuts up to, but not including, |
| 165 // brailleToText[index]. |
| 166 while (index < brailleToText.length && brailleToText[index] == end) { |
| 167 index++; |
| 168 } |
| 169 return this.textBuffer_.substring( |
| 170 brailleToText[this.viewPort_.firstRow * this.displaySize_.columns], |
| 171 brailleToText[index]); |
| 172 }, |
| 173 |
| 174 /** |
| 175 * Sets the current pan strategy and resets the viewport. |
| 176 */ |
| 177 setPanStrategy: function(wordWrap) { |
| 178 this.panStrategyWrapped_ = wordWrap; |
| 179 this.panToPosition_(0); |
| 180 }, |
| 181 |
| 182 /** |
| 61 * Sets the display size. This call may update the viewport. | 183 * Sets the display size. This call may update the viewport. |
| 62 * @param {number} size the new display size, or {@code 0} if no display is | 184 * @param {number} rowCount the new row size, or {@code 0} if no display is |
| 63 * present. | 185 * present. |
| 186 * @param {number} columnCount the new column size, or {@code 0} |
| 187 * if no display is present. |
| 64 */ | 188 */ |
| 65 setDisplaySize: function(size) { | 189 setDisplaySize: function(rowCount, columnCount) { |
| 66 this.displaySize_ = size; | 190 this.displaySize_ = {rows: rowCount, columns: columnCount}; |
| 67 this.panToPosition_(this.viewPort_.start); | 191 this.setContent(this.textBuffer_, this.fixedBuffer_, |
| 192 this.fixedBrailleToText_, 0); |
| 68 }, | 193 }, |
| 69 | 194 |
| 70 /** | 195 /** |
| 71 * Sets the current content that panning should happen within. This call may | 196 * Sets the internal data structures that hold the fixed and wrapped buffers |
| 72 * change the viewport. | 197 * and maps. |
| 73 * @param {!ArrayBuffer} translatedContent The new content. | 198 * @param {string} textBuffer Text of the shown braille. |
| 199 * @param {!ArrayBuffer} translatedContent The new braille content. |
| 200 * @param {Array<number>} fixedBrailleToText Map of Braille cells to the first |
| 201 * index of corresponding text letter. |
| 74 * @param {number} targetPosition Target position. The viewport is changed | 202 * @param {number} targetPosition Target position. The viewport is changed |
| 75 * to overlap this position. | 203 * to overlap this position. |
| 76 */ | 204 */ |
| 77 setContent: function(translatedContent, targetPosition) { | 205 setContent: function(textBuffer, translatedContent, fixedBrailleToText, |
| 78 this.breakPoints_ = this.calculateBreakPoints_(translatedContent); | 206 targetPosition) { |
| 79 this.contentLength_ = translatedContent.byteLength; | 207 this.viewPort_.firstRow = 0; |
| 208 this.viewPort_.lastRow = this.displaySize_.rows - 1; |
| 209 this.fixedBrailleToText_ = fixedBrailleToText; |
| 210 this.wrappedBrailleToText_ = []; |
| 211 this.textBuffer_ = textBuffer; |
| 212 this.fixedBuffer_ = translatedContent; |
| 213 |
| 214 // Convert the cells to Unicode braille pattern characters. |
| 215 var view = new Uint8Array(translatedContent); |
| 216 var wrappedBrailleArray = []; |
| 217 |
| 218 var lastBreak = 0; |
| 219 var cellsPadded = 0; |
| 220 var index; |
| 221 for (index = 0; index < translatedContent.byteLength + cellsPadded; |
| 222 index++) { |
| 223 // Is index at the beginning of a new line? |
| 224 if (index != 0 && index % this.displaySize_.columns == 0) { |
| 225 if (view[index - cellsPadded] == 0) { |
| 226 // Delete all empty cells at the beginning of this line. |
| 227 while (index - cellsPadded < view.length && |
| 228 view[index - cellsPadded] == 0) { |
| 229 cellsPadded--; |
| 230 } |
| 231 index--; |
| 232 lastBreak = index; |
| 233 } else if (view[index - cellsPadded - 1] != 0 && |
| 234 lastBreak % this.displaySize_.columns != 0) { |
| 235 // If first cell is not empty, we need to move the whole word down to |
| 236 // this line and padd to previous line with 0's, from |lastBreak| to |
| 237 // index. The braille to text map is also updated. |
| 238 // If lastBreak is at the beginning of a line, that means the current |
| 239 // word is bigger than |this.displaySize_.columns| so we shouldn't |
| 240 // wrap. |
| 241 for (var j = lastBreak + 1; j < index; j++) { |
| 242 wrappedBrailleArray[j] = 0; |
| 243 this.wrappedBrailleToText_[j] = this.wrappedBrailleToText_[j - 1]; |
| 244 cellsPadded++; |
| 245 } |
| 246 lastBreak = index; |
| 247 index--; |
| 248 } else { |
| 249 // |lastBreak| is at the beginning of a line, so current word is |
| 250 // bigger than |this.displaySize_.columns| so we shouldn't wrap. |
| 251 wrappedBrailleArray.push(view[index - cellsPadded]); |
| 252 this.wrappedBrailleToText_.push( |
| 253 fixedBrailleToText[index - cellsPadded]); |
| 254 } |
| 255 } else { |
| 256 if (view[index - cellsPadded] == 0) { |
| 257 lastBreak = index; |
| 258 } |
| 259 wrappedBrailleArray.push(view[index - cellsPadded]); |
| 260 this.wrappedBrailleToText_.push( |
| 261 fixedBrailleToText[index - cellsPadded]); |
| 262 } |
| 263 } |
| 264 |
| 265 // Convert the wrapped Braille Uint8 Array back to ArrayBuffer. |
| 266 var wrappedBrailleUint8Array = new Uint8Array(wrappedBrailleArray); |
| 267 this.wrappedBuffer_ = new ArrayBuffer(wrappedBrailleUint8Array.length); |
| 268 new Uint8Array(this.wrappedBuffer_).set(wrappedBrailleUint8Array); |
| 80 this.panToPosition_(targetPosition); | 269 this.panToPosition_(targetPosition); |
| 81 }, | 270 }, |
| 82 | 271 |
| 83 /** | 272 /** |
| 84 * If possible, changes the viewport to a part of the line that follows | 273 * If possible, changes the viewport to a part of the line that follows |
| 85 * the current viewport. | 274 * the current viewport. |
| 86 * @return {boolean} {@code true} if the viewport was changed. | 275 * @return {boolean} {@code true} if the viewport was changed. |
| 87 */ | 276 */ |
| 88 next: function() { | 277 next: function() { |
| 89 var newStart = this.viewPort_.end; | 278 var contentLength = this.panStrategyWrapped_ ? |
| 279 this.wrappedLineCount : this.fixedLineCount; |
| 280 var newStart = this.viewPort_.lastRow + 1; |
| 90 var newEnd; | 281 var newEnd; |
| 91 if (newStart + this.displaySize_ < this.contentLength_) { | 282 if (newStart + this.displaySize_.rows - 1 < contentLength) { |
| 92 newEnd = this.extendRight_(newStart); | 283 newEnd = newStart + this.displaySize_.rows - 1; |
| 93 } else { | 284 } else { |
| 94 newEnd = this.contentLength_; | 285 newEnd = contentLength - 1; |
| 95 } | 286 } |
| 96 if (newEnd > newStart) { | 287 if (newEnd >= newStart) { |
| 97 this.viewPort_ = {start: newStart, end: newEnd}; | 288 this.viewPort_ = {firstRow: newStart, lastRow: newEnd}; |
| 98 return true; | 289 return true; |
| 99 } | 290 } |
| 100 return false; | 291 return false; |
| 101 }, | 292 }, |
| 102 | 293 |
| 103 /** | 294 /** |
| 104 * If possible, changes the viewport to a part of the line that precedes | 295 * If possible, changes the viewport to a part of the line that precedes |
| 105 * the current viewport. | 296 * the current viewport. |
| 106 * @return {boolean} {@code true} if the viewport was changed. | 297 * @return {boolean} {@code true} if the viewport was changed. |
| 107 */ | 298 */ |
| 108 previous: function() { | 299 previous: function() { |
| 109 if (this.viewPort_.start > 0) { | 300 var contentLength = this.panStrategyWrapped_ ? |
| 301 this.wrappedLineCount : this.fixedLineCount; |
| 302 if (this.viewPort_.firstRow > 0) { |
| 110 var newStart, newEnd; | 303 var newStart, newEnd; |
| 111 if (this.viewPort_.start <= this.displaySize_) { | 304 if (this.viewPort_.firstRow < this.displaySize_.rows) { |
| 112 newStart = 0; | 305 newStart = 0; |
| 113 newEnd = this.extendRight_(newStart); | 306 newEnd = Math.min(this.displaySize_.rows, contentLength); |
| 114 } else { | 307 } else { |
| 115 newEnd = this.viewPort_.start; | 308 newEnd = this.viewPort_.firstRow - 1; |
| 116 var limit = newEnd - this.displaySize_; | 309 newStart = newEnd - this.displaySize_.rows + 1; |
| 117 newStart = limit; | |
| 118 var pos = 0; | |
| 119 while (pos < this.breakPoints_.length && | |
| 120 this.breakPoints_[pos] < limit) { | |
| 121 pos++; | |
| 122 } | |
| 123 if (pos < this.breakPoints_.length && | |
| 124 this.breakPoints_[pos] < newEnd) { | |
| 125 newStart = this.breakPoints_[pos]; | |
| 126 } | |
| 127 } | 310 } |
| 128 if (newStart < newEnd) { | 311 if (newStart <= newEnd) { |
| 129 this.viewPort_ = {start: newStart, end: newEnd}; | 312 this.viewPort_ = {firstRow: newStart, lastRow: newEnd}; |
| 130 return true; | 313 return true; |
| 131 } | 314 } |
| 132 } | 315 } |
| 133 return false; | 316 return false; |
| 134 }, | 317 }, |
| 135 | 318 |
| 136 /** | 319 /** |
| 137 * Finds the end position for a new viewport start position, considering | |
| 138 * current breakpoints as well as display size and content length. | |
| 139 * @param {number} from Start of the region to extend. | |
| 140 * @return {number} | |
| 141 * @private | |
| 142 */ | |
| 143 extendRight_: function(from) { | |
| 144 var limit = Math.min(from + this.displaySize_, this.contentLength_); | |
| 145 var pos = 0; | |
| 146 var result = limit; | |
| 147 while (pos < this.breakPoints_.length && this.breakPoints_[pos] <= from) { | |
| 148 pos++; | |
| 149 } | |
| 150 while (pos < this.breakPoints_.length && this.breakPoints_[pos] <= limit) { | |
| 151 result = this.breakPoints_[pos]; | |
| 152 pos++; | |
| 153 } | |
| 154 return result; | |
| 155 }, | |
| 156 | |
| 157 /** | |
| 158 * Overridden by subclasses to provide breakpoints given translated | |
| 159 * braille cell content. | |
| 160 * @param {!ArrayBuffer} content New display content. | |
| 161 * @return {!Array<number>} The points before which it is desirable to break | |
| 162 * content if needed or the empty array if no points are more desirable | |
| 163 * than any position. | |
| 164 * @private | |
| 165 */ | |
| 166 calculateBreakPoints_: function(content) {return [];}, | |
| 167 | |
| 168 /** | |
| 169 * Moves the viewport so that it overlaps a target position without taking | 320 * Moves the viewport so that it overlaps a target position without taking |
| 170 * the current viewport position into consideration. | 321 * the current viewport position into consideration. |
| 171 * @param {number} position Target position. | 322 * @param {number} position Target position. |
| 172 */ | 323 */ |
| 173 panToPosition_: function(position) { | 324 panToPosition_: function(position) { |
| 174 if (this.displaySize_ > 0) { | 325 if (this.displaySize_.rows * this.displaySize_.columns > 0) { |
| 175 this.viewPort_ = {start: 0, end: 0}; | 326 this.viewPort_ = {firstRow: -1, lastRow: -1}; |
| 176 while (this.next() && this.viewPort_.end <= position) { | 327 while (this.next() && (this.viewPort_.lastRow + 1) * |
| 328 this.displaySize_.columns <= position) { |
| 177 // Nothing to do. | 329 // Nothing to do. |
| 178 } | 330 } |
| 179 } else { | 331 } else { |
| 180 this.viewPort_ = {start: position, end: position}; | 332 this.viewPort_ = {firstRow: position, lastRow: position}; |
| 181 } | 333 } |
| 182 }, | 334 }, |
| 183 }; | 335 }; |
| 184 | |
| 185 /** | |
| 186 * A pan strategy that fits as much content on the display as possible, that | |
| 187 * is, it doesn't do any wrapping. | |
| 188 * @constructor | |
| 189 * @extends {cvox.PanStrategy} | |
| 190 */ | |
| 191 cvox.FixedPanStrategy = cvox.PanStrategy; | |
| 192 /** | |
| 193 * A pan strategy that tries to wrap 'words' when breaking content. | |
| 194 * A 'word' in this context is just a chunk of non-blank braille cells | |
| 195 * delimited by blank cells. | |
| 196 * @constructor | |
| 197 * @extends {cvox.PanStrategy} | |
| 198 */ | |
| 199 cvox.WrappingPanStrategy = function() { | |
| 200 cvox.PanStrategy.call(this); | |
| 201 }; | |
| 202 | |
| 203 cvox.WrappingPanStrategy.prototype = { | |
| 204 __proto__: cvox.PanStrategy.prototype, | |
| 205 | |
| 206 /** @override */ | |
| 207 calculateBreakPoints_: function(content) { | |
| 208 var view = new Uint8Array(content); | |
| 209 var newContentLength = view.length; | |
| 210 var result = []; | |
| 211 var lastCellWasBlank = false; | |
| 212 for (var pos = 0; pos < view.length; ++pos) { | |
| 213 if (lastCellWasBlank && view[pos] != 0) { | |
| 214 result.push(pos); | |
| 215 } | |
| 216 lastCellWasBlank = (view[pos] == 0); | |
| 217 } | |
| 218 return result; | |
| 219 }, | |
| 220 }; | |
| OLD | NEW |