1/* eslint-disable no-console */ 2import Entity from 'reader/entitymap/Entity'; 3import EntityMapView from 'reader/entitymap/EntityMapView'; 4import EntityPhysics from 'reader/entitymap/EntityPhysics'; 5import EntityMapCondenser from 'reader/entitymap/EntityMapCondenser'; 6import Request from 'reader/Request'; 7import { find, show, empty } from 'reader/helpers/dom-utils'; 8 9/* 10 * decaffeinate suggestions: 11 * DS102: Remove unnecessary code created because of implicit returns 12 * DS205: Consider reworking code to avoid use of IIFEs 13 * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md 14*/ 15 16const SELECTION_LEVEL_NONE = 0; 17const SELECTION_LEVEL_CLUSTER = 1; 18const SELECTION_LEVEL_NODE = 2; 19 20export default class EntityMapController { 21 constructor() { 22 this.childRadius = 20; 23 this.haltThreshold = 0.002; 24 this.tickDelay = 10; 25 this.condenserInterval = 50; 26 this.cliqueLimit = 4; 27 28 this.tickCount = 0; 29 30 this.div = $('#mapper'); 31 32 this.entities = {}; 33 34 this.links = []; 35 this.rootEntities = []; 36 37 this.rootEntityIds = this.div.data('entityId'); 38 this.maxChildren = this.div.data('children') || 3; 39 this.depth = this.div.data('depth') || 2; 40 this.attraction = this.div.data('att') || 0.5; 41 this.repulsion = this.div.data('rep') || 1000; 42 this.radius = this.div.data('radius') || 75; 43 this.mast = this.div.data('mast'); 44 45 this.lastRoot = null; 46 47 this.queue = []; 48 49 this.rootEntityIds.forEach((id) => { 50 this.queue.push({ id, depth: 0, source: null }); 51 }); 52 53 this.view = new EntityMapView(this, this.entities, this.div); 54 55 this.physics = new EntityPhysics(this.entities, this.attraction, this.repulsion); 56 this.condenser = new EntityMapCondenser(this.entities, this.radius); 57 this.notify(''); 58 59 this.running = true; 60 this.paused = false; 61 this.pendingResultsForResume = []; 62 this.selectionLevel = SELECTION_LEVEL_NONE; 63 64 this.retrieveLinks(); 65 66 this.tick(); 67 } 68 69 tick() { 70 if (!this.running) { return; } 71 72 this.tickCount += 1; 73 74 if ((this.tickCount % this.condenserInterval) === 0) { 75 this.condenser.condense(); 76 } 77 78 const temp = this.physics.tick(); 79 80 this.view.render(); 81 82 this.running = (temp > this.haltThreshold) || (this.queue.length > 0); 83 84 setTimeout((() => this.tick()), this.tickDelay); 85 } 86 87 retrieveLinks() { 88 if (this.paused) { 89 return; 90 } 91 92 if (this.queue.length === 0) { 93 console.log('Retrieval queue is empty'); 94 return; 95 } 96 97 const { source, depth } = this.queue[0]; 98 99 const nextEntities = []; 100 101 while ((this.queue.length > 0) && (this.queue[0].source === source)) { 102 nextEntities.push(this.queue.shift().id); 103 } 104 105 const controller = this; 106 107 new Request(`/articles/entitylinks.json?entities=${nextEntities.join(',')}&mast=${encodeURIComponent(this.mast)}`) 108 .json() 109 .then((data) => controller.processLinks(data, depth + 1, source)) 110 .catch((err) => { 111 console.log(err); 112 this.retrieveLinks(); 113 }); 114 } 115 116 processLinks(data, newDepth, source) { 117 if (this.paused) { 118 this.pendingResultsForResume.push({ 119 data: data, 120 newDepth: newDepth, 121 source: source 122 }); 123 124 return; 125 } 126 127 data.forEach((entry) => { 128 let entity = this.entities[entry.entity.id]; 129 130 if (!entity) { 131 if (this.wouldFormClique(entry)) { return; } 132 133 entity = new Entity( 134 entry.entity.id, 135 entry.entity.name, 136 entry.entity.entity_type, 137 entry.entity.ref_count, 138 ); 139 140 if (source) { 141 entity.location.x = source.location.x + (this.childRadius * (Math.random() - 0.5)); 142 entity.location.y = source.location.y + (2 * this.childRadius * (Math.random() - 0.5)); 143 entity.linkTo(source); 144 } 145 146 entity.root = this.rootEntityIds.includes(entity.id); 147 148 if (entity.root) { 149 this.rootEntities.forEach((er) => { 150 entity.linkTo(er); 151 }); 152 153 this.rootEntities.push(entity); 154 } 155 } 156 157 let newEntityCount = 0; 158 159 entry.links.forEach((link) => { 160 const entityDest = this.entities[link]; 161 162 if (entityDest) { 163 entity.linkTo(entityDest); 164 } else if (this.rootEntityIds.includes(link)) { 165 // Was previously a continue, left it in for parity. 166 167 return; 168 } else if ((newDepth <= this.depth) && (newEntityCount < this.maxChildren)) { 169 this.queue.push({ id: link, depth: newDepth, source: entity }); 170 171 newEntityCount += 1; 172 } 173 }); 174 175 this.entities[entity.id] = entity; 176 }); 177 178 if (!this.paused && !this.running) { 179 this.running = true; 180 this.tick(); 181 } 182 183 return this.retrieveLinks(); 184 } 185 186 wouldFormClique(entry) { 187 const existingEntities = new Set(); 188 const children = new Set(); 189 190 entry.links.forEach((link) => { 191 const entityDest = this.entities[link.entity]; 192 193 if (entityDest) { 194 existingEntities.add(entityDest); 195
196 Object.entries(entityDest.linkedEntities).forEach((id) => { 197 const child = entityDest.linkedEntities[id]; 198 children.add(child); 199 }); 200 } 201 }); 202 203 if (existingEntities.size < this.cliqueLimit) { return false; } 204 205 206 Object.keys(existingEntities).forEach((key) => { 207 const existingEntity = existingEntities[key]; 208 if (!children.has(existingEntity)) { return false; } 209 }); 210 211 return true; 212 } 213 214 handleEntityClicked(entity) { 215 $('#guide').slideUp(); 216 217 $('#entity-name').text(`${entity.name} (${entity.entityType})`); 218 show('#articles'); 219 empty('#article-list'); 220 221 const controller = this; 222 223 new Request(`/articles/for_entity.json?entity=${entity.id}&mast=${encodeURIComponent(this.mast)}`) 224 .json() 225 .then((data) => controller.processArticles(data, entity)) 226 .catch((err) => console.log(err)); 227 } 228 229 pause() { 230 this.running = false; 231 this.paused = true; 232 } 233 234 resume() { 235 if (!this.paused) { 236 return; 237 } 238 239 this.running = true; 240 this.paused = false; 241 242 this.pendingResultsForResume.forEach((response) => { 243 this.processLinks(response.data, response.newDepth, response.source); 244 }); 245 246 this.pendingResultsForResume = []; 247 248 this.tick(); 249 this.retrieveLinks(); 250 } 251 252 handleSelect() { 253 let entity = null; 254 255 switch (this.selectionLevel) { 256 case SELECTION_LEVEL_NONE: 257 this.pause(); 258 this.selectionLevel = SELECTION_LEVEL_CLUSTER; 259 this.condenser.clearNodeSelection(); 260 this.notifyCluster(this.condenser.selectNextCluster(false)); 261 break; 262 263 case SELECTION_LEVEL_CLUSTER: 264 this.selectionLevel = SELECTION_LEVEL_NODE; 265 this.notifyEntity(this.condenser.selectNextNode(false)); 266 break; 267 268 case SELECTION_LEVEL_NODE: 269 entity = this.condenser.selectedEntity; 270 271 if (entity) { 272 this.handleEntityClicked(entity); 273 } 274 275 break; 276 } 277 278 this.view.render(); 279 } 280 281 handleNext() { 282 switch (this.selectionLevel) { 283 case SELECTION_LEVEL_CLUSTER: 284 this.notifyCluster(this.condenser.selectNextCluster(false)); 285 break; 286 287 case SELECTION_LEVEL_NODE: 288 this.notifyEntity(this.condenser.selectNextNode(false)); 289 break; 290 } 291 292 this.view.render(); 293 } 294 295 handlePrevious() { 296 switch (this.selectionLevel) { 297 case SELECTION_LEVEL_CLUSTER: 298 this.notifyCluster(this.condenser.selectNextCluster(true)); 299 break; 300 301 case SELECTION_LEVEL_NODE: 302 this.notifyEntity(this.condenser.selectNextNode(true)); 303 break; 304 } 305 306 this.view.render(); 307 } 308 309 handleCancel() { 310 switch (this.selectionLevel) { 311 case SELECTION_LEVEL_CLUSTER: 312 this.condenser.clearClusterSelection(); 313 this.selectionLevel = SELECTION_LEVEL_NONE; 314 this.resume(); 315 this.notify(''); 316 break; 317 318 case SELECTION_LEVEL_NODE: 319 this.condenser.clearNodeSelection(); 320 this.selectionLevel = SELECTION_LEVEL_CLUSTER; 321 this.notifyCluster(this.condenser.selectedCluster); 322 break; 323 } 324 325 this.view.render(); 326 } 327 328 hasSelection() { 329 return this.selectionLevel != SELECTION_LEVEL_NONE; 330 } 331 332 clearSelection() { 333 this.condenser.clearNodeSelection(); 334 this.condenser.clearClusterSelection(); 335 this.selectionLevel = SELECTION_LEVEL_NONE; 336 } 337 338 notifyCluster(cluster) { 339 if (!cluster) { 340 this.notify(''); 341 return; 342 } 343 344 const plural = this.sIfPlural(cluster.entities); 345 346 this.notify(`${cluster.label()}. A cluster of ${cluster.entities.length} concept${plural}.`); 347 } 348 349 notifyEntity(entity) { 350 if (!entity) { 351 this.notify(''); 352 return; 353 } 354 355 const typeDescription = entity.entityType.toLowerCase().replaceAll("_", " "); 356 const article = (typeDescription.match(/^[aeiou]/)) ? 'an' : 'a'; 357 358 this.notify(`${entity.name}, ${article} ${typeDescription}.`); 359 } 360 361 notifyArticles(articles, entity) { 362 const plural = this.sIfPlural(articles); 363 364 this.notify(`Found ${articles.length} article${plural} for ${entity.name}.`); 365 } 366 367 notify(message) { 368 const div = $('#map-notification'); 369 370 div.text(message); 371 372 if (message === '') { 373 div.hide(); 374 } else { 375 div.show(); 376 } 377 } 378 379 sIfPlural(list) { 380 return (list.length === 1) ? '' : 's'; 381 } 382 383 processArticles({ articles }, entity) { 384 const articleList = find('#article-list'); 385 386 empty(articleList); 387 388 const list = document.createElement('ul'); 389 list.classList.add('list-unstyled'); 390
391 articles.forEach(({ 392 url, title, thumbnail, issue, 393 }) => { 394 const titleStripped = title.replaceAll("'", ''); 395 396 const link = ` 397 <li> 398 <a href='${url}' target='_blank' class='thumbnail-spread-link'> 399 <div class='row article mb-3'> 400 <div class='col-3 text-center'> 401 <div class='page-box'> 402 <img alt='${titleStripped}' src='${thumbnail}' class='img-fluid' /> 403 </div> 404 </div> 405 <div class='col-9'> 406 <h1>${title}</h1> 407 <h2>${issue.magazine.name}</h2> 408 <h3>${issue.name}</h3> 409 </div> 410 </div> 411 </a> 412 </li> 413 `; 414 415 list.innerHTML += link; 416 }); 417 418 articleList.append(list); 419 420 this.notifyArticles(articles, entity); 421 } 422}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.