LineBreakMeasurer.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. /**
  2. * Word wrapping
  3. *
  4. * @author (Javascript) Dmitry Farafonov
  5. */
  6. var AttributedStringIterator = function(text){
  7. //this.text = this.rtrim(this.ltrim(text));
  8. text = text.replace(/(\s)+/, " ");
  9. this.text = this.rtrim(text);
  10. /*
  11. if (beginIndex < 0 || beginIndex > endIndex || endIndex > length()) {
  12. throw new IllegalArgumentException("Invalid substring range");
  13. }
  14. */
  15. this.beginIndex = 0;
  16. this.endIndex = this.text.length;
  17. this.currentIndex = this.beginIndex;
  18. //console.group("[AttributedStringIterator]");
  19. var i = 0;
  20. var string = this.text;
  21. var fullPos = 0;
  22. //console.log("string: \"" + string + "\", length: " + string.length);
  23. this.startWordOffsets = [];
  24. this.startWordOffsets.push(fullPos);
  25. // TODO: remove i 1000
  26. while (i<1000) {
  27. var pos = string.search(/[ \t\n\f-\.\,]/);
  28. if (pos == -1)
  29. break;
  30. // whitespace start
  31. fullPos += pos;
  32. string = string.substr(pos);
  33. ////console.log("fullPos: " + fullPos + ", pos: " + pos + ", string: ", string);
  34. // remove whitespaces
  35. var pos = string.search(/[^ \t\n\f-\.\,]/);
  36. if (pos == -1)
  37. break;
  38. // whitespace end
  39. fullPos += pos;
  40. string = string.substr(pos);
  41. ////console.log("fullPos: " + fullPos);
  42. this.startWordOffsets.push(fullPos);
  43. i++;
  44. }
  45. //console.log("startWordOffsets: ", this.startWordOffsets);
  46. //console.groupEnd();
  47. };
  48. AttributedStringIterator.prototype = {
  49. getEndIndex: function(pos){
  50. if (typeof(pos) == "undefined")
  51. return this.endIndex;
  52. var string = this.text.substr(pos, this.endIndex - pos);
  53. var posEndOfLine = string.search(/[\n]/);
  54. if (posEndOfLine == -1)
  55. return this.endIndex;
  56. else
  57. return pos + posEndOfLine;
  58. },
  59. getBeginIndex: function(){
  60. return this.beginIndex;
  61. },
  62. isWhitespace: function(pos){
  63. var str = this.text[pos];
  64. var whitespaceChars = " \t\n\f";
  65. return (whitespaceChars.indexOf(str) != -1);
  66. },
  67. isNewLine: function(pos){
  68. var str = this.text[pos];
  69. var whitespaceChars = "\n";
  70. return (whitespaceChars.indexOf(str) != -1);
  71. },
  72. preceding: function(pos){
  73. //console.group("[AttributedStringIterator.preceding]");
  74. for(var i in this.startWordOffsets) {
  75. var startWordOffset = this.startWordOffsets[i];
  76. if (pos < startWordOffset && i>0) {
  77. //console.log("startWordOffset: " + this.startWordOffsets[i-1]);
  78. //console.groupEnd();
  79. return this.startWordOffsets[i-1];
  80. }
  81. }
  82. //console.log("pos: " + pos);
  83. //console.groupEnd();
  84. return this.startWordOffsets[i];
  85. },
  86. following: function(pos){
  87. //console.group("[AttributedStringIterator.following]");
  88. for(var i in this.startWordOffsets) {
  89. var startWordOffset = this.startWordOffsets[i];
  90. if (pos < startWordOffset && i>0) {
  91. //console.log("startWordOffset: " + this.startWordOffsets[i]);
  92. //console.groupEnd();
  93. return this.startWordOffsets[i];
  94. }
  95. }
  96. //console.log("pos: " + pos);
  97. //console.groupEnd();
  98. return this.startWordOffsets[i];
  99. },
  100. ltrim: function(str){
  101. var patt2=/^\s+/g;
  102. return str.replace(patt2, "");
  103. },
  104. rtrim: function(str){
  105. var patt2=/\s+$/g;
  106. return str.replace(patt2, "");
  107. },
  108. getLayout: function(start, limit){
  109. return this.text.substr(start, limit - start);
  110. },
  111. getCharAtPos: function(pos) {
  112. return this.text[pos];
  113. }
  114. };
  115. var LineBreakMeasurer = function(paper, x, y, text, fontAttrs){
  116. this.paper = paper;
  117. this.text = new AttributedStringIterator(text);
  118. this.fontAttrs = fontAttrs;
  119. if (this.text.getEndIndex() - this.text.getBeginIndex() < 1) {
  120. throw {message: "Text must contain at least one character.", code: "IllegalArgumentException"};
  121. }
  122. //this.measurer = new TextMeasurer(paper, this.text, this.fontAttrs);
  123. this.limit = this.text.getEndIndex();
  124. this.pos = this.start = this.text.getBeginIndex();
  125. this.rafaelTextObject = this.paper.text(x, y, this.text.text).attr(fontAttrs).attr("text-anchor", "start");
  126. this.svgTextObject = this.rafaelTextObject[0];
  127. };
  128. LineBreakMeasurer.prototype = {
  129. nextOffset: function(wrappingWidth, offsetLimit, requireNextWord) {
  130. //console.group("[nextOffset]");
  131. var nextOffset = this.pos;
  132. if (this.pos < this.limit) {
  133. if (offsetLimit <= this.pos) {
  134. throw {message: "offsetLimit must be after current position", code: "IllegalArgumentException"};
  135. }
  136. var charAtMaxAdvance = this.getLineBreakIndex(this.pos, wrappingWidth);
  137. //charAtMaxAdvance --;
  138. //console.log("charAtMaxAdvance:", charAtMaxAdvance, ", [" + this.text.getCharAtPos(charAtMaxAdvance) + "]");
  139. if (charAtMaxAdvance == this.limit) {
  140. nextOffset = this.limit;
  141. //console.log("charAtMaxAdvance == this.limit");
  142. } else if (this.text.isNewLine(charAtMaxAdvance)) {
  143. //console.log("isNewLine");
  144. nextOffset = charAtMaxAdvance+1;
  145. } else if (this.text.isWhitespace(charAtMaxAdvance)) {
  146. // TODO: find next noSpaceChar
  147. //return nextOffset;
  148. nextOffset = this.text.following(charAtMaxAdvance);
  149. } else {
  150. // Break is in a word; back up to previous break.
  151. /*
  152. var testPos = charAtMaxAdvance + 1;
  153. if (testPos == this.limit) {
  154. console.error("hbz...");
  155. } else {
  156. nextOffset = this.text.preceding(charAtMaxAdvance);
  157. }
  158. */
  159. nextOffset = this.text.preceding(charAtMaxAdvance);
  160. if (nextOffset <= this.pos) {
  161. nextOffset = Math.max(this.pos+1, charAtMaxAdvance);
  162. }
  163. }
  164. }
  165. if (nextOffset > offsetLimit) {
  166. nextOffset = offsetLimit;
  167. }
  168. //console.log("nextOffset: " + nextOffset);
  169. //console.groupEnd();
  170. return nextOffset;
  171. },
  172. nextLayout: function(wrappingWidth) {
  173. //console.groupCollapsed("[nextLayout]");
  174. if (this.pos < this.limit) {
  175. var requireNextWord = false;
  176. var layoutLimit = this.nextOffset(wrappingWidth, this.limit, requireNextWord);
  177. //console.log("layoutLimit:", layoutLimit);
  178. if (layoutLimit == this.pos) {
  179. //console.groupEnd();
  180. return null;
  181. }
  182. var result = this.text.getLayout(this.pos, layoutLimit);
  183. //console.log("layout: \"" + result + "\"");
  184. // remove end of line
  185. //var posEndOfLine = this.text.getEndIndex(this.pos);
  186. //if (posEndOfLine < result.length)
  187. // result = result.substr(0, posEndOfLine);
  188. this.pos = layoutLimit;
  189. //console.groupEnd();
  190. return result;
  191. } else {
  192. //console.groupEnd();
  193. return null;
  194. }
  195. },
  196. getLineBreakIndex: function(pos, wrappingWidth) {
  197. //console.group("[getLineBreakIndex]");
  198. //console.log("pos:"+pos + ", text: \""+ this.text.text.replace(/\n/g, "_").substr(pos, 1) + "\"");
  199. var bb = this.rafaelTextObject.getBBox();
  200. var charNum = -1;
  201. try {
  202. var svgPoint = this.svgTextObject.getStartPositionOfChar(pos);
  203. //var dot = this.paper.ellipse(svgPoint.x, svgPoint.y, 1, 1).attr({"stroke-width": 0, fill: Color.blue});
  204. svgPoint.x = svgPoint.x + wrappingWidth;
  205. //svgPoint.y = bb.y;
  206. //console.log("svgPoint:", svgPoint);
  207. //var dot = this.paper.ellipse(svgPoint.x, svgPoint.y, 1, 1).attr({"stroke-width": 0, fill: Color.red});
  208. charNum = this.svgTextObject.getCharNumAtPosition(svgPoint);
  209. } catch (e){
  210. console.warn("getStartPositionOfChar error, pos:" + pos);
  211. /*
  212. var testPos = pos + 1;
  213. if (testPos < this.limit) {
  214. return testPos
  215. }
  216. */
  217. }
  218. //console.log("charNum:", charNum);
  219. if (charNum == -1) {
  220. //console.groupEnd();
  221. return this.text.getEndIndex(pos);
  222. } else {
  223. // When case there is new line between pos and charnum then use this new line
  224. var newLineIndex = this.text.getEndIndex(pos);
  225. if (newLineIndex < charNum ) {
  226. console.log("newLineIndex <= charNum, newLineIndex:"+newLineIndex+", charNum:"+charNum, "\"" + this.text.text.substr(newLineIndex+1).replace(/\n/g, "?") + "\"");
  227. //console.groupEnd();
  228. return newLineIndex;
  229. }
  230. //var charAtMaxAdvance = this.text.text.substring(charNum, charNum + 1);
  231. var charAtMaxAdvance = this.text.getCharAtPos(charNum);
  232. //console.log("!!charAtMaxAdvance: " + charAtMaxAdvance);
  233. //console.groupEnd();
  234. return charNum;
  235. }
  236. },
  237. getPosition: function() {
  238. return this.pos;
  239. }
  240. };