483 lines
18 KiB
Svelte
483 lines
18 KiB
Svelte
<script>
|
|
import { tick } from 'svelte';
|
|
import { uniq, groupByArray, quantile_sorted, debounce, escapeHTML, escapeRegExp, strHashHSL, datetime_datestr, datetime_timestr, isInViewport, filter_pattern, filter_test, filter_mark, filter_snippets_lines, inherit } from '$lib/util.js';
|
|
import { get_api_measurements, get_api_labs } from '$lib/backend.js';
|
|
|
|
export let data;
|
|
|
|
let query = '', pattern = null, selection = null, component_items = decorate_measurements(data.measurements).concat(decorate_labs(data.labs));
|
|
let debounced_pattern = debounce((/*query*/) => (pattern = query ? filter_pattern(escapeHTML(query)) : null), 200);
|
|
|
|
$: debounced_pattern(query); // argument `query` is for reactivity hinting only
|
|
|
|
function decorate_measurements(xs) {
|
|
return xs.map(x => (x = Object.assign({ _content: escapeHTML(Object.values(x).join('\x00')), _ts: new Date(x.time) }, x), x.timestr = datetime_timestr(x._ts), x));
|
|
}
|
|
|
|
function decorate_labs(xs) {
|
|
return components(xs.map(x => Object.assign({ _content: escapeHTML(Object.values(x).join('\x00')), _ts: new Date(x.time_collected) }, x)));
|
|
}
|
|
|
|
function components(reports) {
|
|
const res = [], re_lab_test = /^(?<name>\w[^\r\n]{26})(?<value>[^\r\n]{8}) (?:(?<flag>\w[ \*])| ) (?<unit>[^\r\n]{10}) (?<range>[^\r\n]{16}) \[(?<site>\d+)\]$/gm;
|
|
reports.forEach(function(x) {
|
|
let datestr = datetime_datestr(x._ts), timestr = datetime_timestr(x._ts), m, r;
|
|
while(m = re_lab_test.exec(x.body)) {
|
|
m = m.groups;
|
|
for(let k in m) if(m[k]) m[k] = m[k].replace(/^\s+|\s+$/g, '');
|
|
if((r = m.range) && (r.includes(' - '))) {
|
|
r = r.split(' - ');
|
|
m.rangeL = r[0];
|
|
m.rangeH = r[1];
|
|
}
|
|
m.datestr = datestr;
|
|
m.timestr = timestr;
|
|
m.report = x;
|
|
Object.assign(m, x);
|
|
delete m.body;
|
|
delete m._content;
|
|
res.push(m);
|
|
}
|
|
});
|
|
return res;
|
|
}
|
|
|
|
function calculate(items) {
|
|
var snapshots = [], results = [], history, update, item;
|
|
groupByArray(items, x => x._ts).map(group => group.values.reduce(((acc, x) => (acc.values[x.name] = x, acc)), { key: group.key, values: {}})).sort((a, b) => (a.key > b.key) - (a.key < b.key)).forEach(function(group) {
|
|
snapshots.push({ key: group.key, values: history = Object.assign(snapshots.length > 0 ? inherit(snapshots[snapshots.length - 1].values) : inherit(calc_constants), update = group.values) });
|
|
history['Time'] = update['Time'] = { time: group.key, value: group.key };
|
|
for(var i = 0; i < calc_functions.length; ++i) {
|
|
var calculation = calc_functions[i], deps = calculation.deps;
|
|
for(var j = deps.length - 1, satisfied = true, updated = false; j >= 0; --j) if(!history[deps[j]]) { satisfied = false; break; }
|
|
else if(update[deps[j]]) updated = true;
|
|
if((satisfied) && (updated)) {
|
|
item = calculation.calc(...calculation.deps.map(x => history[x].value), history[calculation.name] && history[calculation.name].value);
|
|
if((item !== undefined) && (item !== null) && (item === item) && (item != 'NaN')) { // item === item if not NaN
|
|
results.push(history[calculation.name] = update[calculation.name] = Object.assign({ _ts: group.key, value: item }, calculation));
|
|
if((calculation.hasOwnProperty('rangeL')) && (item < calculation.rangeL)) update[calculation.name].flag = 'L';
|
|
else if((calculation.hasOwnProperty('rangeH')) && (item > calculation.rangeH)) update[calculation.name].flag = 'H';
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return results;
|
|
}
|
|
const calc_constants = {
|
|
DOB: { _ts: null, value: new Date(data.facesheet_data.dob) },
|
|
Sex: { _ts: null, value: data.facesheet_data.sex }
|
|
};
|
|
const calc_functions = [
|
|
{ name: 'Age', unit: 'yr', deps: ['Time', 'DOB'], calc(Time, DOB, prev) { var x = Math.floor((Time - DOB.getTime())/3.15576e10); return x != prev ? x : undefined; } },
|
|
{ name: 'BMI', unit: 'kg/m²', rangeL: 18.5, rangeH: 24.9, range: '18.5 - 24.9', deps: ['Ht', 'Wt'], calc: (Ht, Wt) => (10000*Wt/(Ht*Ht)).toPrecision(3) },
|
|
{ name: 'BSA', unit: 'm²', deps: ['Ht', 'Wt'], calc: (Ht, Wt) => (0.007184*Math.pow(Ht, 0.725)*Math.pow(Wt, 0.425)).toPrecision(3) },
|
|
{ name: 'CrCl', unit: 'mL/min', deps: ['Age', 'Sex', 'Wt', 'CREATININE'], calc: (Age, Sex, Wt, CREATININE) => (((140 - Age) * Wt)/(72*CREATININE)*(Sex == 'MALE' ? 1 : 0.85)).toPrecision(4) },
|
|
{ name: 'RETICYLOCYTE#', unit: 'K/cmm', rangeL: 50, rangeH: 100, range: '50 - 100', deps: ['RBC', 'RETICULOCYTES'], calc: (RBC, RETICULOCYTES) => (10*RBC*RETICULOCYTES).toPrecision(3) }
|
|
];
|
|
|
|
$: component_calculated = component_items.concat(calculate(component_items));
|
|
$: component_groups = groupByArray(component_calculated, x => new Date(x._ts.getFullYear(), x._ts.getMonth(), x._ts.getDate())).map(function(group) {
|
|
group = group.values.reduce(((acc, x) => ((acc.values[x.name] || (acc.values[x.name] = [])).push(x), acc)), { key: group.key, datestr: datetime_datestr(group.key), datestr_year: group.key.getFullYear(), values: {}});
|
|
for(var k in group.values) if(group.values.hasOwnProperty(k)) {
|
|
var items = group.values[k].sort((a, b) => a._ts - b._ts);
|
|
var strings = items.map(item => item.timestr + ' • ' + item.value + (item.unit ? ' ' + item.unit : '') + (item.flag ? ' [' + item.flag + ']' : '')/* + (item.comment && item.comment.indexOf('\n') < 0 ? ' • ' + item.comment : '')*/);
|
|
var flags = uniq(items.map(item => item.flag).filter(x => x).map(x => x.charAt(0)));
|
|
//var comments = uniq(items.map(item => item.comment).filter(x => x && x.indexOf('\n') >= 0));
|
|
var numbers = uniq(items.map(item => item.value).filter(x => isNumeric(x)));
|
|
var min = Math.min.apply(null, numbers);
|
|
var max = Math.max.apply(null, numbers);
|
|
group.values[k] = {
|
|
time: group.key,
|
|
name: k,
|
|
unit: items[0].unit,
|
|
range: items[0].range,
|
|
value: numbers.length > 1 ? min + ' - ' + max : numbers.length == 1 ? numbers[0] : items.length == 1 ? items[0].value : 'MULTIPLE', min: min, max: max,
|
|
flag: flags.length > 1 ? '*' : flags.length == 1 ? flags[0] : null,
|
|
reports: items.map(item => item.report).filter(x => x),
|
|
comment: (strings.join('\n') /*+ '\n\n' + comments.join('\n\n')*/).replace(/^\s+|\s+$/g, '')
|
|
};
|
|
}
|
|
return group;
|
|
}).sort((a, b) => (a.key > b.key) - (a.key < b.key));
|
|
$: component_names = uniq(component_calculated.map(x => x.name));
|
|
$: component_stats = statistics(component_calculated);
|
|
|
|
function isNumeric(n) {
|
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
|
}
|
|
function statistics(resultset) {
|
|
var res = {}, group, item;
|
|
for(var i = resultset.length - 1; i >= 0; --i) {
|
|
item = resultset[i];
|
|
if(isNumeric(item.value)) {
|
|
if(res[item.name]) res[item.name].push(+item.value);
|
|
else res[item.name] = [+item.value];
|
|
}
|
|
}
|
|
for(var k in res) if(res.hasOwnProperty(k)) {
|
|
item = res[k].sort((a, b) => a - b);
|
|
item = res[k] = {
|
|
n: item.length,
|
|
q25: quantile_sorted(item, 0.25),
|
|
q50: quantile_sorted(item, 0.50),
|
|
q75: quantile_sorted(item, 0.75)
|
|
}
|
|
item.range = item.q25 != item.q75 ? ('IQR: ' + item.q25 + ' - ' + item.q75) : ('Median: ' + item.q50);
|
|
}
|
|
return res;
|
|
}
|
|
function tooltip(item) {
|
|
if(item) {
|
|
var res = [], stat;
|
|
if(item.range) res.push('Ref: ' + item.range + ' ' + item.unit + (item.flag ? ' [' + item.flag + ']' : ''));
|
|
if(stat = component_stats[item.name]) res.push(stat.range + (item.range ? ' ' + item.unit : '') + (isNaN(parseFloat(item.value)) ? '' : item.value < stat.q25 ? ' [L]' : item.value > stat.q75 ? ' [H]' : ''));
|
|
if(item.comment) {
|
|
if(res.length > 0) res.push('');
|
|
res.push(item.comment);
|
|
}
|
|
return res.join('\n');
|
|
}
|
|
};
|
|
function abnormal_ref(item) {
|
|
return (item) && (item.flag);
|
|
};
|
|
function abnormal_ref_low(item) {
|
|
return (item) && (item.flag) && (item.flag.indexOf('L') >= 0);
|
|
};
|
|
function abnormal_ref_high(item) {
|
|
return (item) && (item.flag) && (item.flag.indexOf('H') >= 0);
|
|
};
|
|
function abnormal_iqr(item) {
|
|
var stat;
|
|
if((item) && (stat = component_stats[item.name]) && (stat.n > 2)) {
|
|
if((item.hasOwnProperty('min')) && (item.hasOwnProperty('max'))) return (item.min < stat.q25) || (item.max > stat.q75);
|
|
else if(isNumeric(item.value)) return (item.value < stat.q25) || (item.value > stat.q75);
|
|
}
|
|
};
|
|
function abnormal_iqr_low(item) {
|
|
var stat;
|
|
if((item) && (stat = component_stats[item.name]) && (stat.n > 2)) {
|
|
if((item.hasOwnProperty('min')) && (item.hasOwnProperty('max'))) return item.min < stat.q25;
|
|
else if(isNumeric(item.value)) return item.value < stat.q25;
|
|
}
|
|
};
|
|
function abnormal_iqr_high(item) {
|
|
var stat;
|
|
if((item) && (stat = component_stats[item.name]) && (stat.n > 2)) {
|
|
if((item.hasOwnProperty('min')) && (item.hasOwnProperty('max'))) return item.max > stat.q75;
|
|
else if(isNumeric(item.value)) return item.value > stat.q75;
|
|
}
|
|
};
|
|
|
|
function abstract(body) {
|
|
let re = /^(\w.*?)[ \t]{2,}\S.*?[ \t]{2,}/gm, m, res = [];
|
|
while(m = re.exec(body)) res.push(m[1]);
|
|
re = /^\*[ ]+((?:.+?) REPORT) =>/gm;
|
|
while(m = re.exec(body)) res.push(m[1]);
|
|
return res;
|
|
}
|
|
|
|
async function loadmore(evt, factor = 1.5) {
|
|
if(loadmore.loading) return;
|
|
loadmore.loading = true;
|
|
try {
|
|
const next_offset = data.offset + (loadmore.limit = (factor*loadmore.limit)|0);
|
|
const measurements = await get_api_measurements({ mrn: data.mrn, omega: 'T-' + (data.offset + 1), alpha: 'T-' + next_offset });
|
|
Array.prototype.push.apply(component_items, decorate_measurements(measurements));
|
|
const labs = await get_api_labs({ mrn: data.mrn, omega: 'T-' + (data.offset + 1), alpha: 'T-' + next_offset });
|
|
Array.prototype.push.apply(component_items, decorate_labs(labs));
|
|
component_items = component_items; // reactivity hint
|
|
data.offset = next_offset;
|
|
} finally {
|
|
loadmore.loading = false;
|
|
}
|
|
}
|
|
loadmore.loading = false;
|
|
loadmore.limit = 30;
|
|
|
|
let header;
|
|
(async function loadinit(target = 16, requests = 4) {
|
|
await tick();
|
|
for(let i = 0; (i < requests) && (component_groups.length < target); ++i) await loadmore();
|
|
await tick();
|
|
if((header) && (component_groups)) header.children[header.children.length - 1].scrollIntoView({ block: 'nearest', inline: 'end' });
|
|
})();
|
|
|
|
/*
|
|
const observer = new IntersectionObserver((entries) => { if((!query) && (entries[0].isIntersecting)) loadmore(null); }, { root: null, rootMargin: '0px', threshold: 0.5 });
|
|
let bottom = null;
|
|
$: {
|
|
observer.disconnect();
|
|
if(bottom) observer.observe(bottom);
|
|
}
|
|
|
|
let reportlist;
|
|
async function scroll(selection) {
|
|
if(selection) {
|
|
await tick();
|
|
const el = reportlist.querySelector('.active');
|
|
if((el) && (!isInViewport(el, true))) el.scrollIntoView({ block: 'center' });
|
|
} else {
|
|
const items = reportlist.children;
|
|
for(let i = 0, el; i < items.length; ++i) if(isInViewport(el = items[i])) {
|
|
await tick();
|
|
el.scrollIntoView({ block: 'start' });
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$: if(reportlist) scroll(selection);
|
|
*/
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Flowsheet</title>
|
|
</svelte:head>
|
|
|
|
{#if (selection) && (selection.length > 0)}
|
|
<div class="halfpane rightpane">
|
|
{#each selection as row, idx}
|
|
<nav class="navbar bg-body-secondary">
|
|
<div class="container-fluid">
|
|
<span class="navbar-brand">{datetime_datestr(row._ts)}@{datetime_timestr(row._ts)} {row.accession} {row.specimen}</span>
|
|
{#if idx == 0}<button type="button" class="btn btn-outline-light" on:click={() => selection = null}>❌</button>{/if}
|
|
</div>
|
|
</nav>
|
|
<div class="container-fluid report">{@html pattern ? filter_mark(pattern, escapeHTML(row.body)) : escapeHTML(row.body)}</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
<div class={(selection) && (selection.length > 0) ? 'halfpane leftpane' : ''}>
|
|
<div style="font-family: monospace;" role="region" tabindex="0">
|
|
<table class="table-sticky table-data">
|
|
<thead>
|
|
<tr bind:this={header}>
|
|
<th class="corner">
|
|
<input type="text" class="form-control" placeholder="Filter..." bind:value={query}>
|
|
{#if !loadmore.loading}<span class="badge rounded-pill bg-primary position-absolute top-0 start-100 translate-middle-x" style="cursor: pointer;" on:click={loadmore}>More</span>{/if}
|
|
</th>
|
|
{#each component_groups as group, idx}<th class="date" class:first={ (idx == 0) || (group.key != component_groups[idx - 1].key) }><div class="year">{group.datestr.substring(0, 4)}</div><div class="monthdate">{group.datestr.substring(5)}</div>{#if false}<div class="hourminute daily">{datetime_timestr(group.values[0]._ts)}</div>{/if}</th>{/each}
|
|
</tr>
|
|
</thead>
|
|
{#if pattern}
|
|
<tbody>
|
|
{#each component_names as name}
|
|
{#if filter_test(pattern, name)}
|
|
<tr class="match">
|
|
<th>{@html pattern ? filter_mark(pattern, escapeHTML(name)) : escapeHTML(name)}</th>
|
|
{#each component_groups as group, idx}
|
|
<td class:first={ (idx == 0) || (group.key != component_groups[idx - 1].key) } class:abnormal-ref={abnormal_ref(group.values[name])} class:abnormal-ref-low={abnormal_ref_low(group.values[name])} class:abnormal-ref-high={abnormal_ref_high(group.values[name])} class:abnormal-iqr={abnormal_iqr(group.values[name])} class:abnormal-iqr-low={abnormal_iqr_low(group.values[name])} class:abnormal-iqr-high={abnormal_iqr_high(group.values[name])} title={tooltip(group.values[name])} on:click={() => selection = (group.values[name]) && (group.values[name].reports)}>{group.values[name] ? group.values[name].value : ''}</td>
|
|
{/each}
|
|
</tr>
|
|
{/if}
|
|
{/each}
|
|
</tbody>
|
|
<tbody>
|
|
{#each component_names as name}
|
|
{#if !filter_test(pattern, name)}
|
|
<tr>
|
|
<th>{name}</th>
|
|
{#each component_groups as group, idx}
|
|
<td class:first={ (idx == 0) || (group.key != component_groups[idx - 1].key) } class:abnormal-ref={abnormal_ref(group.values[name])} class:abnormal-ref-low={abnormal_ref_low(group.values[name])} class:abnormal-ref-high={abnormal_ref_high(group.values[name])} class:abnormal-iqr={abnormal_iqr(group.values[name])} class:abnormal-iqr-low={abnormal_iqr_low(group.values[name])} class:abnormal-iqr-high={abnormal_iqr_high(group.values[name])} title={tooltip(group.values[name])} on:click={() => selection = (group.values[name]) && (group.values[name].reports)}>{group.values[name] ? group.values[name].value : ''}</td>
|
|
{/each}
|
|
</tr>
|
|
{/if}
|
|
{/each}
|
|
</tbody>
|
|
{:else}
|
|
<tbody>
|
|
{#each component_names as name}
|
|
<tr>
|
|
<th>{name}</th>
|
|
{#each component_groups as group, idx}
|
|
<td class:first={ (idx == 0) || (group.key != component_groups[idx - 1].key) } class:abnormal-ref={abnormal_ref(group.values[name])} class:abnormal-ref-low={abnormal_ref_low(group.values[name])} class:abnormal-ref-high={abnormal_ref_high(group.values[name])} class:abnormal-iqr={abnormal_iqr(group.values[name])} class:abnormal-iqr-low={abnormal_iqr_low(group.values[name])} class:abnormal-iqr-high={abnormal_iqr_high(group.values[name])} title={tooltip(group.values[name])} on:click={() => selection = (group.values[name]) && (group.values[name].reports)}>{group.values[name] ? group.values[name].value : ''}</td>
|
|
{/each}
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
{/if}
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
:global(th mark) {
|
|
padding: 0;
|
|
font-weight: bold;
|
|
}
|
|
@media (prefers-reduced-motion: no-preference) {
|
|
:root {
|
|
scroll-behavior: auto;
|
|
}
|
|
}
|
|
[role="region"][tabindex] {
|
|
max-height: calc(100vh - 4.5rem);
|
|
overflow: auto;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
}
|
|
table tr.match th:first-child {
|
|
background-color: #c6def1;
|
|
}
|
|
table th, table td {
|
|
padding: 0 0.5rem;
|
|
white-space: nowrap;
|
|
text-align: center;
|
|
}
|
|
table.table-sticky thead {
|
|
position: sticky;
|
|
z-index: 1020;
|
|
top: 0;
|
|
background-color: #fff;
|
|
}
|
|
table.table-sticky th:first-child {
|
|
position: sticky;
|
|
z-index: 1010;
|
|
left: 0;
|
|
background-color: #fff;
|
|
}
|
|
table.table-sticky thead th.corner {
|
|
padding: 0;
|
|
}
|
|
.navbar {
|
|
position: sticky;
|
|
z-index: 1020;
|
|
top: 3.5rem;
|
|
}
|
|
.leftpane {
|
|
display: none;
|
|
}
|
|
li.active {
|
|
scroll-margin-top: 3.5rem;
|
|
}
|
|
div.singleline {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.abstract {
|
|
font-size: 0.8em;
|
|
}
|
|
.snippets {
|
|
font-family: monospace;
|
|
}
|
|
.report {
|
|
font-family: monospace;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
table.table-sticky {
|
|
border: 2px solid #dee2e6;
|
|
}
|
|
table.table-sticky th:first-child {
|
|
border-right: 2px solid #dee2e6;
|
|
}
|
|
table.table-sticky tbody {
|
|
border-top: 2px solid #dee2e6;
|
|
}
|
|
table.table-sticky tbody tr {
|
|
border-top: 1px dashed #dee2e6;
|
|
}
|
|
table.table-sticky tbody tr:hover {
|
|
border: 1px solid #6c757d;
|
|
}
|
|
td:nth-of-type(odd) {
|
|
background-color: rgba(0, 0, 0, 0.05);
|
|
}
|
|
table.table-sticky tbody th, table.table-sticky th.name {
|
|
cursor: default;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
padding-left: 1rem;
|
|
padding-right: 1rem;
|
|
}
|
|
table.table-sticky th.filtered {
|
|
background-color: #6c757d;
|
|
color: #fff;
|
|
}
|
|
table.table-sticky th.date {
|
|
cursor: default;
|
|
font-size: 80%;
|
|
font-weight: normal;
|
|
text-align: center;
|
|
}
|
|
table.table-sticky th.date .monthdate {
|
|
font-size: 125%;
|
|
font-weight: bold;
|
|
}
|
|
table.table-sticky th.date .hourminute.daily {
|
|
display: none;
|
|
}
|
|
table.table-sticky tbody td {
|
|
padding: 0 0.5rem;
|
|
max-width: 12rem;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.first {
|
|
border-left: 1px solid #dee2e6;
|
|
}
|
|
.year, .monthdate {
|
|
visibility: hidden;
|
|
}
|
|
.first .year, .first .monthdate {
|
|
visibility: visible;
|
|
}
|
|
|
|
table.table-data .abnormal-ref::after {
|
|
content: ' *';
|
|
}
|
|
table.table-data .abnormal-ref-low::after {
|
|
content: ' L';
|
|
}
|
|
table.table-data .abnormal-ref-high::after {
|
|
content: ' H';
|
|
}
|
|
table.table-data .abnormal-iqr, table.table-data .abnormal-iqr-low.abnormal-iqr-high {
|
|
color: #f39a27;
|
|
}
|
|
table.table-data .abnormal-iqr-low {
|
|
color: #976ed7;
|
|
}
|
|
table.table-data .abnormal-iqr-high {
|
|
color: #c23b23;
|
|
}
|
|
table.table-data .abnormal-ref, table.table-data .abnormal-iqr {
|
|
background-color: #fbffde;
|
|
}
|
|
table.table-data .abnormal-ref-low.abnormal-iqr-low, table.table-data .abnormal-ref-high.abnormal-iqr-high {
|
|
font-weight: bold;
|
|
background-color: #ffd1d1;
|
|
}
|
|
|
|
@media screen and (min-width: 720px) {
|
|
.halfpane {
|
|
position: absolute;
|
|
top: 3.5rem;
|
|
bottom: 0;
|
|
width: 50%;
|
|
overflow: auto;
|
|
}
|
|
.leftpane {
|
|
display: block;
|
|
left: 0;
|
|
z-index: -1;
|
|
}
|
|
.leftpane [role="region"][tabindex] {
|
|
max-height: calc(100vh - 3.5rem);
|
|
direction: rtl;
|
|
}
|
|
.leftpane [role="region"][tabindex] > * {
|
|
direction: ltr;
|
|
}
|
|
.rightpane {
|
|
right: 0;
|
|
box-shadow: var(--bs-box-shadow);
|
|
}
|
|
.halfpane .navbar {
|
|
top: 0;
|
|
}
|
|
}
|
|
</style>
|