1. 1 : /**
  2. 2 : * The Reader tool provides a way of reading documents in the corpus, text is fetched as needed.
  3. 3 : *
  4. 4 : * @example
  5. 5 : *
  6. 6 : * let config = {
  7. 7 : * "limit": null,
  8. 8 : * "query": null,
  9. 9 : * "skipTodocId": null,
  10. 10 : * "start": null
  11. 11 : * };
  12. 12 : *
  13. 13 : * loadCorpus("austen").tool("Reader", config);
  14. 14 : *
  15. 15 : * @class Reader
  16. 16 : * @tutorial reader
  17. 17 : * @memberof Tools
  18. 18 : */
  19. 19 : Ext.define('Voyant.panel.Reader', {
  20. 20 : extend: 'Ext.panel.Panel',
  21. 21 : requires: ['Voyant.data.store.Tokens'],
  22. 22 : mixins: ['Voyant.panel.Panel'],
  23. 23 : alias: 'widget.reader',
  24. 24 : isConsumptive: true,
  25. 25 : statics: {
  26. 26 : i18n: {
  27. 27 : highlightEntities: 'Highlight Entities',
  28. 28 : entityType: 'entity type',
  29. 29 : nerVoyant: 'Entity Identification with Voyant',
  30. 30 : nerNssi: 'Entity Identification with NSSI',
  31. 31 : nerSpacy: 'Entity Identification with SpaCy'
  32. 32 : },
  33. 33 : api: {
  34. 34 : /**
  35. 35 : * @memberof Tools.Reader
  36. 36 : * @instance
  37. 37 : * @property {start}
  38. 38 : * @default
  39. 39 : */
  40. 40 : start: 0,
  41. 41 :
  42. 42 : /**
  43. 43 : * @memberof Tools.Reader
  44. 44 : * @instance
  45. 45 : * @property {limit}
  46. 46 : * @default
  47. 47 : */
  48. 48 : limit: 1000,
  49. 49 :
  50. 50 : /**
  51. 51 : * @memberof Tools.Reader
  52. 52 : * @instance
  53. 53 : * @property {String} skipToDocId The document ID to start reading from, defaults to the first document in the corpus.
  54. 54 : */
  55. 55 : skipToDocId: undefined,
  56. 56 :
  57. 57 : /**
  58. 58 : * @memberof Tools.Reader
  59. 59 : * @instance
  60. 60 : * @property {query}
  61. 61 : */
  62. 62 : query: undefined
  63. 63 : },
  64. 64 : glyph: 'xf0f6@FontAwesome'
  65. 65 : },
  66. 66 : config: {
  67. 67 : innerContainer: undefined,
  68. 68 : tokensStore: undefined, // for loading the tokens to display in the reader
  69. 69 : documentsStore: undefined, // for storing a copy of the corpus document models
  70. 70 : documentTermsStore: undefined, // for getting document term positions for highlighting
  71. 71 : documentEntitiesStore: undefined, // for storing the results of an entities call
  72. 72 : enableEntitiesList: true, // set to false when using reader as part of entitiesset
  73. 73 : exportVisualization: false,
  74. 74 : lastScrollTop: 0,
  75. 75 : scrollIntoView: false,
  76. 76 : insertWhere: 'beforeEnd',
  77. 77 : lastLocationUpdate: new Date(),
  78. 78 : options: [{xtype: 'stoplistoption'},{xtype: 'categoriesoption'}]
  79. 79 : },
  80. 80 :
  81. 81 : SCROLL_UP: -1,
  82. 82 : SCROLL_EQ: 0,
  83. 83 : SCROLL_DOWN: 1,
  84. 84 :
  85. 85 : LOCATION_UPDATE_FREQ: 100,
  86. 86 :
  87. 87 : INITIAL_LIMIT: 1000, // need to keep track since limit can be changed when scrolling,
  88. 88 :
  89. 89 : MAX_TOKENS_FOR_NER: 100000, // upper limit on document size for ner submission
  90. 90 :
  91. 91 : constructor: function(config) {
  92. 92 : this.mixins['Voyant.util.Api'].constructor.apply(this, arguments);
  93. 93 : this.callParent(arguments);
  94. 94 : this.mixins['Voyant.panel.Panel'].constructor.apply(this, arguments);
  95. 95 : },
  96. 96 :
  97. 97 : initComponent: function(config) {
  98. 98 : var tokensStore = Ext.create("Voyant.data.store.Tokens", {
  99. 99 : parentTool: this,
  100. 100 : proxy: {
  101. 101 : extraParams: {
  102. 102 : forTool: 'reader'
  103. 103 : }
  104. 104 : }
  105. 105 : })
  106. 106 : var me = this;
  107. 107 : tokensStore.on("beforeload", function(store) {
  108. 108 : return me.hasCorpusAccess(store.getCorpus());
  109. 109 : })
  110. 110 : tokensStore.on("load", function(s, records, success) {
  111. 111 : if (success) {
  112. 112 : var contents = "";
  113. 113 : var documentFrequency = this.localize("documentFrequency");
  114. 114 : var isPlainText = false;
  115. 115 : var docIndex = -1;
  116. 116 : var isLastNewLine = false;
  117. 117 : records.forEach(function(record) {
  118. 118 : if (record.getPosition()==0) {
  119. 119 : contents+="<h3>"+this.getDocumentsStore().getById(record.getDocId()).getFullLabel()+"</h3>";
  120. 120 : }
  121. 121 : if (record.getDocIndex()!=docIndex) {
  122. 122 : isPlainText = this.getDocumentsStore().getById(record.getDocId()).isPlainText();
  123. 123 : docIndex = record.getDocIndex();
  124. 124 : }
  125. 125 : if (record.isWord()) {
  126. 126 : isLastNewLine = false;
  127. 127 : contents += "<span class='word' id='"+ record.getId() + "' data-qtip='<div class=\"freq\">"+documentFrequency+" "+record.getDocumentRawFreq()+"</div>'>"+ record.getTerm() + "</span>";
  128. 128 : }
  129. 129 : else {
  130. 130 : var newContents = record.getTermWithLineSpacing(isPlainText);
  131. 131 : var isNewLine = newContents.indexOf("<br />")==0;
  132. 132 : if (isLastNewLine && (isNewLine || newContents.trim().length==0)) {}
  133. 133 : else {
  134. 134 : contents += newContents;
  135. 135 : isLastNewLine = isNewLine;
  136. 136 : }
  137. 137 : }
  138. 138 : }, this);
  139. 139 : this.updateText(contents);
  140. 140 :
  141. 141 : this.highlightKeywords();
  142. 142 :
  143. 143 : if (this.getDocumentEntitiesStore() !== undefined) {
  144. 144 : this.highlightEntities();
  145. 145 : }
  146. 146 : }
  147. 147 : }, this);
  148. 148 : this.setTokensStore(tokensStore);
  149. 149 :
  150. 150 : this.on("query", function(src, queries) {
  151. 151 : this.loadQueryTerms(queries);
  152. 152 : }, this);
  153. 153 :
  154. 154 : this.setDocumentTermsStore(Ext.create("Ext.data.Store", {
  155. 155 : model: "Voyant.data.model.DocumentTerm",
  156. 156 : autoLoad: false,
  157. 157 : remoteSort: false,
  158. 158 : proxy: {
  159. 159 : type: 'ajax',
  160. 160 : url: Voyant.application.getTromboneUrl(),
  161. 161 : extraParams: {
  162. 162 : tool: 'corpus.DocumentTerms',
  163. 163 : withPositions: true,
  164. 164 : bins: 25,
  165. 165 : forTool: 'reader'
  166. 166 : },
  167. 167 : reader: {
  168. 168 : type: 'json',
  169. 169 : rootProperty: 'documentTerms.terms',
  170. 170 : totalProperty: 'documentTerms.total'
  171. 171 : },
  172. 172 : simpleSortMode: true
  173. 173 : },
  174. 174 : listeners: {
  175. 175 : load: function(store, records, successful, opts) {
  176. 176 : this.highlightKeywords(records);
  177. 177 : },
  178. 178 : scope: this
  179. 179 : }
  180. 180 : }));
  181. 181 :
  182. 182 : this.on("afterrender", function() {
  183. 183 : var centerPanel = this.down('panel[region="center"]');
  184. 184 : this.setInnerContainer(centerPanel.getLayout().getRenderTarget());
  185. 185 :
  186. 186 : // scroll listener
  187. 187 : centerPanel.body.on("scroll", function(event, target) {
  188. 188 : var scrollDir = this.getLastScrollTop() < target.scrollTop ? this.SCROLL_DOWN
  189. 189 : : this.getLastScrollTop() > target.scrollTop ? this.SCROLL_UP
  190. 190 : : this.SCROLL_EQ;
  191. 191 :
  192. 192 : // scroll up
  193. 193 : if (scrollDir == this.SCROLL_UP && target.scrollTop < 1) {
  194. 194 : this.fetchPrevious(true);
  195. 195 : // scroll down
  196. 196 : } else if (scrollDir == this.SCROLL_DOWN && target.scrollHeight - target.scrollTop < target.offsetHeight*1.5) {//target.scrollTop+target.offsetHeight>target.scrollHeight/2) { // more than half-way down
  197. 197 : this.fetchNext(false);
  198. 198 : } else {
  199. 199 : var amount;
  200. 200 : if (target.scrollTop == 0) {
  201. 201 : amount = 0;
  202. 202 : } else if (target.scrollHeight - target.scrollTop == target.clientHeight) {
  203. 203 : amount = 1;
  204. 204 : } else {
  205. 205 : amount = (target.scrollTop + target.clientHeight * 0.5) / target.scrollHeight;
  206. 206 : }
  207. 207 :
  208. 208 : var now = new Date();
  209. 209 : if (now - this.getLastLocationUpdate() > this.LOCATION_UPDATE_FREQ || amount == 0 || amount == 1) {
  210. 210 : this.updateLocationMarker(amount, scrollDir);
  211. 211 : }
  212. 212 : }
  213. 213 : this.setLastScrollTop(target.scrollTop);
  214. 214 : }, this);
  215. 215 :
  216. 216 : // click listener
  217. 217 : centerPanel.body.on("click", function(event, target) {
  218. 218 : target = Ext.get(target);
  219. 219 : // if (target.hasCls('entity')) {} TODO
  220. 220 : if (target.hasCls('word')) {
  221. 221 : var info = Voyant.data.model.Token.getInfoFromElement(target);
  222. 222 : var term = target.getHtml();
  223. 223 : var data = [{
  224. 224 : term: term,
  225. 225 : docIndex: info.docIndex
  226. 226 : }];
  227. 227 : this.loadQueryTerms([term]);
  228. 228 : this.getApplication().dispatchEvent('termsClicked', this, data);
  229. 229 : }
  230. 230 : }, this);
  231. 231 :
  232. 232 : if (this.getCorpus()) {
  233. 233 : if (this.getApiParam('skipToDocId') === undefined) {
  234. 234 : this.setApiParam('skipToDocId', this.getCorpus().getDocument(0).getId());
  235. 235 : }
  236. 236 : this.load();
  237. 237 : var query = this.getApiParam('query');
  238. 238 : if (query) {
  239. 239 : this.loadQueryTerms(Ext.isString(query) ? [query] : query);
  240. 240 : }
  241. 241 : }
  242. 242 : this.on("loadedCorpus", function() {
  243. 243 : if (this.getApiParam('skipToDocId') === undefined) {
  244. 244 : this.setApiParam('skipToDocId', this.getCorpus().getDocument(0).getId());
  245. 245 : }
  246. 246 : this.load(true); // make sure to clear in case we're replacing the corpus
  247. 247 : var query = this.getApiParam('query');
  248. 248 : if (query) {
  249. 249 : this.loadQueryTerms(Ext.isString(query) ? [query] : query);
  250. 250 : }
  251. 251 : }, this);
  252. 252 : }, this);
  253. 253 :
  254. 254 : Ext.apply(this, {
  255. 255 : title: this.localize('title'),
  256. 256 : cls: 'voyant-reader',
  257. 257 : layout: 'fit',
  258. 258 : items: {
  259. 259 : layout: 'border',
  260. 260 : items: [{
  261. 261 : bodyPadding: 10,
  262. 262 : region: 'center',
  263. 263 : border: false,
  264. 264 : autoScroll: true,
  265. 265 : html: '<div class="readerContainer"></div>'
  266. 266 : },{
  267. 267 : xtype: 'readergraph',
  268. 268 : region: 'south',
  269. 269 : weight: 0,
  270. 270 : height: 30,
  271. 271 : split: {
  272. 272 : size: 2
  273. 273 : },
  274. 274 : splitterResize: true,
  275. 275 : border: false,
  276. 276 : listeners: {
  277. 277 : documentRelativePositionSelected: function(src, data) {
  278. 278 : var doc = this.getDocumentsStore().getAt(data.docIndex);
  279. 279 : var totalTokens = doc.get('tokensCount-lexical');
  280. 280 : var position = Math.floor(totalTokens * data.fraction);
  281. 281 : var bufferPosition = position - (this.getApiParam('limit')/2);
  282. 282 : this.setApiParams({'skipToDocId': doc.getId(), start: bufferPosition < 0 ? 0 : bufferPosition});
  283. 283 : this.load(true);
  284. 284 : },
  285. 285 : scope: this
  286. 286 : }
  287. 287 : },{
  288. 288 : xtype: 'entitieslist',
  289. 289 : region: 'east',
  290. 290 : weight: 10,
  291. 291 : width: '40%',
  292. 292 : split: {
  293. 293 : size: 2
  294. 294 : },
  295. 295 : splitterResize: true,
  296. 296 : border: false,
  297. 297 : hidden: true,
  298. 298 : collapsible: true,
  299. 299 : animCollapse: false
  300. 300 : }]
  301. 301 : },
  302. 302 :
  303. 303 : dockedItems: [{
  304. 304 : dock: 'bottom',
  305. 305 : xtype: 'toolbar',
  306. 306 : overflowHandler: 'scroller',
  307. 307 : items: [{
  308. 308 : glyph: 'xf060@FontAwesome',
  309. 309 : handler: function() {
  310. 310 : this.fetchPrevious(true);
  311. 311 : },
  312. 312 : scope: this
  313. 313 : },{
  314. 314 : glyph: 'xf061@FontAwesome',
  315. 315 : handler: function() {
  316. 316 : this.fetchNext(true);
  317. 317 : },
  318. 318 : scope: this
  319. 319 : },{xtype: 'tbseparator'},{
  320. 320 : xtype: 'querysearchfield'
  321. 321 : },'->',{
  322. 322 : glyph: 'xf0eb@FontAwesome',
  323. 323 : tooltip: this.localize('highlightEntities'),
  324. 324 : itemId: 'nerServiceParent',
  325. 325 : hidden: true,
  326. 326 : menu: {
  327. 327 : items: [{
  328. 328 : xtype: 'menucheckitem',
  329. 329 : group: 'nerService',
  330. 330 : text: this.localize('nerSpacy'),
  331. 331 : itemId: 'spacy',
  332. 332 : checked: true,
  333. 333 : handler: this.nerServiceHandler,
  334. 334 : scope: this
  335. 335 : },{
  336. 336 : xtype: 'menucheckitem',
  337. 337 : group: 'nerService',
  338. 338 : text: this.localize('nerNssi'),
  339. 339 : itemId: 'nssi',
  340. 340 : checked: false,
  341. 341 : handler: this.nerServiceHandler,
  342. 342 : scope: this
  343. 343 : },{
  344. 344 : xtype: 'menucheckitem',
  345. 345 : group: 'nerService',
  346. 346 : text: this.localize('nerVoyant'),
  347. 347 : itemId: 'stanford',
  348. 348 : checked: false,
  349. 349 : handler: this.nerServiceHandler,
  350. 350 : scope: this
  351. 351 : }
  352. 352 : // ,{
  353. 353 : // xtype: 'menucheckitem',
  354. 354 : // group: 'nerService',
  355. 355 : // text: 'NER with Voyant (OpenNLP)',
  356. 356 : // itemId: 'opennlp',
  357. 357 : // checked: false,
  358. 358 : // handler: this.nerServiceHandler,
  359. 359 : // scope: this
  360. 360 : // }
  361. 361 : ]
  362. 362 : }
  363. 363 : }]
  364. 364 : }],
  365. 365 : listeners: {
  366. 366 : loadedCorpus: function(src, corpus) {
  367. 367 : this.getTokensStore().setCorpus(corpus);
  368. 368 : this.getDocumentTermsStore().getProxy().setExtraParam('corpus', corpus.getId());
  369. 369 :
  370. 370 : var docs = corpus.getDocuments();
  371. 371 : this.setDocumentsStore(docs);
  372. 372 :
  373. 373 : if (this.rendered) {
  374. 374 : this.load();
  375. 375 : if (this.hasCorpusAccess(corpus)==false) {
  376. 376 : this.mask(this.localize("limitedAccess"), 'mask-no-spinner')
  377. 377 : }
  378. 378 : var query = this.getApiParam('query');
  379. 379 : if (query) {
  380. 380 : this.loadQueryTerms(Ext.isString(query) ? [query] : query);
  381. 381 : }
  382. 382 : }
  383. 383 :
  384. 384 : },
  385. 385 : termsClicked: function(src, terms) {
  386. 386 : var queryTerms = [];
  387. 387 : terms.forEach(function(term) {
  388. 388 : if (Ext.isString(term)) {queryTerms.push(term);}
  389. 389 : else if (term.term) {queryTerms.push(term.term);}
  390. 390 : else if (term.getTerm) {queryTerms.push(term.getTerm());}
  391. 391 : });
  392. 392 : if (queryTerms.length > 0) {
  393. 393 : this.loadQueryTerms(queryTerms);
  394. 394 : }
  395. 395 : },
  396. 396 : corpusTermsClicked: function(src, terms) {
  397. 397 : var queryTerms = [];
  398. 398 : terms.forEach(function(term) {
  399. 399 : if (term.getTerm()) {queryTerms.push(term.getTerm());}
  400. 400 : });
  401. 401 : this.loadQueryTerms(queryTerms);
  402. 402 : },
  403. 403 : documentTermsClicked: function(src, terms) {
  404. 404 : var queryTerms = [];
  405. 405 : terms.forEach(function(term) {
  406. 406 : if (term.getTerm()) {queryTerms.push(term.getTerm());}
  407. 407 : });
  408. 408 : this.loadQueryTerms(queryTerms);
  409. 409 : },
  410. 410 : documentSelected: function(src, document) {
  411. 411 : var corpus = this.getTokensStore().getCorpus();
  412. 412 : var doc = corpus.getDocument(document);
  413. 413 : this.setApiParams({'skipToDocId': doc.getId(), start: 0});
  414. 414 : this.load(true);
  415. 415 : },
  416. 416 : documentsClicked: function(src, documents, corpus) {
  417. 417 : if (documents.length > 0) {
  418. 418 : var doc = documents[0];
  419. 419 : this.setApiParams({'skipToDocId': doc.getId(), start: 0});
  420. 420 : this.load(true);
  421. 421 : }
  422. 422 : },
  423. 423 : termLocationClicked: function(src, terms) {
  424. 424 : if (terms[0] !== undefined) {
  425. 425 : var term = terms[0];
  426. 426 : var docIndex = term.get('docIndex');
  427. 427 : var position = term.get('position');
  428. 428 : this.showTermLocation(docIndex, position, term);
  429. 429 : };
  430. 430 : },
  431. 431 : documentIndexTermsClicked: function(src, terms) {
  432. 432 : if (terms[0] !== undefined) {
  433. 433 : var term = terms[0];
  434. 434 : var termRec = Ext.create('Voyant.data.model.Token', term);
  435. 435 : this.fireEvent('termLocationClicked', this, [termRec]);
  436. 436 : }
  437. 437 : },
  438. 438 : entityResults: function(src, entities) {
  439. 439 : if (entities !== null) {
  440. 440 : this.clearEntityHighlights(); // clear again in case failed documents were rerun
  441. 441 : this.setDocumentEntitiesStore(entities);
  442. 442 : this.highlightEntities();
  443. 443 : if (this.getEnableEntitiesList()) {
  444. 444 : this.down('entitieslist').expand().show();
  445. 445 : }
  446. 446 : }
  447. 447 : },
  448. 448 : entitiesClicked: function(src, entities) {
  449. 449 : if (entities[0] !== undefined) {
  450. 450 : var entity = entities[0];
  451. 451 : var docIndex = entity.get('docIndex');
  452. 452 : var position = entity.get('positions')[0];
  453. 453 : if (Array.isArray(position)) position = position[0];
  454. 454 : this.showTermLocation(docIndex, position, entity);
  455. 455 : }
  456. 456 : },
  457. 457 : entityLocationClicked: function(src, entity, positionIndex) {
  458. 458 : var docIndex = entity.get('docIndex');
  459. 459 : var position = entity.get('positions')[positionIndex];
  460. 460 : if (Array.isArray(position)) position = position[0];
  461. 461 : this.showTermLocation(docIndex, position, entity);
  462. 462 : },
  463. 463 : scope: this
  464. 464 : }
  465. 465 : });
  466. 466 :
  467. 467 : this.callParent(arguments);
  468. 468 : },
  469. 469 :
  470. 470 : loadQueryTerms: function(queryTerms) {
  471. 471 : if (queryTerms && queryTerms.length > 0) {
  472. 472 : var docId = this.getApiParam('skipToDocId');
  473. 473 : if (docId === undefined) {
  474. 474 : var docIndex = 0;
  475. 475 : var locationInfo = this.getLocationInfo();
  476. 476 : if (locationInfo) {
  477. 477 : docIndex = locationInfo[0].docIndex;
  478. 478 : }
  479. 479 : docId = this.getCorpus().getDocument(docIndex).getId();
  480. 480 : }
  481. 481 : this.getDocumentTermsStore().load({
  482. 482 : params: {
  483. 483 : query: queryTerms,
  484. 484 : docId: docId,
  485. 485 : categories: this.getApiParam('categories'),
  486. 486 : limit: -1
  487. 487 : }
  488. 488 : });
  489. 489 : this.down('readergraph').loadQueryTerms(queryTerms);
  490. 490 : }
  491. 491 : },
  492. 492 :
  493. 493 : showTermLocation: function(docIndex, position, term) {
  494. 494 : var bufferPosition = position - (this.getApiParam('limit')/2);
  495. 495 : var doc = this.getCorpus().getDocument(docIndex);
  496. 496 : this.setApiParams({'skipToDocId': doc.getId(), start: bufferPosition < 0 ? 0 : bufferPosition});
  497. 497 : this.load(true, {
  498. 498 : callback: function() {
  499. 499 : var el = this.body.dom.querySelector("#_" + docIndex + "_" + position);
  500. 500 : if (el) {
  501. 501 : el.scrollIntoView({
  502. 502 : block: 'center'
  503. 503 : });
  504. 504 : Ext.fly(el).frame('#f80');
  505. 505 : }
  506. 506 : if (term.get('type')) {
  507. 507 : this.highlightEntities();
  508. 508 : } else {
  509. 509 : this.highlightKeywords(term, false);
  510. 510 : }
  511. 511 : },
  512. 512 : scope: this
  513. 513 : });
  514. 514 : },
  515. 515 :
  516. 516 : highlightKeywords: function(termRecords, doScroll) {
  517. 517 : var container = this.getInnerContainer().first();
  518. 518 : container.select('span[class*=keyword]').removeCls('keyword').applyStyles({backgroundColor: 'transparent', color: 'black'});
  519. 519 :
  520. 520 : if (termRecords === undefined && this.getDocumentTermsStore().getCount() > 0) {
  521. 521 : termRecords = this.getDocumentTermsStore().getData().items;
  522. 522 : }
  523. 523 : if (termRecords === undefined) {
  524. 524 : return;
  525. 525 : }
  526. 526 :
  527. 527 : if (!Ext.isArray(termRecords)) termRecords = [termRecords];
  528. 528 :
  529. 529 : termRecords.forEach(function(r) {
  530. 530 : var term = r.get('term');
  531. 531 : var bgColor = this.getApplication().getColorForTerm(term);
  532. 532 : var textColor = this.getApplication().getTextColorForBackground(bgColor);
  533. 533 : bgColor = 'rgb('+bgColor.join(',')+') !important';
  534. 534 : textColor = 'rgb('+textColor.join(',')+') !important';
  535. 535 : var styles = 'background-color:'+bgColor+';color:'+textColor+';';
  536. 536 :
  537. 537 : // might be slightly faster to use positions so do that if they're available
  538. 538 : if (r.get('positions')) {
  539. 539 : var positions = r.get('positions');
  540. 540 : var docIndex = r.get('docIndex');
  541. 541 :
  542. 542 : positions.forEach(function(pos) {
  543. 543 : var match = container.dom.querySelector('#_'+docIndex+'_'+pos);
  544. 544 : if (match) {
  545. 545 : Ext.fly(match).addCls('keyword').dom.setAttribute('style', styles);
  546. 546 : }
  547. 547 : })
  548. 548 : } else {
  549. 549 : var caseInsensitiveQuery = new RegExp('^'+term+'$', 'i');
  550. 550 : var nodes = container.select('span.word');
  551. 551 : nodes.each(function(el, compEl, index) {
  552. 552 : if (el.dom.firstChild && el.dom.firstChild.nodeValue.match(caseInsensitiveQuery)) {
  553. 553 : el.addCls('keyword').dom.setAttribute('style', styles);
  554. 554 : }
  555. 555 : });
  556. 556 : }
  557. 557 : }, this);
  558. 558 : },
  559. 559 :
  560. 560 : nerServiceHandler: function(menuitem) {
  561. 561 : var annotator = menuitem.itemId;
  562. 562 :
  563. 563 : var docIndex = [];
  564. 564 : var locationInfo = this.getLocationInfo();
  565. 565 : if (locationInfo) {
  566. 566 : for (var i = locationInfo[0].docIndex; i <= locationInfo[1].docIndex; i++) {
  567. 567 : docIndex.push(i);
  568. 568 : }
  569. 569 : } else {
  570. 570 : docIndex.push(0);
  571. 571 : }
  572. 572 :
  573. 573 : this.clearEntityHighlights();
  574. 574 :
  575. 575 : var entitiesList = this.down('entitieslist');
  576. 576 : entitiesList.clearEntities();
  577. 577 : entitiesList.getEntities(annotator, docIndex);
  578. 578 : },
  579. 579 :
  580. 580 : clearEntityHighlights: function() {
  581. 581 : var container = this.getInnerContainer().first();
  582. 582 : container.select('.entity').each(function(el) {
  583. 583 : el.removeCls('entity start middle end location person organization misc money time percent date duration set unknown');
  584. 584 : el.dom.setAttribute('data-qtip', el.dom.getAttribute('data-qtip').replace(/<div class="entity">.*?<\/div>/g, ''));
  585. 585 : });
  586. 586 : },
  587. 587 :
  588. 588 : highlightEntities: function() {
  589. 589 : var container = this.getInnerContainer().first();
  590. 590 : var entities = this.getDocumentEntitiesStore();
  591. 591 : var entityTypeStr = this.localize('entityType');
  592. 592 : entities.forEach(function(entity) {
  593. 593 : var positionInstances = entity.positions;
  594. 594 : if (positionInstances) {
  595. 595 : positionInstances.forEach(function(positions) {
  596. 596 : var multiTermEntity = positions.length > 1;
  597. 597 : if (multiTermEntity) {
  598. 598 : // find the difference between start and end positions
  599. 599 : if (positions.length === 2 && positions[1]-positions[0] > 1) {
  600. 600 : // more than two terms, so fill in the middle positions
  601. 601 : var endPos = positions[1];
  602. 602 : var curPos = positions[0]+1;
  603. 603 : var curIndex = 1;
  604. 604 : while (curPos < endPos) {
  605. 605 : positions.splice(curIndex, 0, curPos);
  606. 606 : curPos++;
  607. 607 : curIndex++;
  608. 608 : }
  609. 609 : }
  610. 610 : }
  611. 611 :
  612. 612 : for (var i = 0, len = positions.length; i < len; i++) {
  613. 613 : var position = positions[i];
  614. 614 : if (position === -1) {
  615. 615 : console.warn('missing position for: '+entity.term);
  616. 616 : } else {
  617. 617 : var match = container.selectNode('#_'+entity.docIndex+'_'+position, false);
  618. 618 : if (match) {
  619. 619 : var termEntityPosition = '';
  620. 620 : if (multiTermEntity) {
  621. 621 : if (i === 0) {
  622. 622 : termEntityPosition = 'start ';
  623. 623 : } else if (i === len-1) {
  624. 624 : termEntityPosition = 'end ';
  625. 625 : } else {
  626. 626 : termEntityPosition = 'middle ';
  627. 627 : }
  628. 628 : }
  629. 629 :
  630. 630 : match.addCls('entity '+termEntityPosition+entity.type);
  631. 631 : var prevQTip = match.dom.getAttribute('data-qtip');
  632. 632 : if (prevQTip.indexOf('class="entity"') === -1) {
  633. 633 : match.dom.setAttribute('data-qtip', prevQTip+'<div class="entity">'+entityTypeStr+': '+entity.type+'</div>');
  634. 634 : }
  635. 635 : }
  636. 636 : }
  637. 637 : }
  638. 638 : });
  639. 639 : } else {
  640. 640 : console.warn('no positions for: '+entity.term);
  641. 641 : }
  642. 642 : });
  643. 643 : },
  644. 644 :
  645. 645 : fetchPrevious: function(scroll) {
  646. 646 : var readerContainer = this.getInnerContainer().first();
  647. 647 : var first = readerContainer.first('.word');
  648. 648 : if (first != null && first.hasCls("loading")===false) {
  649. 649 : while(first) {
  650. 650 : if (first.hasCls("word")) {
  651. 651 : var info = Voyant.data.model.Token.getInfoFromElement(first);
  652. 652 : var docIndex = info.docIndex;
  653. 653 : var start = info.position;
  654. 654 : var doc = this.getDocumentsStore().getAt(docIndex);
  655. 655 : var limit = this.getApiParam('limit');
  656. 656 : var getPrevDoc = false;
  657. 657 : if (docIndex === 0 && start === 0) {
  658. 658 : var scrollContainer = this.down('panel[region="center"]').body;
  659. 659 : var scrollNeeded = first.getScrollIntoViewXY(scrollContainer, scrollContainer.dom.scrollTop, scrollContainer.dom.scrollLeft);
  660. 660 : if (scrollNeeded.y != 0) {
  661. 661 : first.dom.scrollIntoView();
  662. 662 : }
  663. 663 : first.frame("red");
  664. 664 : break;
  665. 665 : }
  666. 666 : if (docIndex > 0 && start === 0) {
  667. 667 : getPrevDoc = true;
  668. 668 : docIndex--;
  669. 669 : doc = this.getDocumentsStore().getAt(docIndex);
  670. 670 : var totalTokens = doc.get('tokensCount-lexical');
  671. 671 : start = totalTokens-limit;
  672. 672 : if (start < 0) {
  673. 673 : start = 0;
  674. 674 : this.setApiParam('limit', totalTokens);
  675. 675 : }
  676. 676 : } else {
  677. 677 : limit--; // subtract one to limit for the word we're removing. need to do this to account for non-lexical tokens before/after first word.
  678. 678 : start -= limit;
  679. 679 : }
  680. 680 : if (start < 0) start = 0;
  681. 681 :
  682. 682 : var mask = first.insertSibling("<div class='loading'>"+this.localize('loading')+"</div>", 'before', false).mask();
  683. 683 : if (!getPrevDoc) {
  684. 684 : first.destroy();
  685. 685 : }
  686. 686 :
  687. 687 : var id = doc.getId();
  688. 688 : this.setApiParams({'skipToDocId': id, start: start});
  689. 689 : this.setInsertWhere('afterBegin')
  690. 690 : this.setScrollIntoView(scroll);
  691. 691 : this.load();
  692. 692 : this.setApiParam('limit', this.INITIAL_LIMIT);
  693. 693 : break;
  694. 694 : }
  695. 695 : first.destroy(); // remove non word
  696. 696 : first = readerContainer.first();
  697. 697 : }
  698. 698 : }
  699. 699 : },
  700. 700 :
  701. 701 : fetchNext: function(scroll) {
  702. 702 : var readerContainer = this.getInnerContainer().first();
  703. 703 : var last = readerContainer.last();
  704. 704 : if (last.hasCls("loading")===false) {
  705. 705 : while(last) {
  706. 706 : if (last.hasCls("word")) {
  707. 707 : var info = Voyant.data.model.Token.getInfoFromElement(last);
  708. 708 : var docIndex = info.docIndex;
  709. 709 : var start = info.position;
  710. 710 : var doc = this.getDocumentsStore().getAt(info.docIndex);
  711. 711 : var id = doc.getId();
  712. 712 :
  713. 713 : var totalTokens = doc.get('tokensCount-lexical');
  714. 714 : if (start + this.getApiParam('limit') >= totalTokens && docIndex == this.getCorpus().getDocumentsCount()-1) {
  715. 715 : var limit = totalTokens - start;
  716. 716 : if (limit <= 1) {
  717. 717 : last.dom.scrollIntoView();
  718. 718 : last.frame("red")
  719. 719 : break;
  720. 720 : } else {
  721. 721 : this.setApiParam('limit', limit);
  722. 722 : }
  723. 723 : }
  724. 724 :
  725. 725 : // remove any text after the last word
  726. 726 : var nextSib = last.dom.nextSibling;
  727. 727 : while(nextSib) {
  728. 728 : var oldNext = nextSib;
  729. 729 : nextSib = nextSib.nextSibling;
  730. 730 : oldNext.parentNode.removeChild(oldNext);
  731. 731 : }
  732. 732 :
  733. 733 : var mask = last.insertSibling("<div class='loading'>"+this.localize('loading')+"</div>", 'after', false).mask();
  734. 734 : last.destroy();
  735. 735 : this.setApiParams({'skipToDocId': id, start: info.position});
  736. 736 : this.setInsertWhere('beforeEnd');
  737. 737 : this.setScrollIntoView(scroll);
  738. 738 : this.load(); // callback not working on buffered store
  739. 739 : this.setApiParam('limit', this.INITIAL_LIMIT);
  740. 740 : break;
  741. 741 : }
  742. 742 : last.destroy(); // remove non word
  743. 743 : last = readerContainer.last();
  744. 744 : }
  745. 745 : }
  746. 746 : },
  747. 747 :
  748. 748 : load: function(doClear, config) {
  749. 749 : if (doClear) {
  750. 750 : this.getInnerContainer().first().destroy(); // clear everything
  751. 751 : this.getInnerContainer().setHtml('<div class="readerContainer"><div class="loading">'+this.localize('loading')+'</div></div>');
  752. 752 : this.getInnerContainer().first().first().mask();
  753. 753 : }
  754. 754 :
  755. 755 : // check if we're loading a different doc and update terms store if so
  756. 756 : var tokensStore = this.getTokensStore();
  757. 757 : if (tokensStore.lastOptions && tokensStore.lastOptions.params.skipToDocId && tokensStore.lastOptions.params.skipToDocId !== this.getApiParam('skipToDocId')) {
  758. 758 : var dts = this.getDocumentTermsStore();
  759. 759 : if (dts.lastOptions) {
  760. 760 : var query = dts.lastOptions.params.query;
  761. 761 : this.loadQueryTerms(query);
  762. 762 : }
  763. 763 : }
  764. 764 :
  765. 765 : this.getTokensStore().load(Ext.apply(config || {}, {
  766. 766 : params: Ext.apply(this.getApiParams(), {
  767. 767 : stripTags: 'blocksOnly',
  768. 768 : stopList: '' // token requests shouldn't have stopList
  769. 769 : })
  770. 770 : }));
  771. 771 : },
  772. 772 :
  773. 773 : updateText: function(contents) {
  774. 774 : var loadingMask = this.getInnerContainer().down('.loading');
  775. 775 : if (loadingMask) loadingMask.destroy();
  776. 776 : // FIXME: something is weird here in tool/Reader mode, this.getInnerContainer() seems empty but this.getInnerContainer().first() gets the canvas?!?
  777. 777 : var inserted = this.getInnerContainer().first().insertHtml(this.getInsertWhere()/* where is this defined? */, contents, true); // return Element, not dom
  778. 778 : if (inserted && this.getScrollIntoView()) {
  779. 779 : inserted.dom.scrollIntoView(); // use dom
  780. 780 : // we can't rely on the returned element because it can be a transient fly element, but the id is right in a deferred call
  781. 781 : Ext.Function.defer(function() {
  782. 782 : var el = Ext.get(inserted.id); // re-get el
  783. 783 : if (el) {el.frame("red")}
  784. 784 : }, 100);
  785. 785 : }
  786. 786 : var target = this.down('panel[region="center"]').body.dom;
  787. 787 : var amount;
  788. 788 : if (target.scrollTop == 0) {
  789. 789 : amount = 0;
  790. 790 : } else if (target.scrollHeight - target.scrollTop == target.clientHeight) {
  791. 791 : amount = 1;
  792. 792 : } else {
  793. 793 : amount = (target.scrollTop + target.clientHeight * 0.5) / target.scrollHeight;
  794. 794 : }
  795. 795 : this.updateLocationMarker(amount);
  796. 796 : },
  797. 797 :
  798. 798 : updateLocationMarker: function(amount, scrollDir) {
  799. 799 : var locationInfo = this.getLocationInfo();
  800. 800 : if (locationInfo) {
  801. 801 : var info1 = locationInfo[0];
  802. 802 : var info2 = locationInfo[1];
  803. 803 :
  804. 804 : var corpus = this.getCorpus();
  805. 805 : var partialFirstDoc = false;
  806. 806 :
  807. 807 : if (info1.position !== 0) {
  808. 808 : partialFirstDoc = true;
  809. 809 : }
  810. 810 :
  811. 811 : var docTokens = {};
  812. 812 : var totalTokens = 0;
  813. 813 : var showNerButton = this.getEnableEntitiesList() && this.getApplication().getEntitiesEnabled ? this.getApplication().getEntitiesEnabled() : false;
  814. 814 : var currIndex = info1.docIndex;
  815. 815 : while (currIndex <= info2.docIndex) {
  816. 816 : var tokens = corpus.getDocument(currIndex).get('tokensCount-lexical');
  817. 817 : if (tokens > this.MAX_TOKENS_FOR_NER) {
  818. 818 : showNerButton = false;
  819. 819 : }
  820. 820 : if (currIndex === info2.docIndex) {
  821. 821 : tokens = info2.position; // only count tokens up until last displayed word
  822. 822 : }
  823. 823 : if (currIndex === info1.docIndex) {
  824. 824 : tokens -= info1.position; // subtract missing tokens, if any
  825. 825 : }
  826. 826 : totalTokens += tokens;
  827. 827 : docTokens[currIndex] = tokens;
  828. 828 : currIndex++;
  829. 829 : }
  830. 830 :
  831. 831 : var nerParent = this.down('#nerServiceParent');
  832. 832 : if (showNerButton) {
  833. 833 : nerParent.show();
  834. 834 : } else {
  835. 835 : nerParent.hide();
  836. 836 : }
  837. 837 :
  838. 838 : var tokenPos = Math.round(totalTokens * amount);
  839. 839 : var docIndex = 0;
  840. 840 : var currToken = 0;
  841. 841 : for (var i = info1.docIndex; i <= info2.docIndex; i++) {
  842. 842 : docIndex = i;
  843. 843 : currToken += docTokens[i];
  844. 844 : if (currToken >= tokenPos) {
  845. 845 : break;
  846. 846 : }
  847. 847 : }
  848. 848 : var remains = (currToken - tokenPos);
  849. 849 : var tokenPosInDoc = docTokens[docIndex] - remains;
  850. 850 :
  851. 851 : if (partialFirstDoc && docIndex === info1.docIndex) {
  852. 852 : tokenPosInDoc += info1.position;
  853. 853 : }
  854. 854 :
  855. 855 : var fraction = tokenPosInDoc / corpus.getDocument(docIndex).get('tokensCount-lexical');
  856. 856 :
  857. 857 : this.down('readergraph').moveLocationMarker(docIndex, fraction, scrollDir);
  858. 858 : }
  859. 859 : },
  860. 860 :
  861. 861 : getLocationInfo: function() {
  862. 862 : var readerWords = Ext.DomQuery.select('.word', this.getInnerContainer().down('.readerContainer', true));
  863. 863 : var firstWord = readerWords[0];
  864. 864 : var lastWord = readerWords[readerWords.length-1];
  865. 865 : if (firstWord !== undefined && lastWord !== undefined) {
  866. 866 : var info1 = Voyant.data.model.Token.getInfoFromElement(firstWord);
  867. 867 : var info2 = Voyant.data.model.Token.getInfoFromElement(lastWord);
  868. 868 : return [info1, info2];
  869. 869 : } else {
  870. 870 : return null;
  871. 871 : }
  872. 872 : }
  873. 873 : });