PageSourceSearch

https://reader.exacteditions.com/assets/reader/entitymap/EntityMapController-d11031f5.js

js exacteditions.com collected 2026-10-01 10:19:21 UTC 10,463 bytes, 422 lines download raw bytes

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.