archaeopteryx 3.26.0 → 3.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -98
- package/archaeopteryx.d.ts +56 -0
- package/archaeopteryx.js +861 -31
- package/forester.js +8 -1
- package/package.json +2 -2
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.27.0
|
|
24
24
|
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// Archaeopteryx.js is a software tool for the visualization and
|
|
@@ -103,7 +103,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
103
103
|
// IIFE's own function name -- a plain object says what it is.)
|
|
104
104
|
let archaeopteryx = {};
|
|
105
105
|
|
|
106
|
-
const VERSION = '3.
|
|
106
|
+
const VERSION = '3.27.0';
|
|
107
107
|
const WEBSITE = 'https://cmzmasek.github.io/archaeopteryx-js/';
|
|
108
108
|
const DESKTOP_WEBSITE = 'https://cmzmasek.github.io/archaeopteryx/';
|
|
109
109
|
const SOURCE_WEBSITE = 'https://github.com/cmzmasek/archaeopteryx-js';
|
|
@@ -303,6 +303,52 @@ function (root, d3, forester, phyloXml) {
|
|
|
303
303
|
const MESSAGE = 'ArchaeopteryxJS: ';
|
|
304
304
|
const ERROR = 'ArchaeopteryxJS: ERROR: ';
|
|
305
305
|
|
|
306
|
+
// What this launch had to say, kept as well as printed. The console is
|
|
307
|
+
// where a warning goes and where nobody embedding the viewer looks: a
|
|
308
|
+
// site passed a retired key for a month and a forced visualization for
|
|
309
|
+
// longer (BV-BRC, 2026-10-08), each announced on every page load. The
|
|
310
|
+
// Diagnostics report (archaeopteryx.doctor, further down) reads this
|
|
311
|
+
// list; 'problem' entries are somebody's mistake and light the dot on
|
|
312
|
+
// the program name, 'note' entries are only stated. The same line twice
|
|
313
|
+
// is one entry with a count.
|
|
314
|
+
//
|
|
315
|
+
// The list belongs to the tree on view. It starts afresh whenever the
|
|
316
|
+
// launch proper (launchInto) begins for a tree -- a new launch, or
|
|
317
|
+
// showTree() moving to another tree of the file, whose config warnings
|
|
318
|
+
// are then said again and whose per-tree ones are its own. A launch that
|
|
319
|
+
// is REJECTED while an earlier viewer is still on the page gives that
|
|
320
|
+
// viewer its list back and adds one line saying what failed (the shell
|
|
321
|
+
// round launch()): the page still shows the earlier tree, and the report
|
|
322
|
+
// describes the page.
|
|
323
|
+
const DIAGNOSTICS_MAX = 40;
|
|
324
|
+
let _diagnostics = [];
|
|
325
|
+
// What is on view, noted at the moment a launch takes the page over (it
|
|
326
|
+
// clears the container): the report's "launch" section and the dot read
|
|
327
|
+
// this, never the module's working state, which a launch writes to
|
|
328
|
+
// before the checks that can still reject it.
|
|
329
|
+
let _onView = null;
|
|
330
|
+
let _tookOver = false;
|
|
331
|
+
let _recordedErrors = (typeof WeakSet === 'function') ? new WeakSet() : null;
|
|
332
|
+
function recordDiagnostic(level, line) {
|
|
333
|
+
let text = String(line).replace(/^ArchaeopteryxJS: (WARNING: |ERROR: )?/, '');
|
|
334
|
+
let seen = _diagnostics.find(function (d) {
|
|
335
|
+
return d.level === level && d.text === text;
|
|
336
|
+
});
|
|
337
|
+
if (seen) {
|
|
338
|
+
seen.count += 1;
|
|
339
|
+
} else if (_diagnostics.length < DIAGNOSTICS_MAX) {
|
|
340
|
+
_diagnostics.push({level: level, text: text, count: 1});
|
|
341
|
+
}
|
|
342
|
+
if (level === 'problem') {
|
|
343
|
+
syncDoctorDot();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Print as before (same console method, same line) and keep it.
|
|
347
|
+
function say(level, method, line) {
|
|
348
|
+
console[method](line);
|
|
349
|
+
recordDiagnostic(level, line);
|
|
350
|
+
}
|
|
351
|
+
|
|
306
352
|
// Call-time dependency guards (the UMD header explains why they are not
|
|
307
353
|
// load-time): each failure names exactly what is missing.
|
|
308
354
|
function requireD3() {
|
|
@@ -440,6 +486,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
440
486
|
const CLOCK_PLOT_BUTTON = 'clock_plot_b';
|
|
441
487
|
const CLOCK_PLOT_DIALOG = 'aptx_clock_plot';
|
|
442
488
|
const ABOUT_DIALOG = 'aptx_about';
|
|
489
|
+
const DOCTOR_DIALOG = 'aptx_doctor';
|
|
443
490
|
const TREE_PROPERTIES_DIALOG = 'aptx_tree_props';
|
|
444
491
|
const PROG_NAME = 'progname';
|
|
445
492
|
const PROGNAMELINK = 'prognamelink';
|
|
@@ -1938,7 +1985,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1938
1985
|
|| (c.label && String(c.label).toLowerCase() === want);
|
|
1939
1986
|
}) || null;
|
|
1940
1987
|
if (!requested) {
|
|
1941
|
-
|
|
1988
|
+
say('problem', 'warn', WARNING + ': initialVisualization "'
|
|
1942
1989
|
+ _settings.initialVisualization
|
|
1943
1990
|
+ '" names no visualization of this tree; using the automatic choice');
|
|
1944
1991
|
}
|
|
@@ -1968,7 +2015,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1968
2015
|
? requested.values.length + ' distinct values on this tree, more than the '
|
|
1969
2016
|
+ LEGEND_MAX_ROWS + ' the legend lists'
|
|
1970
2017
|
: 'a value on ' + requested.coverage + ' of the tree\'s ' + requested.total + ' tips';
|
|
1971
|
-
|
|
2018
|
+
say('problem', 'warn', WARNING + ': initialVisualization "' + _settings.initialVisualization
|
|
1972
2019
|
+ '" names a field with ' + why + '; left to itself the viewer would '
|
|
1973
2020
|
+ (auto ? 'open with "' + auto.label + '" (' + auto.values.length + ' values)'
|
|
1974
2021
|
: 'open uncoloured'));
|
|
@@ -5825,14 +5872,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
5825
5872
|
return;
|
|
5826
5873
|
}
|
|
5827
5874
|
if (dep.to) {
|
|
5828
|
-
|
|
5875
|
+
say('problem', 'warn', WARNING + ': "' + k + '" was replaced by "' + dep.to
|
|
5829
5876
|
+ '": ' + dep.note);
|
|
5830
5877
|
// An explicit new key always wins over the old alias.
|
|
5831
5878
|
if (given[dep.to] === undefined) {
|
|
5832
5879
|
given[dep.to] = dep.map(given[k]);
|
|
5833
5880
|
}
|
|
5834
5881
|
} else {
|
|
5835
|
-
|
|
5882
|
+
say('problem', 'warn', WARNING + ': "' + k + '" is retired and has no effect: ' + dep.note);
|
|
5836
5883
|
}
|
|
5837
5884
|
delete given[k];
|
|
5838
5885
|
});
|
|
@@ -6078,7 +6125,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6078
6125
|
// malformed domains are skipped, never fatal -- said once, here
|
|
6079
6126
|
let ignored = forester.domainArchitectureStats(_treeData).ignored;
|
|
6080
6127
|
if (ignored > 0) {
|
|
6081
|
-
|
|
6128
|
+
say('note', 'warn', MESSAGE + ignored + ' protein domain' + (ignored === 1 ? '' : 's')
|
|
6082
6129
|
+ ' with a missing or impossible from / to / E-value ignored');
|
|
6083
6130
|
}
|
|
6084
6131
|
}
|
|
@@ -6111,11 +6158,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6111
6158
|
// are left out, never fatal -- said once, here
|
|
6112
6159
|
let gs = forester.geneRegionStats(_treeData);
|
|
6113
6160
|
if (gs.ignored > 0) {
|
|
6114
|
-
|
|
6161
|
+
say('note', 'warn', MESSAGE + gs.ignored + ' sequence location' + (gs.ignored === 1 ? '' : 's')
|
|
6115
6162
|
+ ' not in the form contig:start-end(+) ignored');
|
|
6116
6163
|
}
|
|
6117
6164
|
if (gs.otherContigs > 0) {
|
|
6118
|
-
|
|
6165
|
+
say('note', 'warn', MESSAGE + gs.otherContigs + ' gene' + (gs.otherContigs === 1 ? '' : 's')
|
|
6119
6166
|
+ ' on a genome\'s other contigs not drawn: the gene track shows one contig per genome');
|
|
6120
6167
|
}
|
|
6121
6168
|
}
|
|
@@ -6347,11 +6394,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6347
6394
|
_settings.enableDynamicSizing = true;
|
|
6348
6395
|
}
|
|
6349
6396
|
if (_settings.displayWidth && _settings.enableDynamicSizing === true) {
|
|
6350
|
-
|
|
6397
|
+
say('note', 'log', WARNING + ': dynamic sizing is turned on, will ignore displayWidth setting');
|
|
6351
6398
|
_settings.displayWidth = 0;
|
|
6352
6399
|
}
|
|
6353
6400
|
if (_settings.displayHeight && _settings.enableDynamicSizing === true) {
|
|
6354
|
-
|
|
6401
|
+
say('note', 'log', WARNING + ': dynamic sizing is turned on, will ignore displayHeight setting');
|
|
6355
6402
|
_settings.displayHeight = 0;
|
|
6356
6403
|
}
|
|
6357
6404
|
if ((!_settings.displayWidth) && (!_settings.enableDynamicSizing)) {
|
|
@@ -6640,7 +6687,40 @@ function (root, d3, forester, phyloXml) {
|
|
|
6640
6687
|
|
|
6641
6688
|
// launch(container, tree, config) -> a viewer handle {getSelectedNodes,
|
|
6642
6689
|
// destroy}. Exactly three arguments; config is THE one config object.
|
|
6643
|
-
|
|
6690
|
+
// launch() proper is launchChecked below. This shell keeps what a
|
|
6691
|
+
// rejected launch said: the thrown message reaches whoever called, which
|
|
6692
|
+
// on an embedding site is often a catch block nobody reads, and
|
|
6693
|
+
// archaeopteryx.doctor() can still tell. (launchArchaeopteryx has its own
|
|
6694
|
+
// failures -- a file that does not parse -- and reports them through the
|
|
6695
|
+
// same function; an error is recorded once, whoever catches it first.)
|
|
6696
|
+
function recordLaunchFailure(e) {
|
|
6697
|
+
if (_recordedErrors && e && typeof e === 'object') {
|
|
6698
|
+
if (_recordedErrors.has(e)) {
|
|
6699
|
+
return;
|
|
6700
|
+
}
|
|
6701
|
+
_recordedErrors.add(e);
|
|
6702
|
+
}
|
|
6703
|
+
recordDiagnostic('problem', 'launch failed: '
|
|
6704
|
+
+ String(e && e.message ? e.message : e).replace(/^ArchaeopteryxJS: ERROR: /, ''));
|
|
6705
|
+
}
|
|
6706
|
+
archaeopteryx.launch = function () {
|
|
6707
|
+
let before = _diagnostics;
|
|
6708
|
+
let wasOnView = _onView;
|
|
6709
|
+
_tookOver = false;
|
|
6710
|
+
try {
|
|
6711
|
+
return launchChecked.apply(null, arguments);
|
|
6712
|
+
} catch (e) {
|
|
6713
|
+
if (wasOnView && !_tookOver) {
|
|
6714
|
+
// rejected before it touched the page: the earlier viewer is
|
|
6715
|
+
// still there, and so is everything said about it
|
|
6716
|
+
_diagnostics = before;
|
|
6717
|
+
}
|
|
6718
|
+
recordLaunchFailure(e);
|
|
6719
|
+
throw e;
|
|
6720
|
+
}
|
|
6721
|
+
};
|
|
6722
|
+
|
|
6723
|
+
function launchChecked(container, phylo, config) {
|
|
6644
6724
|
|
|
6645
6725
|
// Bad input is the caller's bug, so it is thrown at the caller. It used
|
|
6646
6726
|
// to pop a browser alert and return, which blocks the whole tab and
|
|
@@ -6715,12 +6795,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
6715
6795
|
let view = config && config.view;
|
|
6716
6796
|
let start = (view && Number.isInteger(view.tree) && view.tree > 0 && view.tree < trees.length) ? view.tree : 0;
|
|
6717
6797
|
return launchInto(container, trees, start, config);
|
|
6718
|
-
}
|
|
6798
|
+
}
|
|
6719
6799
|
|
|
6720
6800
|
// The launch proper, for one tree of the list: launch() validates and
|
|
6721
6801
|
// starts at the first, showTree() re-enters here for another.
|
|
6722
6802
|
function launchInto(container, trees, index, config) {
|
|
6723
6803
|
let phylo = trees[index];
|
|
6804
|
+
// what is said from here on is said about THIS tree (see _diagnostics)
|
|
6805
|
+
_diagnostics = [];
|
|
6724
6806
|
let cfg = readConfig(config);
|
|
6725
6807
|
requireForester();
|
|
6726
6808
|
requireD3();
|
|
@@ -6767,6 +6849,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6767
6849
|
checkLayoutValue(cfg.state ? cfg.state.layout : undefined);
|
|
6768
6850
|
initializeSettings(cfg.settings);
|
|
6769
6851
|
|
|
6852
|
+
// From here the page is this launch's: nothing below is expected to
|
|
6853
|
+
// throw, and what a previous one drew goes now.
|
|
6854
|
+
_tookOver = true;
|
|
6855
|
+
_onView = {config: config, container: containerEl, tips: _basicTreeProperties.externalNodesCount,
|
|
6856
|
+
tree: index + 1, trees: trees.length};
|
|
6770
6857
|
// whatever a previous launch left in this container (or in the
|
|
6771
6858
|
// container before it) goes now, so the old tree does not sit under
|
|
6772
6859
|
// the card while the new one is prepared
|
|
@@ -6977,6 +7064,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6977
7064
|
cfg.view = view || viewForTree(_trees[index]);
|
|
6978
7065
|
let handle = launchInto(_container, _trees, index, cfg);
|
|
6979
7066
|
_launchConfig = launchConfig;
|
|
7067
|
+
// the report shows the config the SITE passed, not this copy with
|
|
7068
|
+
// the tree's own view added
|
|
7069
|
+
if (_onView) {
|
|
7070
|
+
_onView.config = launchConfig;
|
|
7071
|
+
}
|
|
6980
7072
|
return handle;
|
|
6981
7073
|
}
|
|
6982
7074
|
|
|
@@ -10370,7 +10462,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10370
10462
|
try {
|
|
10371
10463
|
_settings.onViewChange(state, encoded);
|
|
10372
10464
|
} catch (e) {
|
|
10373
|
-
|
|
10465
|
+
say('problem', 'error', ERROR + 'onViewChange threw: ' + (e && e.message ? e.message : e));
|
|
10374
10466
|
}
|
|
10375
10467
|
}
|
|
10376
10468
|
|
|
@@ -15904,14 +15996,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
15904
15996
|
// The async clipboard API where the page allows it (a secure context);
|
|
15905
15997
|
// otherwise the old select-and-copy, so a page served over plain http
|
|
15906
15998
|
// still copies. Resolves to whether the text made it.
|
|
15907
|
-
|
|
15999
|
+
//
|
|
16000
|
+
// `host` is the open modal dialog the copy is made from, if any: under a
|
|
16001
|
+
// modal everything outside it is inert, a textarea on the body included.
|
|
16002
|
+
function copyText(text, host) {
|
|
15908
16003
|
let legacy = function () {
|
|
15909
16004
|
let ta = document.createElement('textarea');
|
|
15910
16005
|
ta.value = text;
|
|
15911
16006
|
ta.setAttribute('readonly', '');
|
|
15912
16007
|
ta.style.position = 'fixed';
|
|
15913
16008
|
ta.style.left = '-10000px';
|
|
15914
|
-
document.body.appendChild(ta);
|
|
16009
|
+
(host || document.body).appendChild(ta);
|
|
15915
16010
|
ta.select();
|
|
15916
16011
|
let ok = false;
|
|
15917
16012
|
try {
|
|
@@ -16817,6 +16912,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
16817
16912
|
+ '.aptx-dialog-close:hover { background:var(--p-accent-weak); color:var(--p-accent-ink);'
|
|
16818
16913
|
+ ' border-color:var(--p-accent); }'
|
|
16819
16914
|
+ '.aptx-dialog-body { padding:10px 13px 12px; overflow:auto; }'
|
|
16915
|
+
// a dialog that scrolls gets the panel's own scrollbar: the
|
|
16916
|
+
// browser's default track is white in the dark theme too
|
|
16917
|
+
+ '.aptx-dialog-body::-webkit-scrollbar { width:9px; height:9px; }'
|
|
16918
|
+
+ '.aptx-dialog-body::-webkit-scrollbar-thumb { background:var(--p-line-strong); border-radius:9px; border:2px solid var(--p-bg); }'
|
|
16919
|
+
+ '.aptx-dialog-body::-webkit-scrollbar-track, .aptx-dialog-body::-webkit-scrollbar-corner { background:transparent; }'
|
|
16820
16920
|
+ '.aptx-cp-sv { position:relative; width:100%; box-sizing:border-box; height:104px; border-radius:7px;'
|
|
16821
16921
|
+ ' border:1px solid var(--p-line-strong); cursor:crosshair; touch-action:none; }'
|
|
16822
16922
|
+ '.aptx-cp-hue { position:relative; width:100%; box-sizing:border-box; height:13px; margin-top:9px;'
|
|
@@ -16974,6 +17074,23 @@ function (root, d3, forester, phyloXml) {
|
|
|
16974
17074
|
+ '.aptx-panel .' + PROGNAMELINK + ' { background:none; padding:0; margin:0; cursor:pointer;'
|
|
16975
17075
|
+ ' font-family:inherit; font-weight:inherit; letter-spacing:inherit; }'
|
|
16976
17076
|
+ '.aptx-panel .' + PROGNAMELINK + ':hover { text-decoration:underline; }'
|
|
17077
|
+
+ '.aptx-doctor-dot { display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--p-muted); vertical-align:middle; }'
|
|
17078
|
+
+ '.aptx-doctor-dot[hidden] { display:none; }'
|
|
17079
|
+
+ '.aptx-panel .' + PROGNAMELINK + ' .aptx-doctor-dot { margin-left:6px; }'
|
|
17080
|
+
+ '.aptx-about .aptx-doctor-dot { margin-right:6px; }'
|
|
17081
|
+
+ '.aptx-dialog-body.aptx-doctor { max-height:min(70vh,560px); }'
|
|
17082
|
+
+ '.aptx-doctor .aptx-dialog-key { flex-basis:34%; }'
|
|
17083
|
+
+ '.aptx-doctor-head { display:flex; align-items:center; gap:10px; margin-bottom:6px; }'
|
|
17084
|
+
+ '.aptx-doctor-status { flex:1 1 auto; min-width:0; font-weight:650; }'
|
|
17085
|
+
+ '.aptx-doctor-status.aptx-doctor-bad { box-shadow:inset 3px 0 0 var(--p-warn); padding-left:9px; }'
|
|
17086
|
+
+ '.aptx-doctor-item { position:relative; padding:2px 0 2px 14px; line-height:1.35; overflow-wrap:anywhere; }'
|
|
17087
|
+
+ '.aptx-doctor-item::before { content:""; position:absolute; left:2px; top:0.62em; width:6px; height:6px; border-radius:50%; background:var(--p-faint); }'
|
|
17088
|
+
+ '.aptx-doctor-item.aptx-doctor-bad::before { background:var(--p-warn); }'
|
|
17089
|
+
+ '.aptx-doctor-item.aptx-doctor-note { color:var(--p-muted); }'
|
|
17090
|
+
+ '.aptx-doctor-section { margin:11px 0 3px; font-size:10.5px; font-weight:650; letter-spacing:0.05em; text-transform:uppercase; color:var(--p-muted); }'
|
|
17091
|
+
+ '.aptx-doctor-detail { display:block; color:var(--p-muted); line-height:1.35; }'
|
|
17092
|
+
+ '.aptx-doctor .aptx-dialog-line.aptx-doctor-bad .aptx-doctor-value { color:var(--p-warn); font-weight:600; }'
|
|
17093
|
+
+ '.aptx-doctor-copy { flex:none; }'
|
|
16977
17094
|
+ '.aptx-dialog-body.aptx-about { padding-top:13px; }'
|
|
16978
17095
|
// the label column is narrower here than for node data: these values
|
|
16979
17096
|
// are URLs, and they should not have to wrap mid-word
|
|
@@ -17255,6 +17372,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
17255
17372
|
e.stopPropagation();
|
|
17256
17373
|
showAboutDialog();
|
|
17257
17374
|
});
|
|
17375
|
+
// config warnings are recorded before this header exists
|
|
17376
|
+
syncDoctorDot();
|
|
17377
|
+
// ... and a library can arrive after it (see syncDoctorDot)
|
|
17378
|
+
let panel = header.closest('.aptx-panel');
|
|
17379
|
+
if (panel) {
|
|
17380
|
+
panel.addEventListener('pointerenter', syncDoctorDot);
|
|
17381
|
+
}
|
|
17382
|
+
if (document.readyState !== 'complete') {
|
|
17383
|
+
window.addEventListener('load', syncDoctorDot, {once: true});
|
|
17384
|
+
}
|
|
17258
17385
|
|
|
17259
17386
|
let themeBtn = document.createElement('button');
|
|
17260
17387
|
themeBtn.type = 'button';
|
|
@@ -18088,10 +18215,693 @@ function (root, d3, forester, phyloXml) {
|
|
|
18088
18215
|
shell.dialog.showModal();
|
|
18089
18216
|
}
|
|
18090
18217
|
|
|
18218
|
+
// ===================== Diagnostics =====================
|
|
18219
|
+
// "Dependencies are checked when used, never at load time" makes the
|
|
18220
|
+
// viewer hard to break and easy to run half-equipped without knowing: on
|
|
18221
|
+
// BV-BRC it ran for weeks with no PDF or PNG export, a retired config key
|
|
18222
|
+
// and a forced visualization, and every one of those was found by reading
|
|
18223
|
+
// the site's source (Christian, 2026-10-09: a "check engine light" and a
|
|
18224
|
+
// doctor behind it). This is the one place that says what is loaded, what
|
|
18225
|
+
// that switches off, and what the launch complained about.
|
|
18226
|
+
//
|
|
18227
|
+
// Two rules decide what is a PROBLEM (lights the dot) and what is only
|
|
18228
|
+
// stated:
|
|
18229
|
+
// * an optional library that is simply absent is a site's choice, and
|
|
18230
|
+
// is stated ("PDF export: off"), never flagged;
|
|
18231
|
+
// * something nobody chooses is a problem: half of a pair (jspdf
|
|
18232
|
+
// without svg2pdf.js), a library from the 2.x days under its old
|
|
18233
|
+
// global, a d3 that is not 7, forester.js from another release, a
|
|
18234
|
+
// phyloxml that no longer writes what this release expects, and every
|
|
18235
|
+
// warning or error the launch printed about its config.
|
|
18236
|
+
// Only d3, jsPDF and forester state a version, so everything else is
|
|
18237
|
+
// judged by what it can do.
|
|
18238
|
+
//
|
|
18239
|
+
// Nothing in here may throw: every check runs in its own try, and a check
|
|
18240
|
+
// that fails says so in its own row.
|
|
18241
|
+
|
|
18242
|
+
// phyloxml states no version, so a small tree goes through it and what
|
|
18243
|
+
// comes back is read for what an OLDER release is known to do. Each of
|
|
18244
|
+
// these is a named fault, and each published release before 1.1.7 shows
|
|
18245
|
+
// at least one (measured on 1.0.2, 1.1.3, 1.1.4, 1.1.5 and 1.1.6):
|
|
18246
|
+
// * it drops the domain architecture, or a <phylogeny>'s own property
|
|
18247
|
+
// (before 1.1.5 / 1.1.3): a saved tree LOSES data;
|
|
18248
|
+
// * it writes a style colour as it came, or the style property before
|
|
18249
|
+
// the node's other properties (before 1.1.7): the saved file differs
|
|
18250
|
+
// from the desktop program's.
|
|
18251
|
+
// Whether the output is byte for byte what 1.1.7 writes is looked at too,
|
|
18252
|
+
// but a difference there alone is only a NOTE. package.json admits any
|
|
18253
|
+
// 1.1.x from 1.1.7 up, a release of this viewer cannot be corrected
|
|
18254
|
+
// after it is published, and a newer phyloxml that lays a file out
|
|
18255
|
+
// differently must not light a dot on every page that has it. (A first
|
|
18256
|
+
// version demanded the bytes and called every difference "an older
|
|
18257
|
+
// copy".)
|
|
18258
|
+
const PHYLOXML_PROBE_IN = '<phyloxml xmlns="http://www.phyloxml.org"><phylogeny rooted="true"><clade>'
|
|
18259
|
+
+ '<clade><name>a</name><branch_length>1</branch_length>'
|
|
18260
|
+
+ '<sequence><domain_architecture length="9"><domain from="1" to="5" confidence="0.1">D</domain>'
|
|
18261
|
+
+ '</domain_architecture></sequence>'
|
|
18262
|
+
+ '<property ref="style:font_color" datatype="xsd:token" applies_to="node">#CE1616</property>'
|
|
18263
|
+
+ '<property ref="x:y" datatype="xsd:string" applies_to="node">v</property></clade>'
|
|
18264
|
+
+ '<clade><name>b</name><branch_length>2</branch_length></clade></clade>'
|
|
18265
|
+
+ '<property ref="p:q" datatype="xsd:string" applies_to="phylogeny">w</property></phylogeny></phyloxml>';
|
|
18266
|
+
const PHYLOXML_PROBE_OUT = ['<?xml version="1.0" encoding="UTF-8"?>',
|
|
18267
|
+
'<phyloxml xmlns="http://www.phyloxml.org">',
|
|
18268
|
+
'<phylogeny rooted="true" rerootable="true">',
|
|
18269
|
+
' <clade>',
|
|
18270
|
+
' <clade>',
|
|
18271
|
+
' <name>a</name>',
|
|
18272
|
+
' <branch_length>1</branch_length>',
|
|
18273
|
+
' <sequence>',
|
|
18274
|
+
' <domain_architecture length="9">',
|
|
18275
|
+
' <domain from="1" to="5" confidence="0.1">D</domain>',
|
|
18276
|
+
' </domain_architecture>',
|
|
18277
|
+
' </sequence>',
|
|
18278
|
+
' <property ref="x:y" datatype="xsd:string" applies_to="node">v</property>',
|
|
18279
|
+
' <property ref="style:font_color" datatype="xsd:token" applies_to="node">#ce1616</property>',
|
|
18280
|
+
' </clade>',
|
|
18281
|
+
' <clade>',
|
|
18282
|
+
' <name>b</name>',
|
|
18283
|
+
' <branch_length>2</branch_length>',
|
|
18284
|
+
' </clade>',
|
|
18285
|
+
' </clade>',
|
|
18286
|
+
' <property ref="p:q" datatype="xsd:string" applies_to="phylogeny">w</property>',
|
|
18287
|
+
'</phylogeny>',
|
|
18288
|
+
'</phyloxml>'].join('\n');
|
|
18289
|
+
const PHYLOXML_EXPECTED = '1.1.7';
|
|
18290
|
+
const D3_TESTED_MINOR = 9; // package.json: d3 ^7.9.0
|
|
18291
|
+
const JSPDF_TESTED_MAJOR = 4; // package.json: jspdf ^4.2.1
|
|
18292
|
+
|
|
18293
|
+
// The probe's answer for the library object it was asked about: the
|
|
18294
|
+
// dot asks often, and the answer cannot change.
|
|
18295
|
+
// threw / stage reading ('read') or writing ('write') failed, with the message
|
|
18296
|
+
// lost what a saved tree no longer has
|
|
18297
|
+
// old what is written as releases before 1.1.7 wrote it
|
|
18298
|
+
// same the output is byte for byte 1.1.7's
|
|
18299
|
+
let _phyloXmlProbe = null;
|
|
18300
|
+
function probePhyloXml(px) {
|
|
18301
|
+
if (_phyloXmlProbe && _phyloXmlProbe.lib === px) {
|
|
18302
|
+
return _phyloXmlProbe.result;
|
|
18303
|
+
}
|
|
18304
|
+
let result = {threw: null, stage: null, lost: [], old: [], same: false};
|
|
18305
|
+
let message = function (e) {
|
|
18306
|
+
return String(e && e.message ? e.message : e);
|
|
18307
|
+
};
|
|
18308
|
+
let tree = null;
|
|
18309
|
+
try {
|
|
18310
|
+
tree = px.parse(PHYLOXML_PROBE_IN, {trim: true, normalize: true})[0];
|
|
18311
|
+
} catch (e) {
|
|
18312
|
+
result.threw = message(e);
|
|
18313
|
+
result.stage = 'read';
|
|
18314
|
+
}
|
|
18315
|
+
let written = null;
|
|
18316
|
+
if (!result.threw) {
|
|
18317
|
+
try {
|
|
18318
|
+
written = String(px.toPhyloXML(tree, 9));
|
|
18319
|
+
} catch (e) {
|
|
18320
|
+
result.threw = message(e);
|
|
18321
|
+
result.stage = 'write';
|
|
18322
|
+
}
|
|
18323
|
+
}
|
|
18324
|
+
if (written !== null) {
|
|
18325
|
+
result.same = written === PHYLOXML_PROBE_OUT;
|
|
18326
|
+
if (!/<domain from="1" to="5"[^>]*>D<\/domain>/.test(written)) {
|
|
18327
|
+
result.lost.push('protein domains');
|
|
18328
|
+
}
|
|
18329
|
+
if (written.indexOf('ref="p:q"') < 0) {
|
|
18330
|
+
result.lost.push('a tree\'s own properties');
|
|
18331
|
+
}
|
|
18332
|
+
if (written.indexOf('ref="x:y"') < 0) {
|
|
18333
|
+
result.lost.push('node properties');
|
|
18334
|
+
}
|
|
18335
|
+
let style = written.indexOf('ref="style:font_color"');
|
|
18336
|
+
if (style < 0) {
|
|
18337
|
+
result.lost.push('node styles');
|
|
18338
|
+
} else {
|
|
18339
|
+
if (written.indexOf('>#ce1616<') < 0) {
|
|
18340
|
+
result.old.push('a style colour as it came, not in lower case');
|
|
18341
|
+
}
|
|
18342
|
+
if (written.indexOf('ref="x:y"') > style) {
|
|
18343
|
+
result.old.push('a node\'s style before its other properties');
|
|
18344
|
+
}
|
|
18345
|
+
}
|
|
18346
|
+
}
|
|
18347
|
+
_phyloXmlProbe = {lib: px, result: result};
|
|
18348
|
+
return result;
|
|
18349
|
+
}
|
|
18350
|
+
|
|
18351
|
+
// One library's row. `judge` returns {loaded, version, status, detail};
|
|
18352
|
+
// status is 'ok', 'absent' (optional and not there: stated), 'note' or
|
|
18353
|
+
// 'problem'.
|
|
18354
|
+
function doctorLibrary(name, role, judge) {
|
|
18355
|
+
let row = {name: name, role: role, loaded: false, version: null, status: 'problem', detail: ''};
|
|
18356
|
+
try {
|
|
18357
|
+
let j = judge();
|
|
18358
|
+
row.loaded = !!j.loaded;
|
|
18359
|
+
row.version = j.version || null;
|
|
18360
|
+
row.status = j.status;
|
|
18361
|
+
row.detail = j.detail || '';
|
|
18362
|
+
} catch (e) {
|
|
18363
|
+
row.detail = 'this check failed: ' + (e && e.message ? e.message : e);
|
|
18364
|
+
}
|
|
18365
|
+
return row;
|
|
18366
|
+
}
|
|
18367
|
+
|
|
18368
|
+
function doctorLibraries() {
|
|
18369
|
+
let g = root || {};
|
|
18370
|
+
// without a document there is nothing to launch into: the parsers
|
|
18371
|
+
// work with no d3 at all, so its absence there is not a fault
|
|
18372
|
+
let inBrowser = typeof g.document !== 'undefined';
|
|
18373
|
+
let libs = [];
|
|
18374
|
+
|
|
18375
|
+
libs.push(doctorLibrary('forester.js', 'required', function () {
|
|
18376
|
+
let f = forester || g.forester;
|
|
18377
|
+
if (!f) {
|
|
18378
|
+
return {loaded: false, status: 'problem', detail: 'not loaded: nothing can be read or drawn'};
|
|
18379
|
+
}
|
|
18380
|
+
if (f.VERSION === VERSION) {
|
|
18381
|
+
return {loaded: true, version: f.VERSION, status: 'ok'};
|
|
18382
|
+
}
|
|
18383
|
+
return {loaded: true, version: f.VERSION || null, status: 'problem',
|
|
18384
|
+
detail: (f.VERSION ? 'forester.js is ' + f.VERSION : 'forester.js states no version, so it is an older file')
|
|
18385
|
+
+ ', archaeopteryx.js is ' + VERSION + ': the two must come from the same release'};
|
|
18386
|
+
}));
|
|
18387
|
+
|
|
18388
|
+
libs.push(doctorLibrary('d3', 'required', function () {
|
|
18389
|
+
// THE d3 THE VIEWER USES, chosen as requireD3 chooses it: the one
|
|
18390
|
+
// it was handed at load, and the page's global only where that
|
|
18391
|
+
// one is missing or unusable. (A first version looked at the
|
|
18392
|
+
// global first and could report on a d3 nothing was drawn with.)
|
|
18393
|
+
let d = (g.d3 && (!d3 || typeof d3.zoom !== 'function')) ? g.d3 : d3;
|
|
18394
|
+
if (!d) {
|
|
18395
|
+
return inBrowser
|
|
18396
|
+
? {loaded: false, status: 'problem', detail: 'not loaded: launch() fails without d3 version 7'}
|
|
18397
|
+
: {loaded: false, status: 'absent', detail: 'not loaded: only launch() needs it'};
|
|
18398
|
+
}
|
|
18399
|
+
let usable = typeof d.zoom === 'function' && typeof d.cluster === 'function';
|
|
18400
|
+
let version = typeof d.version === 'string' ? d.version : null;
|
|
18401
|
+
if (version === null) {
|
|
18402
|
+
// d3's ES-module build exports no `version`: a bundler's
|
|
18403
|
+
// `import * as d3`, jsdelivr's +esm, Node's require('d3'). It
|
|
18404
|
+
// is judged by what only d3 7 has -- InternMap arrived with
|
|
18405
|
+
// d3-array 3, which is d3 7.0.0 -- and never called outdated
|
|
18406
|
+
// for not saying its number.
|
|
18407
|
+
if (usable && typeof d.InternMap === 'function') {
|
|
18408
|
+
return {loaded: true, status: 'ok', detail: 'states no version (an ES-module build); has what d3 7 has'};
|
|
18409
|
+
}
|
|
18410
|
+
return {loaded: true, status: inBrowser ? 'problem' : 'absent',
|
|
18411
|
+
detail: inBrowser ? 'states no version and lacks what d3 7 has: Archaeopteryx.js needs d3 version 7'
|
|
18412
|
+
: 'not usable here as d3 version 7: only launch() needs it'};
|
|
18413
|
+
}
|
|
18414
|
+
let parts = version.split('.').map(Number);
|
|
18415
|
+
if (parts[0] !== 7 || !usable) {
|
|
18416
|
+
return {loaded: true, version: version, status: inBrowser ? 'problem' : 'absent',
|
|
18417
|
+
detail: inBrowser ? 'Archaeopteryx.js needs d3 version 7'
|
|
18418
|
+
: 'not usable here as d3 version 7: only launch() needs it'};
|
|
18419
|
+
}
|
|
18420
|
+
if (parts[1] < D3_TESTED_MINOR) {
|
|
18421
|
+
return {loaded: true, version: version, status: 'note',
|
|
18422
|
+
detail: 'older than the 7.' + D3_TESTED_MINOR + ' this release is tested with'};
|
|
18423
|
+
}
|
|
18424
|
+
return {loaded: true, version: version, status: 'ok'};
|
|
18425
|
+
}));
|
|
18426
|
+
|
|
18427
|
+
let px = null;
|
|
18428
|
+
let probe = null;
|
|
18429
|
+
libs.push(doctorLibrary('phyloxml', 'required', function () {
|
|
18430
|
+
px = phyloXml || g.phyloXml;
|
|
18431
|
+
probe = px ? probePhyloXml(px) : null;
|
|
18432
|
+
if (!px) {
|
|
18433
|
+
return {loaded: false, status: 'problem',
|
|
18434
|
+
detail: 'not loaded: phyloXML cannot be read, and the phyloXML download fails'};
|
|
18435
|
+
}
|
|
18436
|
+
if (probe.threw) {
|
|
18437
|
+
// on a page phyloxml takes sax from the global, and a missing
|
|
18438
|
+
// one is the usual reason reading fails; anywhere else, and
|
|
18439
|
+
// for any other failure, the message itself is the finding
|
|
18440
|
+
return {loaded: true, status: 'problem', detail: (probe.stage === 'read' && inBrowser && !g.sax)
|
|
18441
|
+
? 'cannot read phyloXML: sax.js is not loaded'
|
|
18442
|
+
: (probe.stage === 'read' ? 'reading' : 'writing') + ' a small tree failed: ' + probe.threw};
|
|
18443
|
+
}
|
|
18444
|
+
if (probe.lost.length > 0) {
|
|
18445
|
+
return {loaded: true, status: 'problem', detail: 'an older copy: saving a tree as phyloXML drops '
|
|
18446
|
+
+ probe.lost.join(', ') + ' (phyloxml ' + PHYLOXML_EXPECTED + ' keeps them)'};
|
|
18447
|
+
}
|
|
18448
|
+
if (probe.old.length > 0) {
|
|
18449
|
+
return {loaded: true, status: 'problem', detail: 'an older copy: it writes ' + probe.old.join(', and ')
|
|
18450
|
+
+ ', so a saved tree differs from the desktop program\'s file (phyloxml ' + PHYLOXML_EXPECTED
|
|
18451
|
+
+ ' does not)'};
|
|
18452
|
+
}
|
|
18453
|
+
if (!probe.same) {
|
|
18454
|
+
return {loaded: true, status: 'note', detail: 'writes a small tree differently from phyloxml '
|
|
18455
|
+
+ PHYLOXML_EXPECTED + ', losing nothing: a newer release, or an altered copy'};
|
|
18456
|
+
}
|
|
18457
|
+
return {loaded: true, status: 'ok', detail: 'writes phyloXML as ' + PHYLOXML_EXPECTED + ' does'};
|
|
18458
|
+
}));
|
|
18459
|
+
libs.push(doctorLibrary('sax', 'required', function () {
|
|
18460
|
+
// A page hands sax over as a global. Anywhere else (Node, a
|
|
18461
|
+
// bundler) phyloxml gets it by require() when it loads, so a
|
|
18462
|
+
// phyloxml that loaded has it, and a tree it read proves it.
|
|
18463
|
+
if (g.sax || (probe && probe.stage !== 'read')) {
|
|
18464
|
+
return {loaded: true, status: 'ok'};
|
|
18465
|
+
}
|
|
18466
|
+
if (!inBrowser && px) {
|
|
18467
|
+
return {loaded: true, status: 'ok', detail: 'taken by phyloxml when it loaded'};
|
|
18468
|
+
}
|
|
18469
|
+
return {loaded: false, status: 'problem', detail: 'not loaded: phyloxml needs it to read phyloXML'};
|
|
18470
|
+
}));
|
|
18471
|
+
|
|
18472
|
+
libs.push(doctorLibrary('canvg', 'optional', function () {
|
|
18473
|
+
if (g.Canvg && typeof g.Canvg.fromString === 'function') {
|
|
18474
|
+
return {loaded: true, status: 'ok'};
|
|
18475
|
+
}
|
|
18476
|
+
if (g.Canvg) {
|
|
18477
|
+
return {loaded: true, status: 'problem',
|
|
18478
|
+
detail: 'window.Canvg has no fromString: not canvg 4, so PNG export is off'};
|
|
18479
|
+
}
|
|
18480
|
+
if (g.canvg) {
|
|
18481
|
+
return {loaded: true, status: 'problem', detail: 'loaded under its old global "canvg" (canvg 1 to 3);'
|
|
18482
|
+
+ ' PNG export needs canvg 4 as window.Canvg, so it is off'};
|
|
18483
|
+
}
|
|
18484
|
+
return {loaded: false, status: 'absent', detail: 'not loaded: PNG export and Copy PNG are off'};
|
|
18485
|
+
}));
|
|
18486
|
+
|
|
18487
|
+
// The pair is read inside each row's own guard: whatever a page has
|
|
18488
|
+
// put under these names, a lookup that throws costs its row and no
|
|
18489
|
+
// other.
|
|
18490
|
+
let pdfPair = function () {
|
|
18491
|
+
let J = g.jspdf && g.jspdf.jsPDF;
|
|
18492
|
+
return {J: J, attached: !!(J && J.API && J.API.svg), svg2pdf: !!g.svg2pdf};
|
|
18493
|
+
};
|
|
18494
|
+
libs.push(doctorLibrary('jspdf', 'optional', function () {
|
|
18495
|
+
let pair = pdfPair();
|
|
18496
|
+
let J = pair.J;
|
|
18497
|
+
let attached = pair.attached;
|
|
18498
|
+
if (J) {
|
|
18499
|
+
let version = typeof J.version === 'string' ? J.version : null;
|
|
18500
|
+
if (!attached && !g.svg2pdf) {
|
|
18501
|
+
return {loaded: true, version: version, status: 'problem',
|
|
18502
|
+
detail: 'loaded without svg2pdf.js: PDF export needs both, so it is off'};
|
|
18503
|
+
}
|
|
18504
|
+
if (version && Number(version.split('.')[0]) !== JSPDF_TESTED_MAJOR) {
|
|
18505
|
+
return {loaded: true, version: version, status: 'note',
|
|
18506
|
+
detail: 'this release is tested with jspdf ' + JSPDF_TESTED_MAJOR};
|
|
18507
|
+
}
|
|
18508
|
+
return {loaded: true, version: version, status: 'ok'};
|
|
18509
|
+
}
|
|
18510
|
+
if (g.jsPDF) {
|
|
18511
|
+
return {loaded: true, status: 'problem', detail: 'loaded under its old global "jsPDF" (jsPDF 1);'
|
|
18512
|
+
+ ' PDF export needs jspdf ' + JSPDF_TESTED_MAJOR + ' as window.jspdf, so it is off'};
|
|
18513
|
+
}
|
|
18514
|
+
return {loaded: false, status: 'absent', detail: 'not loaded: PDF export is off'};
|
|
18515
|
+
}));
|
|
18516
|
+
libs.push(doctorLibrary('svg2pdf.js', 'optional', function () {
|
|
18517
|
+
let pair = pdfPair();
|
|
18518
|
+
let J = pair.J;
|
|
18519
|
+
let attached = pair.attached;
|
|
18520
|
+
if (attached) {
|
|
18521
|
+
return {loaded: true, status: 'ok'};
|
|
18522
|
+
}
|
|
18523
|
+
if (g.svg2pdf && J) {
|
|
18524
|
+
return {loaded: true, status: 'problem', detail: 'loaded, but not attached to jspdf (jsPDF.API.svg'
|
|
18525
|
+
+ ' is missing): load jspdf first. PDF export is off'};
|
|
18526
|
+
}
|
|
18527
|
+
if (g.svg2pdf) {
|
|
18528
|
+
return {loaded: true, status: 'problem',
|
|
18529
|
+
detail: 'loaded without jspdf: PDF export needs both, so it is off'};
|
|
18530
|
+
}
|
|
18531
|
+
return {loaded: false, status: 'absent',
|
|
18532
|
+
detail: J ? 'not loaded' : 'not loaded: PDF export is off'};
|
|
18533
|
+
}));
|
|
18534
|
+
return libs;
|
|
18535
|
+
}
|
|
18536
|
+
|
|
18537
|
+
// What the user gets or does not get, each with the reason.
|
|
18538
|
+
function doctorFunctions(libs) {
|
|
18539
|
+
let lib = function (name) {
|
|
18540
|
+
return libs.find(function (l) {
|
|
18541
|
+
return l.name === name;
|
|
18542
|
+
});
|
|
18543
|
+
};
|
|
18544
|
+
let inBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
18545
|
+
let fn = function (name, test, whyNot) {
|
|
18546
|
+
let available = false;
|
|
18547
|
+
let detail = '';
|
|
18548
|
+
try {
|
|
18549
|
+
available = inBrowser && !!test();
|
|
18550
|
+
detail = available ? '' : (inBrowser ? whyNot() : 'needs a browser');
|
|
18551
|
+
} catch (e) {
|
|
18552
|
+
detail = 'this check failed: ' + (e && e.message ? e.message : e);
|
|
18553
|
+
}
|
|
18554
|
+
return {name: name, available: available, detail: detail};
|
|
18555
|
+
};
|
|
18556
|
+
// why a function is off, in the library's name: its row's reason
|
|
18557
|
+
// reads "not loaded: ..." under its own name and needs the subject here
|
|
18558
|
+
let why = function (l) {
|
|
18559
|
+
return l.status === 'absent' ? l.name + ' is not loaded' : l.name + ': ' + l.detail;
|
|
18560
|
+
};
|
|
18561
|
+
let out = [];
|
|
18562
|
+
try {
|
|
18563
|
+
// ON whenever phyloxml reads and writes at all -- which is what
|
|
18564
|
+
// the Download menu's phyloXML entry does, and it is always
|
|
18565
|
+
// offered. An older copy still works: its row above says what a
|
|
18566
|
+
// saved file loses, and this row repeats it without turning off.
|
|
18567
|
+
let px = lib('phyloxml');
|
|
18568
|
+
let works = px.loaded && !/(cannot read phyloXML|a small tree failed|this check failed)/.test(px.detail);
|
|
18569
|
+
out.push({name: 'Read and save phyloXML', available: works,
|
|
18570
|
+
detail: px.status === 'ok' ? '' : (works ? px.detail : why(px))});
|
|
18571
|
+
} catch (e) {
|
|
18572
|
+
out.push({name: 'Read and save phyloXML', available: false,
|
|
18573
|
+
detail: 'this check failed: ' + (e && e.message ? e.message : e)});
|
|
18574
|
+
}
|
|
18575
|
+
out.push(fn('PNG export', pngExportAvailable, function () {
|
|
18576
|
+
return why(lib('canvg'));
|
|
18577
|
+
}));
|
|
18578
|
+
out.push(fn('Copy PNG', pngCopyAvailable, function () {
|
|
18579
|
+
if (!pngExportAvailable()) {
|
|
18580
|
+
return why(lib('canvg'));
|
|
18581
|
+
}
|
|
18582
|
+
return window.isSecureContext ? 'this browser cannot write an image to the clipboard'
|
|
18583
|
+
: 'needs a secure page (https, or localhost)';
|
|
18584
|
+
}));
|
|
18585
|
+
out.push(fn('PDF export', pdfExportAvailable, function () {
|
|
18586
|
+
let pair = [lib('jspdf'), lib('svg2pdf.js')];
|
|
18587
|
+
let bad = pair.find(function (l) {
|
|
18588
|
+
return l.status === 'problem';
|
|
18589
|
+
});
|
|
18590
|
+
if (bad) {
|
|
18591
|
+
return why(bad);
|
|
18592
|
+
}
|
|
18593
|
+
return pair.every(function (l) {
|
|
18594
|
+
return l.status === 'absent';
|
|
18595
|
+
}) ? 'jspdf and svg2pdf.js are not loaded' : why(pair.find(function (l) {
|
|
18596
|
+
return l.status !== 'ok';
|
|
18597
|
+
}) || pair[0]);
|
|
18598
|
+
}));
|
|
18599
|
+
return out;
|
|
18600
|
+
}
|
|
18601
|
+
|
|
18602
|
+
// A config value as one short word or number: enough to see what a site
|
|
18603
|
+
// passed, never the value itself where that could be a tree or a callback.
|
|
18604
|
+
function doctorConfigValue(v) {
|
|
18605
|
+
if (v === null || typeof v === 'boolean' || typeof v === 'number') {
|
|
18606
|
+
return v;
|
|
18607
|
+
}
|
|
18608
|
+
if (typeof v === 'string') {
|
|
18609
|
+
return v.length > 80 ? v.substring(0, 77) + '...' : v;
|
|
18610
|
+
}
|
|
18611
|
+
if (typeof v === 'function') {
|
|
18612
|
+
return '(function)';
|
|
18613
|
+
}
|
|
18614
|
+
if (Array.isArray(v)) {
|
|
18615
|
+
return '(list of ' + v.length + ')';
|
|
18616
|
+
}
|
|
18617
|
+
return '(' + typeof v + ')';
|
|
18618
|
+
}
|
|
18619
|
+
|
|
18620
|
+
// The tree on view, as noted when its launch took the page over
|
|
18621
|
+
// (_onView): null until one has, so a first launch that was rejected
|
|
18622
|
+
// describes no tree. The tip count is the one the launch computed.
|
|
18623
|
+
function doctorLaunch() {
|
|
18624
|
+
if (!_onView) {
|
|
18625
|
+
return null;
|
|
18626
|
+
}
|
|
18627
|
+
let launch = {config: {}, tips: null, tree: _onView.tree, trees: _onView.trees, container: null};
|
|
18628
|
+
try {
|
|
18629
|
+
Object.keys(_onView.config || {}).sort().forEach(function (k) {
|
|
18630
|
+
launch.config[k] = doctorConfigValue(_onView.config[k]);
|
|
18631
|
+
});
|
|
18632
|
+
} catch {
|
|
18633
|
+
launch.config = {};
|
|
18634
|
+
}
|
|
18635
|
+
launch.tips = (typeof _onView.tips === 'number') ? _onView.tips : null;
|
|
18636
|
+
try {
|
|
18637
|
+
launch.container = {width: _onView.container.clientWidth, height: _onView.container.clientHeight};
|
|
18638
|
+
} catch {
|
|
18639
|
+
launch.container = null;
|
|
18640
|
+
}
|
|
18641
|
+
return launch;
|
|
18642
|
+
}
|
|
18643
|
+
|
|
18644
|
+
function doctorText(rep) {
|
|
18645
|
+
let pad = function (text, n) {
|
|
18646
|
+
text = String(text);
|
|
18647
|
+
return text + ' '.repeat(Math.max(1, n - text.length));
|
|
18648
|
+
};
|
|
18649
|
+
let lines = [NAME + ' ' + rep.version + ' diagnostics', ''];
|
|
18650
|
+
lines.push('Status: ' + rep.summary);
|
|
18651
|
+
rep.problems.forEach(function (p) {
|
|
18652
|
+
lines.push(' ! ' + p);
|
|
18653
|
+
});
|
|
18654
|
+
lines.push('', 'Libraries');
|
|
18655
|
+
rep.libraries.forEach(function (l) {
|
|
18656
|
+
lines.push(' ' + pad(l.name, 13) + pad(l.version || (l.loaded ? 'loaded' : 'not loaded'), 12)
|
|
18657
|
+
+ pad(l.status === 'absent' ? l.role : l.status, 9) + l.detail);
|
|
18658
|
+
});
|
|
18659
|
+
lines.push('', 'Functions');
|
|
18660
|
+
rep.functions.forEach(function (f) {
|
|
18661
|
+
lines.push(' ' + pad(f.name, 24) + (f.available ? 'on' : 'off') + (f.detail ? ' ' + f.detail : ''));
|
|
18662
|
+
});
|
|
18663
|
+
if (rep.notes.length > 0) {
|
|
18664
|
+
lines.push('', 'Notes');
|
|
18665
|
+
rep.notes.forEach(function (n) {
|
|
18666
|
+
lines.push(' - ' + n);
|
|
18667
|
+
});
|
|
18668
|
+
}
|
|
18669
|
+
lines.push('', 'Launch');
|
|
18670
|
+
if (rep.launch) {
|
|
18671
|
+
lines.push(' config: ' + JSON.stringify(rep.launch.config));
|
|
18672
|
+
if (rep.launch.tips !== null) {
|
|
18673
|
+
lines.push(' tree: ' + rep.launch.tips + ' tips' + (rep.launch.trees > 1
|
|
18674
|
+
? ' (tree ' + rep.launch.tree + ' of ' + rep.launch.trees + ')' : ''));
|
|
18675
|
+
}
|
|
18676
|
+
if (rep.launch.container) {
|
|
18677
|
+
lines.push(' container: ' + rep.launch.container.width + ' x ' + rep.launch.container.height + ' px');
|
|
18678
|
+
}
|
|
18679
|
+
} else {
|
|
18680
|
+
lines.push(' no tree is on view');
|
|
18681
|
+
}
|
|
18682
|
+
lines.push('', 'Environment');
|
|
18683
|
+
lines.push(' ' + (rep.environment.userAgent || 'no browser'));
|
|
18684
|
+
if (rep.environment.secureContext !== null) {
|
|
18685
|
+
lines.push(' secure page: ' + (rep.environment.secureContext ? 'yes' : 'no'));
|
|
18686
|
+
}
|
|
18687
|
+
return lines.join('\n');
|
|
18688
|
+
}
|
|
18689
|
+
|
|
18690
|
+
// The problems and the notes: the libraries' and what the launch said.
|
|
18691
|
+
// This is all the dot needs, so it is kept apart from the report, which
|
|
18692
|
+
// also works out the functions, the launch section and the text.
|
|
18693
|
+
function doctorFindings() {
|
|
18694
|
+
let libs;
|
|
18695
|
+
try {
|
|
18696
|
+
libs = doctorLibraries();
|
|
18697
|
+
} catch (e) {
|
|
18698
|
+
libs = [{name: 'libraries', role: 'required', loaded: false, version: null, status: 'problem',
|
|
18699
|
+
detail: 'the checks failed: ' + (e && e.message ? e.message : e)}];
|
|
18700
|
+
}
|
|
18701
|
+
let problems = [];
|
|
18702
|
+
let notes = [];
|
|
18703
|
+
libs.forEach(function (l) {
|
|
18704
|
+
if (l.status === 'problem') {
|
|
18705
|
+
problems.push(l.name + ': ' + l.detail);
|
|
18706
|
+
} else if (l.status === 'note') {
|
|
18707
|
+
notes.push(l.name + (l.version ? ' ' + l.version : '') + ': ' + l.detail);
|
|
18708
|
+
}
|
|
18709
|
+
});
|
|
18710
|
+
_diagnostics.forEach(function (d) {
|
|
18711
|
+
(d.level === 'problem' ? problems : notes).push(d.text + (d.count > 1 ? ' (' + d.count + ' times)' : ''));
|
|
18712
|
+
});
|
|
18713
|
+
return {libraries: libs, problems: problems, notes: notes,
|
|
18714
|
+
summary: problems.length === 0 ? 'nothing to report'
|
|
18715
|
+
: problems.length + (problems.length === 1 ? ' thing' : ' things') + ' to check'};
|
|
18716
|
+
}
|
|
18717
|
+
|
|
18718
|
+
function doctorReport() {
|
|
18719
|
+
let found = doctorFindings();
|
|
18720
|
+
let libs = found.libraries;
|
|
18721
|
+
let problems = found.problems;
|
|
18722
|
+
let notes = found.notes;
|
|
18723
|
+
let functions;
|
|
18724
|
+
try {
|
|
18725
|
+
functions = doctorFunctions(libs);
|
|
18726
|
+
} catch {
|
|
18727
|
+
functions = [];
|
|
18728
|
+
}
|
|
18729
|
+
let launch = null;
|
|
18730
|
+
try {
|
|
18731
|
+
launch = doctorLaunch();
|
|
18732
|
+
} catch {
|
|
18733
|
+
launch = null;
|
|
18734
|
+
}
|
|
18735
|
+
let nav = (typeof navigator !== 'undefined') ? navigator : null;
|
|
18736
|
+
let rep = {
|
|
18737
|
+
program: NAME,
|
|
18738
|
+
version: VERSION,
|
|
18739
|
+
ok: problems.length === 0,
|
|
18740
|
+
summary: found.summary,
|
|
18741
|
+
problems: problems,
|
|
18742
|
+
notes: notes,
|
|
18743
|
+
libraries: libs,
|
|
18744
|
+
functions: functions,
|
|
18745
|
+
launch: launch,
|
|
18746
|
+
environment: {
|
|
18747
|
+
userAgent: nav && nav.userAgent ? nav.userAgent : null,
|
|
18748
|
+
secureContext: (typeof window !== 'undefined' && typeof window.isSecureContext === 'boolean')
|
|
18749
|
+
? window.isSecureContext : null
|
|
18750
|
+
}
|
|
18751
|
+
};
|
|
18752
|
+
try {
|
|
18753
|
+
rep.text = doctorText(rep);
|
|
18754
|
+
} catch (e) {
|
|
18755
|
+
rep.text = NAME + ' ' + VERSION + ' diagnostics: the report could not be written ('
|
|
18756
|
+
+ (e && e.message ? e.message : e) + ')';
|
|
18757
|
+
}
|
|
18758
|
+
return rep;
|
|
18759
|
+
}
|
|
18760
|
+
|
|
18761
|
+
/**
|
|
18762
|
+
* The state of the program on this page: which libraries are loaded (and
|
|
18763
|
+
* their versions, where a library states one), which functions that
|
|
18764
|
+
* switches off, and what the last launch() warned about. Works before a
|
|
18765
|
+
* launch and after a failed one, and in Node. Never throws.
|
|
18766
|
+
*
|
|
18767
|
+
* @returns {{program: string, version: string, ok: boolean, summary: string,
|
|
18768
|
+
* problems: string[], notes: string[], libraries: Object[], functions: Object[],
|
|
18769
|
+
* launch: Object|null, environment: Object, text: string}} `ok` is false when
|
|
18770
|
+
* something is wrong that nobody chooses (see README "Diagnostics");
|
|
18771
|
+
* `text` is the whole report as plain text, for pasting into an issue.
|
|
18772
|
+
*/
|
|
18773
|
+
archaeopteryx.doctor = function () {
|
|
18774
|
+
let rep = doctorReport();
|
|
18775
|
+
syncDoctorDot(); // whoever asks sees a dot that agrees with the answer
|
|
18776
|
+
return rep;
|
|
18777
|
+
};
|
|
18778
|
+
|
|
18779
|
+
// The "check engine light": a small dot after the program name in the
|
|
18780
|
+
// panel header, there only while the report has a problem. Neutral, not
|
|
18781
|
+
// a warning colour: the people who see it are mostly the site's users,
|
|
18782
|
+
// who can do nothing about it, and it is for the one who can.
|
|
18783
|
+
//
|
|
18784
|
+
// What a page has loaded can change under a viewer that is already up (a
|
|
18785
|
+
// library arriving late, a script replacing a global) and nothing
|
|
18786
|
+
// announces it, so the dot is brought up to date whenever it could be
|
|
18787
|
+
// looked at or asked about: when a problem is recorded, when the header
|
|
18788
|
+
// is built, when the page finishes loading, when the pointer enters the
|
|
18789
|
+
// panel, when the About box opens and when doctor() is called.
|
|
18790
|
+
function syncDoctorDot() {
|
|
18791
|
+
try {
|
|
18792
|
+
let host = _onView && _onView.container;
|
|
18793
|
+
if (!host || typeof host.querySelector !== 'function') {
|
|
18794
|
+
return;
|
|
18795
|
+
}
|
|
18796
|
+
let button = host.querySelector('.aptx-panel .' + PROGNAMELINK);
|
|
18797
|
+
let dot = button && button.querySelector('.aptx-doctor-dot');
|
|
18798
|
+
if (!dot) {
|
|
18799
|
+
return;
|
|
18800
|
+
}
|
|
18801
|
+
let found = doctorFindings();
|
|
18802
|
+
let ok = found.problems.length === 0;
|
|
18803
|
+
dot.hidden = ok;
|
|
18804
|
+
button.title = 'About ' + NAME + (ok ? '' : ': ' + found.summary);
|
|
18805
|
+
} catch {
|
|
18806
|
+
// a light that cannot be lit must not take the viewer with it
|
|
18807
|
+
}
|
|
18808
|
+
}
|
|
18809
|
+
|
|
18810
|
+
function showDoctorDialog() {
|
|
18811
|
+
let rep = doctorReport();
|
|
18812
|
+
let shell = makeDialogShell(DOCTOR_DIALOG, 'Diagnostics', 500);
|
|
18813
|
+
shell.body.classList.add('aptx-doctor');
|
|
18814
|
+
|
|
18815
|
+
// The summary and Copy report share the first line: the report is
|
|
18816
|
+
// longer than a small window, and the one button must not be the
|
|
18817
|
+
// thing scrolled out of sight.
|
|
18818
|
+
let head = document.createElement('div');
|
|
18819
|
+
head.className = 'aptx-doctor-head';
|
|
18820
|
+
let status = document.createElement('div');
|
|
18821
|
+
status.className = 'aptx-doctor-status' + (rep.ok ? '' : ' aptx-doctor-bad');
|
|
18822
|
+
status.textContent = NAME + ' ' + rep.version + ': ' + rep.summary;
|
|
18823
|
+
let copy = document.createElement('button');
|
|
18824
|
+
copy.type = 'button';
|
|
18825
|
+
copy.className = 'aptx-reps-button aptx-doctor-copy';
|
|
18826
|
+
copy.textContent = 'Copy report';
|
|
18827
|
+
copy.title = 'copy this report as text, with the launch configuration and the browser added,'
|
|
18828
|
+
+ ' to paste into an issue or an e-mail';
|
|
18829
|
+
copy.addEventListener('click', function () {
|
|
18830
|
+
copyText(rep.text, shell.dialog).then(function (ok) {
|
|
18831
|
+
flashButton(copy, ok ? 'Copied \u2713' : 'Copy failed');
|
|
18832
|
+
});
|
|
18833
|
+
});
|
|
18834
|
+
head.appendChild(status);
|
|
18835
|
+
head.appendChild(copy);
|
|
18836
|
+
shell.body.appendChild(head);
|
|
18837
|
+
|
|
18838
|
+
let list = function (items, cls) {
|
|
18839
|
+
items.forEach(function (text) {
|
|
18840
|
+
let item = document.createElement('div');
|
|
18841
|
+
item.className = 'aptx-doctor-item ' + cls;
|
|
18842
|
+
item.textContent = text; // may quote a config value: never markup
|
|
18843
|
+
shell.body.appendChild(item);
|
|
18844
|
+
});
|
|
18845
|
+
};
|
|
18846
|
+
let section = function (title) {
|
|
18847
|
+
let h = document.createElement('div');
|
|
18848
|
+
h.className = 'aptx-doctor-section';
|
|
18849
|
+
h.textContent = title;
|
|
18850
|
+
shell.body.appendChild(h);
|
|
18851
|
+
};
|
|
18852
|
+
let row = function (key, value, detail, bad) {
|
|
18853
|
+
let line = document.createElement('div');
|
|
18854
|
+
line.className = 'aptx-dialog-line' + (bad ? ' aptx-doctor-bad' : '');
|
|
18855
|
+
let k = document.createElement('span');
|
|
18856
|
+
k.className = 'aptx-dialog-key';
|
|
18857
|
+
k.textContent = key;
|
|
18858
|
+
let v = document.createElement('span');
|
|
18859
|
+
v.className = 'aptx-dialog-val';
|
|
18860
|
+
let main = document.createElement('span');
|
|
18861
|
+
main.className = 'aptx-doctor-value';
|
|
18862
|
+
main.textContent = value;
|
|
18863
|
+
v.appendChild(main);
|
|
18864
|
+
if (detail) {
|
|
18865
|
+
let d = document.createElement('span');
|
|
18866
|
+
d.className = 'aptx-doctor-detail';
|
|
18867
|
+
d.textContent = detail;
|
|
18868
|
+
v.appendChild(d);
|
|
18869
|
+
}
|
|
18870
|
+
line.appendChild(k);
|
|
18871
|
+
line.appendChild(v);
|
|
18872
|
+
shell.body.appendChild(line);
|
|
18873
|
+
};
|
|
18874
|
+
list(rep.problems, 'aptx-doctor-bad');
|
|
18875
|
+
|
|
18876
|
+
section('Libraries');
|
|
18877
|
+
rep.libraries.forEach(function (l) {
|
|
18878
|
+
// "not loaded" is the value: a reason that opens with the same
|
|
18879
|
+
// two words and a colon does not say them again. (Only that
|
|
18880
|
+
// exact opening: "loaded, but not attached..." keeps its start.)
|
|
18881
|
+
let value = l.version || (l.loaded ? 'loaded' : 'not loaded');
|
|
18882
|
+
let detail = l.detail === value ? ''
|
|
18883
|
+
: (l.detail.indexOf('not loaded: ') === 0 && value === 'not loaded' ? l.detail.substring(12) : l.detail);
|
|
18884
|
+
row(l.name, value, detail, l.status === 'problem');
|
|
18885
|
+
});
|
|
18886
|
+
section('Functions');
|
|
18887
|
+
rep.functions.forEach(function (f) {
|
|
18888
|
+
row(f.name, f.available ? 'on' : 'off', f.detail, false);
|
|
18889
|
+
});
|
|
18890
|
+
if (rep.notes.length > 0) {
|
|
18891
|
+
section('Notes');
|
|
18892
|
+
list(rep.notes, 'aptx-doctor-note');
|
|
18893
|
+
}
|
|
18894
|
+
|
|
18895
|
+
shell.dialog.showModal();
|
|
18896
|
+
}
|
|
18897
|
+
|
|
18091
18898
|
// The About box behind the panel's title. Everything here is fixed text and
|
|
18092
18899
|
// fixed URLs -- no tree data reaches it -- so the links are built as real
|
|
18093
|
-
// elements rather than markup.
|
|
18900
|
+
// elements rather than markup. (The Diagnostics row's text is the doctor's
|
|
18901
|
+
// own summary: a count, never a value.)
|
|
18094
18902
|
function showAboutDialog() {
|
|
18903
|
+
let rep = doctorReport();
|
|
18904
|
+
syncDoctorDot();
|
|
18095
18905
|
let shell = makeDialogShell(ABOUT_DIALOG, 'About', 380);
|
|
18096
18906
|
shell.body.classList.add('aptx-about');
|
|
18097
18907
|
|
|
@@ -18123,8 +18933,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
18123
18933
|
['Desktop version', DESKTOP_WEBSITE, 'cmzmasek.github.io/archaeopteryx'],
|
|
18124
18934
|
['Source code', SOURCE_WEBSITE, 'github.com/cmzmasek/archaeopteryx-js'],
|
|
18125
18935
|
['License', LICENSE_WEBSITE, LICENSE_NAME],
|
|
18126
|
-
['Keyboard', null, 'shortcuts (' + (IS_MAC ? '\u2318 /' : 'Ctrl+/') + ')'],
|
|
18127
|
-
['Control panel', null, 'what each control does']
|
|
18936
|
+
['Keyboard', null, 'shortcuts (' + (IS_MAC ? '\u2318 /' : 'Ctrl+/') + ')', showShortcutsDialog],
|
|
18937
|
+
['Control panel', null, 'what each control does', showCheatSheetDialog],
|
|
18938
|
+
['Diagnostics', null, rep.summary, showDoctorDialog]].forEach(function (row) {
|
|
18128
18939
|
let line = document.createElement('div');
|
|
18129
18940
|
line.className = 'aptx-dialog-line';
|
|
18130
18941
|
let key = document.createElement('span');
|
|
@@ -18138,9 +18949,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
18138
18949
|
a.target = '_blank';
|
|
18139
18950
|
a.rel = 'noopener noreferrer';
|
|
18140
18951
|
} else {
|
|
18141
|
-
// the
|
|
18142
|
-
//
|
|
18143
|
-
let opener = row[
|
|
18952
|
+
// the rows with no URL open another dialog in place of this
|
|
18953
|
+
// one: the key list, the control-panel sheet, the diagnostics
|
|
18954
|
+
let opener = row[3];
|
|
18144
18955
|
a.href = '#';
|
|
18145
18956
|
a.addEventListener('click', function (e) {
|
|
18146
18957
|
e.preventDefault();
|
|
@@ -18149,6 +18960,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
18149
18960
|
});
|
|
18150
18961
|
}
|
|
18151
18962
|
a.textContent = row[2];
|
|
18963
|
+
if (row[0] === 'Diagnostics') {
|
|
18964
|
+
a.className = 'aptx-doctor-link';
|
|
18965
|
+
if (!rep.ok) {
|
|
18966
|
+
let dot = document.createElement('span');
|
|
18967
|
+
dot.className = 'aptx-doctor-dot';
|
|
18968
|
+
val.appendChild(dot);
|
|
18969
|
+
}
|
|
18970
|
+
}
|
|
18152
18971
|
val.appendChild(a);
|
|
18153
18972
|
line.appendChild(key);
|
|
18154
18973
|
line.appendChild(val);
|
|
@@ -18720,7 +19539,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
18720
19539
|
// by this, and a version is a value -- it belongs in the About box
|
|
18721
19540
|
// it opens, not in a row that says what the button is
|
|
18722
19541
|
h = h.concat('<button type="button" class="' + PROGNAMELINK + '" aria-label="' + NAME
|
|
18723
|
-
+ '" title="About ' + NAME + '">' + NAME + ' ' + VERSION
|
|
19542
|
+
+ '" title="About ' + NAME + '">' + NAME + ' ' + VERSION
|
|
19543
|
+
+ '<span class="aptx-doctor-dot" hidden></span></button>');
|
|
18724
19544
|
h = h.concat('</div>');
|
|
18725
19545
|
return h;
|
|
18726
19546
|
}
|
|
@@ -19397,7 +20217,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
19397
20217
|
// 2x, 4x and 8x, and the setting is now the one it starts on
|
|
19398
20218
|
// (Christian, 2026-09-28) -- said, not done silently
|
|
19399
20219
|
if (_pngScale !== _settings.pngExportScale) {
|
|
19400
|
-
|
|
20220
|
+
say('problem', 'warn', WARNING + ': pngExportScale ' + _settings.pngExportScale + ' is not one of '
|
|
19401
20221
|
+ DOWNLOAD_PNG_SCALES.join(', ') + ': the PNG starts at ' + _pngScale + 'x');
|
|
19402
20222
|
}
|
|
19403
20223
|
setRadioButtonValue(_downloadFullSize ? DOWNLOAD_FULL_SIZE_BUTTON : DOWNLOAD_AS_SHOWN_BUTTON, true);
|
|
@@ -20139,7 +20959,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20139
20959
|
|
|
20140
20960
|
function downloadAsPdf(graphic) {
|
|
20141
20961
|
if (!pdfExportAvailable()) {
|
|
20142
|
-
|
|
20962
|
+
say('note', 'error', ERROR + 'PDF export needs the optional jspdf and svg2pdf.js libraries on the page');
|
|
20143
20963
|
return;
|
|
20144
20964
|
}
|
|
20145
20965
|
let el = new DOMParser().parseFromString(graphic.text, 'image/svg+xml').documentElement;
|
|
@@ -20188,7 +21008,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20188
21008
|
holder.remove();
|
|
20189
21009
|
}, function (err) {
|
|
20190
21010
|
holder.remove();
|
|
20191
|
-
|
|
21011
|
+
say('note', 'error', ERROR + 'PDF export failed: ' + err);
|
|
20192
21012
|
});
|
|
20193
21013
|
}
|
|
20194
21014
|
|
|
@@ -20234,7 +21054,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20234
21054
|
renderPng(graphic).then(function (blob) {
|
|
20235
21055
|
saveAs(blob, downloadFileName(PNG_SUFFIX));
|
|
20236
21056
|
}, function (err) {
|
|
20237
|
-
|
|
21057
|
+
say('note', 'error', ERROR + 'PNG export failed: ' + err);
|
|
20238
21058
|
});
|
|
20239
21059
|
}
|
|
20240
21060
|
|
|
@@ -20252,7 +21072,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20252
21072
|
return Promise.reject(new Error('this browser cannot paint a canvas of that size'));
|
|
20253
21073
|
}
|
|
20254
21074
|
if (target.scale < _pngScale) {
|
|
20255
|
-
|
|
21075
|
+
say('note', 'warn', WARNING + ': PNG exported at ' + target.scale.toFixed(2) + 'x rather than ' + _pngScale
|
|
20256
21076
|
+ 'x: the largest image this browser can paint');
|
|
20257
21077
|
}
|
|
20258
21078
|
let canvas = target.canvas;
|
|
@@ -20313,7 +21133,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20313
21133
|
written.then(function () {
|
|
20314
21134
|
flashButton(btn, 'Copied ✓');
|
|
20315
21135
|
}, function (err) {
|
|
20316
|
-
|
|
21136
|
+
say('note', 'error', ERROR + 'copying the PNG failed: ' + err);
|
|
20317
21137
|
flashButton(btn, 'Copy failed');
|
|
20318
21138
|
});
|
|
20319
21139
|
}
|
|
@@ -20480,7 +21300,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
20480
21300
|
return undefined;
|
|
20481
21301
|
}
|
|
20482
21302
|
|
|
20483
|
-
archaeopteryx.launchArchaeopteryx = function (
|
|
21303
|
+
archaeopteryx.launchArchaeopteryx = function () {
|
|
21304
|
+
try {
|
|
21305
|
+
return parseAndLaunch.apply(null, arguments);
|
|
21306
|
+
} catch (e) {
|
|
21307
|
+
// its own failures -- a fifth argument, a file that does not
|
|
21308
|
+
// parse -- happen before launch() and its shell are reached
|
|
21309
|
+
recordLaunchFailure(e);
|
|
21310
|
+
throw e;
|
|
21311
|
+
}
|
|
21312
|
+
};
|
|
21313
|
+
function parseAndLaunch(container, fileName, data, config) {
|
|
20484
21314
|
if (arguments.length > 4) {
|
|
20485
21315
|
throw new Error(ERROR + 'launchArchaeopteryx() takes exactly (container, fileName,'
|
|
20486
21316
|
+ ' data, config); the old trailing arguments were removed. The separate settings'
|
|
@@ -20508,7 +21338,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20508
21338
|
// added nothing but a prefix, and swallowing them left the caller with
|
|
20509
21339
|
// a blank page and no way to find out why.
|
|
20510
21340
|
return archaeopteryx.launch(container, trees, config);
|
|
20511
|
-
}
|
|
21341
|
+
}
|
|
20512
21342
|
|
|
20513
21343
|
|
|
20514
21344
|
// --------------------------------------------------------------
|