@jbrowse/plugin-bed 3.0.0 → 3.0.2
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/dist/BedAdapter/BedAdapter.js +6 -3
- package/dist/BedTabixAdapter/BedTabixAdapter.d.ts +25 -1
- package/dist/BedTabixAdapter/BedTabixAdapter.js +27 -10
- package/dist/BigBedAdapter/BigBedAdapter.js +13 -6
- package/esm/BedAdapter/BedAdapter.js +6 -3
- package/esm/BedTabixAdapter/BedTabixAdapter.d.ts +25 -1
- package/esm/BedTabixAdapter/BedTabixAdapter.js +28 -11
- package/esm/BigBedAdapter/BigBedAdapter.js +14 -7
- package/package.json +4 -6
|
@@ -127,9 +127,12 @@ class BedAdapter extends BaseAdapter_1.BaseFeatureDataAdapter {
|
|
|
127
127
|
return (0, rxjs_1.ObservableCreate)(async (observer) => {
|
|
128
128
|
const { start, end, refName } = query;
|
|
129
129
|
const intervalTree = await this.loadFeatureIntervalTree(refName);
|
|
130
|
-
intervalTree === null || intervalTree === void 0 ? void 0 : intervalTree.search([start, end])
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
const features = intervalTree === null || intervalTree === void 0 ? void 0 : intervalTree.search([start, end]);
|
|
131
|
+
if (features) {
|
|
132
|
+
for (const f of features) {
|
|
133
|
+
observer.next(f);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
133
136
|
observer.complete();
|
|
134
137
|
}, opts.stopToken);
|
|
135
138
|
}
|
|
@@ -11,9 +11,33 @@ export default class BedTabixAdapter extends BaseFeatureDataAdapter {
|
|
|
11
11
|
protected columnNames: string[];
|
|
12
12
|
protected scoreColumn: string;
|
|
13
13
|
static capabilities: string[];
|
|
14
|
+
setupP?: Promise<{
|
|
15
|
+
meta: Awaited<ReturnType<TabixIndexedFile['getMetadata']>>;
|
|
16
|
+
}>;
|
|
14
17
|
constructor(config: AnyConfigurationModel, getSubAdapter?: getSubAdapterType, pluginManager?: PluginManager);
|
|
15
18
|
getRefNames(opts?: BaseOptions): Promise<string[]>;
|
|
16
|
-
getHeader(): Promise<string>;
|
|
19
|
+
getHeader(opts?: BaseOptions): Promise<string>;
|
|
20
|
+
getMetadataPre2(_opts?: BaseOptions): Promise<{
|
|
21
|
+
meta: Awaited<ReturnType<TabixIndexedFile["getMetadata"]>>;
|
|
22
|
+
}>;
|
|
23
|
+
getMetadataPre(): Promise<{
|
|
24
|
+
meta: {
|
|
25
|
+
[key: string]: any;
|
|
26
|
+
refNameToId: Record<string, number>;
|
|
27
|
+
refIdToName: string[];
|
|
28
|
+
metaChar: string | null;
|
|
29
|
+
columnNumbers: {
|
|
30
|
+
ref: number;
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
};
|
|
34
|
+
coordinateType: string;
|
|
35
|
+
format: string;
|
|
36
|
+
};
|
|
37
|
+
}>;
|
|
38
|
+
getMetadata(opts?: BaseOptions): Promise<{
|
|
39
|
+
meta: Awaited<ReturnType<TabixIndexedFile["getMetadata"]>>;
|
|
40
|
+
}>;
|
|
17
41
|
getNames(): Promise<string[] | undefined>;
|
|
18
42
|
getFeatures(query: Region, opts?: BaseOptions): import("rxjs").Observable<Feature>;
|
|
19
43
|
freeResources(): void;
|
|
@@ -32,14 +32,31 @@ class BedTabixAdapter extends BaseAdapter_1.BaseFeatureDataAdapter {
|
|
|
32
32
|
async getRefNames(opts = {}) {
|
|
33
33
|
return this.bed.getReferenceSequenceNames(opts);
|
|
34
34
|
}
|
|
35
|
-
async getHeader() {
|
|
36
|
-
return this.bed.getHeader();
|
|
35
|
+
async getHeader(opts) {
|
|
36
|
+
return this.bed.getHeader(opts);
|
|
37
|
+
}
|
|
38
|
+
async getMetadataPre2(_opts) {
|
|
39
|
+
if (!this.setupP) {
|
|
40
|
+
this.setupP = this.getMetadataPre().catch((e) => {
|
|
41
|
+
this.setupP = undefined;
|
|
42
|
+
throw e;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return this.setupP;
|
|
46
|
+
}
|
|
47
|
+
async getMetadataPre() {
|
|
48
|
+
const meta = await this.bed.getMetadata();
|
|
49
|
+
return { meta };
|
|
50
|
+
}
|
|
51
|
+
async getMetadata(opts) {
|
|
52
|
+
const { statusCallback = () => { } } = opts || {};
|
|
53
|
+
return (0, util_1.updateStatus)('Downloading index', statusCallback, () => this.getMetadataPre2(opts));
|
|
37
54
|
}
|
|
38
55
|
async getNames() {
|
|
39
56
|
if (this.columnNames.length) {
|
|
40
57
|
return this.columnNames;
|
|
41
58
|
}
|
|
42
|
-
const header = await this.
|
|
59
|
+
const header = await this.getHeader();
|
|
43
60
|
const defs = header.split(/\n|\r\n|\r/).filter(f => !!f);
|
|
44
61
|
const defline = defs.at(-1);
|
|
45
62
|
return (defline === null || defline === void 0 ? void 0 : defline.includes('\t'))
|
|
@@ -49,10 +66,10 @@ class BedTabixAdapter extends BaseAdapter_1.BaseFeatureDataAdapter {
|
|
|
49
66
|
.map(f => f.trim())
|
|
50
67
|
: undefined;
|
|
51
68
|
}
|
|
52
|
-
getFeatures(query, opts
|
|
53
|
-
const { stopToken } = opts;
|
|
69
|
+
getFeatures(query, opts) {
|
|
70
|
+
const { stopToken, statusCallback = () => { } } = opts || {};
|
|
54
71
|
return (0, rxjs_1.ObservableCreate)(async (observer) => {
|
|
55
|
-
const meta = await this.
|
|
72
|
+
const { meta } = await this.getMetadata();
|
|
56
73
|
const { columnNumbers } = meta;
|
|
57
74
|
const colRef = columnNumbers.ref - 1;
|
|
58
75
|
const colStart = columnNumbers.start - 1;
|
|
@@ -60,7 +77,7 @@ class BedTabixAdapter extends BaseAdapter_1.BaseFeatureDataAdapter {
|
|
|
60
77
|
const names = await this.getNames();
|
|
61
78
|
let start = performance.now();
|
|
62
79
|
(0, stopToken_1.checkStopToken)(stopToken);
|
|
63
|
-
await this.bed.getLines(query.refName, query.start, query.end, {
|
|
80
|
+
await (0, util_1.updateStatus)('Downloading features', statusCallback, () => this.bed.getLines(query.refName, query.start, query.end, {
|
|
64
81
|
lineCallback: (line, fileOffset) => {
|
|
65
82
|
if (performance.now() - start > 200) {
|
|
66
83
|
(0, stopToken_1.checkStopToken)(stopToken);
|
|
@@ -77,10 +94,10 @@ class BedTabixAdapter extends BaseAdapter_1.BaseFeatureDataAdapter {
|
|
|
77
94
|
names,
|
|
78
95
|
})));
|
|
79
96
|
},
|
|
80
|
-
stopToken
|
|
81
|
-
});
|
|
97
|
+
stopToken,
|
|
98
|
+
}));
|
|
82
99
|
observer.complete();
|
|
83
|
-
},
|
|
100
|
+
}, stopToken);
|
|
84
101
|
}
|
|
85
102
|
freeResources() { }
|
|
86
103
|
}
|
|
@@ -67,17 +67,18 @@ class BigBedAdapter extends BaseAdapter_1.BaseFeatureDataAdapter {
|
|
|
67
67
|
}
|
|
68
68
|
async getFeaturesHelper({ query, opts, observer, allowRedispatch, originalQuery = query, }) {
|
|
69
69
|
var _a;
|
|
70
|
-
const { stopToken } = opts;
|
|
70
|
+
const { stopToken, statusCallback = () => { } } = opts;
|
|
71
71
|
const scoreColumn = this.getConf('scoreColumn');
|
|
72
72
|
const aggregateField = this.getConf('aggregateField');
|
|
73
|
-
const { parser, bigbed } = await this.configure(opts);
|
|
74
|
-
const feats = await bigbed.getFeatures(query.refName, query.start, query.end, {
|
|
73
|
+
const { parser, bigbed } = await (0, util_1.updateStatus)('Downloading header', statusCallback, () => this.configure(opts));
|
|
74
|
+
const feats = await (0, util_1.updateStatus)('Downloading features', statusCallback, () => bigbed.getFeatures(query.refName, query.start, query.end, {
|
|
75
75
|
stopToken,
|
|
76
76
|
basesPerSpan: query.end - query.start,
|
|
77
|
-
});
|
|
77
|
+
}));
|
|
78
78
|
if (allowRedispatch && feats.length) {
|
|
79
79
|
let minStart = Number.POSITIVE_INFINITY;
|
|
80
80
|
let maxEnd = Number.NEGATIVE_INFINITY;
|
|
81
|
+
let hasAnyAggregationField = false;
|
|
81
82
|
for (const feat of feats) {
|
|
82
83
|
if (feat.start < minStart) {
|
|
83
84
|
minStart = feat.start;
|
|
@@ -85,8 +86,12 @@ class BigBedAdapter extends BaseAdapter_1.BaseFeatureDataAdapter {
|
|
|
85
86
|
if (feat.end > maxEnd) {
|
|
86
87
|
maxEnd = feat.end;
|
|
87
88
|
}
|
|
89
|
+
if (feat[aggregateField]) {
|
|
90
|
+
hasAnyAggregationField = true;
|
|
91
|
+
}
|
|
88
92
|
}
|
|
89
|
-
if (
|
|
93
|
+
if (hasAnyAggregationField &&
|
|
94
|
+
(maxEnd > query.end || minStart < query.start)) {
|
|
90
95
|
await this.getFeaturesHelper({
|
|
91
96
|
query: {
|
|
92
97
|
...query,
|
|
@@ -112,7 +117,9 @@ class BigBedAdapter extends BaseAdapter_1.BaseFeatureDataAdapter {
|
|
|
112
117
|
`${feat.end}`,
|
|
113
118
|
...(((_a = feat.rest) === null || _a === void 0 ? void 0 : _a.split('\t')) || []),
|
|
114
119
|
];
|
|
115
|
-
const data = parser.parseLine(splitLine, {
|
|
120
|
+
const data = parser.parseLine(splitLine, {
|
|
121
|
+
uniqueId: feat.uniqueId,
|
|
122
|
+
});
|
|
116
123
|
const aggr = data[aggregateField];
|
|
117
124
|
if (!parentAggregation[aggr]) {
|
|
118
125
|
parentAggregation[aggr] = [];
|
|
@@ -122,9 +122,12 @@ class BedAdapter extends BaseFeatureDataAdapter {
|
|
|
122
122
|
return ObservableCreate(async (observer) => {
|
|
123
123
|
const { start, end, refName } = query;
|
|
124
124
|
const intervalTree = await this.loadFeatureIntervalTree(refName);
|
|
125
|
-
intervalTree === null || intervalTree === void 0 ? void 0 : intervalTree.search([start, end])
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
const features = intervalTree === null || intervalTree === void 0 ? void 0 : intervalTree.search([start, end]);
|
|
126
|
+
if (features) {
|
|
127
|
+
for (const f of features) {
|
|
128
|
+
observer.next(f);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
128
131
|
observer.complete();
|
|
129
132
|
}, opts.stopToken);
|
|
130
133
|
}
|
|
@@ -11,9 +11,33 @@ export default class BedTabixAdapter extends BaseFeatureDataAdapter {
|
|
|
11
11
|
protected columnNames: string[];
|
|
12
12
|
protected scoreColumn: string;
|
|
13
13
|
static capabilities: string[];
|
|
14
|
+
setupP?: Promise<{
|
|
15
|
+
meta: Awaited<ReturnType<TabixIndexedFile['getMetadata']>>;
|
|
16
|
+
}>;
|
|
14
17
|
constructor(config: AnyConfigurationModel, getSubAdapter?: getSubAdapterType, pluginManager?: PluginManager);
|
|
15
18
|
getRefNames(opts?: BaseOptions): Promise<string[]>;
|
|
16
|
-
getHeader(): Promise<string>;
|
|
19
|
+
getHeader(opts?: BaseOptions): Promise<string>;
|
|
20
|
+
getMetadataPre2(_opts?: BaseOptions): Promise<{
|
|
21
|
+
meta: Awaited<ReturnType<TabixIndexedFile["getMetadata"]>>;
|
|
22
|
+
}>;
|
|
23
|
+
getMetadataPre(): Promise<{
|
|
24
|
+
meta: {
|
|
25
|
+
[key: string]: any;
|
|
26
|
+
refNameToId: Record<string, number>;
|
|
27
|
+
refIdToName: string[];
|
|
28
|
+
metaChar: string | null;
|
|
29
|
+
columnNumbers: {
|
|
30
|
+
ref: number;
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
};
|
|
34
|
+
coordinateType: string;
|
|
35
|
+
format: string;
|
|
36
|
+
};
|
|
37
|
+
}>;
|
|
38
|
+
getMetadata(opts?: BaseOptions): Promise<{
|
|
39
|
+
meta: Awaited<ReturnType<TabixIndexedFile["getMetadata"]>>;
|
|
40
|
+
}>;
|
|
17
41
|
getNames(): Promise<string[] | undefined>;
|
|
18
42
|
getFeatures(query: Region, opts?: BaseOptions): import("rxjs").Observable<Feature>;
|
|
19
43
|
freeResources(): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import BED from '@gmod/bed';
|
|
2
2
|
import { TabixIndexedFile } from '@gmod/tabix';
|
|
3
3
|
import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
|
|
4
|
-
import { SimpleFeature } from '@jbrowse/core/util';
|
|
4
|
+
import { SimpleFeature, updateStatus } from '@jbrowse/core/util';
|
|
5
5
|
import { openLocation } from '@jbrowse/core/util/io';
|
|
6
6
|
import { ObservableCreate } from '@jbrowse/core/util/rxjs';
|
|
7
7
|
import { checkStopToken } from '@jbrowse/core/util/stopToken';
|
|
@@ -27,14 +27,31 @@ class BedTabixAdapter extends BaseFeatureDataAdapter {
|
|
|
27
27
|
async getRefNames(opts = {}) {
|
|
28
28
|
return this.bed.getReferenceSequenceNames(opts);
|
|
29
29
|
}
|
|
30
|
-
async getHeader() {
|
|
31
|
-
return this.bed.getHeader();
|
|
30
|
+
async getHeader(opts) {
|
|
31
|
+
return this.bed.getHeader(opts);
|
|
32
|
+
}
|
|
33
|
+
async getMetadataPre2(_opts) {
|
|
34
|
+
if (!this.setupP) {
|
|
35
|
+
this.setupP = this.getMetadataPre().catch((e) => {
|
|
36
|
+
this.setupP = undefined;
|
|
37
|
+
throw e;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return this.setupP;
|
|
41
|
+
}
|
|
42
|
+
async getMetadataPre() {
|
|
43
|
+
const meta = await this.bed.getMetadata();
|
|
44
|
+
return { meta };
|
|
45
|
+
}
|
|
46
|
+
async getMetadata(opts) {
|
|
47
|
+
const { statusCallback = () => { } } = opts || {};
|
|
48
|
+
return updateStatus('Downloading index', statusCallback, () => this.getMetadataPre2(opts));
|
|
32
49
|
}
|
|
33
50
|
async getNames() {
|
|
34
51
|
if (this.columnNames.length) {
|
|
35
52
|
return this.columnNames;
|
|
36
53
|
}
|
|
37
|
-
const header = await this.
|
|
54
|
+
const header = await this.getHeader();
|
|
38
55
|
const defs = header.split(/\n|\r\n|\r/).filter(f => !!f);
|
|
39
56
|
const defline = defs.at(-1);
|
|
40
57
|
return (defline === null || defline === void 0 ? void 0 : defline.includes('\t'))
|
|
@@ -44,10 +61,10 @@ class BedTabixAdapter extends BaseFeatureDataAdapter {
|
|
|
44
61
|
.map(f => f.trim())
|
|
45
62
|
: undefined;
|
|
46
63
|
}
|
|
47
|
-
getFeatures(query, opts
|
|
48
|
-
const { stopToken } = opts;
|
|
64
|
+
getFeatures(query, opts) {
|
|
65
|
+
const { stopToken, statusCallback = () => { } } = opts || {};
|
|
49
66
|
return ObservableCreate(async (observer) => {
|
|
50
|
-
const meta = await this.
|
|
67
|
+
const { meta } = await this.getMetadata();
|
|
51
68
|
const { columnNumbers } = meta;
|
|
52
69
|
const colRef = columnNumbers.ref - 1;
|
|
53
70
|
const colStart = columnNumbers.start - 1;
|
|
@@ -55,7 +72,7 @@ class BedTabixAdapter extends BaseFeatureDataAdapter {
|
|
|
55
72
|
const names = await this.getNames();
|
|
56
73
|
let start = performance.now();
|
|
57
74
|
checkStopToken(stopToken);
|
|
58
|
-
await this.bed.getLines(query.refName, query.start, query.end, {
|
|
75
|
+
await updateStatus('Downloading features', statusCallback, () => this.bed.getLines(query.refName, query.start, query.end, {
|
|
59
76
|
lineCallback: (line, fileOffset) => {
|
|
60
77
|
if (performance.now() - start > 200) {
|
|
61
78
|
checkStopToken(stopToken);
|
|
@@ -72,10 +89,10 @@ class BedTabixAdapter extends BaseFeatureDataAdapter {
|
|
|
72
89
|
names,
|
|
73
90
|
})));
|
|
74
91
|
},
|
|
75
|
-
stopToken
|
|
76
|
-
});
|
|
92
|
+
stopToken,
|
|
93
|
+
}));
|
|
77
94
|
observer.complete();
|
|
78
|
-
},
|
|
95
|
+
}, stopToken);
|
|
79
96
|
}
|
|
80
97
|
freeResources() { }
|
|
81
98
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BigBed } from '@gmod/bbi';
|
|
2
2
|
import BED from '@gmod/bed';
|
|
3
3
|
import { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
|
|
4
|
-
import { SimpleFeature, doesIntersect2, max, min } from '@jbrowse/core/util';
|
|
4
|
+
import { SimpleFeature, doesIntersect2, max, min, updateStatus, } from '@jbrowse/core/util';
|
|
5
5
|
import { openLocation } from '@jbrowse/core/util/io';
|
|
6
6
|
import { ObservableCreate } from '@jbrowse/core/util/rxjs';
|
|
7
7
|
import { firstValueFrom, toArray } from 'rxjs';
|
|
@@ -62,17 +62,18 @@ export default class BigBedAdapter extends BaseFeatureDataAdapter {
|
|
|
62
62
|
}
|
|
63
63
|
async getFeaturesHelper({ query, opts, observer, allowRedispatch, originalQuery = query, }) {
|
|
64
64
|
var _a;
|
|
65
|
-
const { stopToken } = opts;
|
|
65
|
+
const { stopToken, statusCallback = () => { } } = opts;
|
|
66
66
|
const scoreColumn = this.getConf('scoreColumn');
|
|
67
67
|
const aggregateField = this.getConf('aggregateField');
|
|
68
|
-
const { parser, bigbed } = await this.configure(opts);
|
|
69
|
-
const feats = await bigbed.getFeatures(query.refName, query.start, query.end, {
|
|
68
|
+
const { parser, bigbed } = await updateStatus('Downloading header', statusCallback, () => this.configure(opts));
|
|
69
|
+
const feats = await updateStatus('Downloading features', statusCallback, () => bigbed.getFeatures(query.refName, query.start, query.end, {
|
|
70
70
|
stopToken,
|
|
71
71
|
basesPerSpan: query.end - query.start,
|
|
72
|
-
});
|
|
72
|
+
}));
|
|
73
73
|
if (allowRedispatch && feats.length) {
|
|
74
74
|
let minStart = Number.POSITIVE_INFINITY;
|
|
75
75
|
let maxEnd = Number.NEGATIVE_INFINITY;
|
|
76
|
+
let hasAnyAggregationField = false;
|
|
76
77
|
for (const feat of feats) {
|
|
77
78
|
if (feat.start < minStart) {
|
|
78
79
|
minStart = feat.start;
|
|
@@ -80,8 +81,12 @@ export default class BigBedAdapter extends BaseFeatureDataAdapter {
|
|
|
80
81
|
if (feat.end > maxEnd) {
|
|
81
82
|
maxEnd = feat.end;
|
|
82
83
|
}
|
|
84
|
+
if (feat[aggregateField]) {
|
|
85
|
+
hasAnyAggregationField = true;
|
|
86
|
+
}
|
|
83
87
|
}
|
|
84
|
-
if (
|
|
88
|
+
if (hasAnyAggregationField &&
|
|
89
|
+
(maxEnd > query.end || minStart < query.start)) {
|
|
85
90
|
await this.getFeaturesHelper({
|
|
86
91
|
query: {
|
|
87
92
|
...query,
|
|
@@ -107,7 +112,9 @@ export default class BigBedAdapter extends BaseFeatureDataAdapter {
|
|
|
107
112
|
`${feat.end}`,
|
|
108
113
|
...(((_a = feat.rest) === null || _a === void 0 ? void 0 : _a.split('\t')) || []),
|
|
109
114
|
];
|
|
110
|
-
const data = parser.parseLine(splitLine, {
|
|
115
|
+
const data = parser.parseLine(splitLine, {
|
|
116
|
+
uniqueId: feat.uniqueId,
|
|
117
|
+
});
|
|
111
118
|
const aggr = data[aggregateField];
|
|
112
119
|
if (!parentAggregation[aggr]) {
|
|
113
120
|
parentAggregation[aggr] = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jbrowse/plugin-bed",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "JBrowse 2 bed adapters, tracks, etc.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jbrowse",
|
|
@@ -40,10 +40,8 @@
|
|
|
40
40
|
"@gmod/bbi": "^6.0.0",
|
|
41
41
|
"@gmod/bed": "^2.1.2",
|
|
42
42
|
"@gmod/bgzf-filehandle": "^2.0.1",
|
|
43
|
-
"@gmod/tabix": "^2.0.0"
|
|
44
|
-
|
|
45
|
-
"peerDependencies": {
|
|
46
|
-
"@jbrowse/core": "^2.0.0",
|
|
43
|
+
"@gmod/tabix": "^2.0.0",
|
|
44
|
+
"@jbrowse/core": "^3.0.2",
|
|
47
45
|
"mobx": "^6.0.0",
|
|
48
46
|
"mobx-react": "^9.0.0",
|
|
49
47
|
"mobx-state-tree": "^5.0.0",
|
|
@@ -55,5 +53,5 @@
|
|
|
55
53
|
"distModule": "esm/index.js",
|
|
56
54
|
"srcModule": "src/index.ts",
|
|
57
55
|
"module": "esm/index.js",
|
|
58
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "c01a35edcb2612e94661af8793f09c95c0b13c75"
|
|
59
57
|
}
|