Skip to content

Commit

Permalink
Merge cabe15c into 7cc3016
Browse files Browse the repository at this point in the history
  • Loading branch information
akmorrow13 committed Feb 16, 2021
2 parents 7cc3016 + cabe15c commit 2e557d3
Show file tree
Hide file tree
Showing 16 changed files with 358 additions and 83 deletions.
1 change: 1 addition & 0 deletions lib/underscore.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

declare module "underscore" {
declare function find<T>(list: T[], predicate: (val: T)=>boolean): ?T;
declare function findIndex<T>(list: T[], predicate: (val: T)=>boolean): number;
declare function findWhere<T>(list: Array<T>, properties: {[key:string]: any}): ?T;
declare function clone<T>(obj: T): T;

Expand Down
26 changes: 21 additions & 5 deletions src/main/Controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
'use strict';

import type {GenomeRange, PartialGenomeRange} from './types';
import type ContigInterval from './ContigInterval';

import React from 'react';
import _ from 'underscore';
Expand All @@ -14,7 +15,7 @@ import Interval from './Interval';

type Props = {
range: ?GenomeRange;
contigList: string[];
contigList: ContigInterval<string>[];
onChange: (newRange: GenomeRange)=>void;
};

Expand Down Expand Up @@ -54,8 +55,8 @@ class Controls extends React.Component<Props, State> {
// There are major performance issues with having a 'chr' mismatch in the
// global location object.
const contig = range.contig;
var altContig = _.find(this.props.contigList, ref => utils.isChrMatch(contig, ref));
if (altContig) range.contig = altContig;
var altContig = _.find(this.props.contigList, ref => utils.isChrMatch(contig, ref.contig));
if (altContig) range.contig = altContig.contig;
}

return (_.extend(_.clone(this.props.range), range) : any);
Expand Down Expand Up @@ -86,7 +87,8 @@ class Controls extends React.Component<Props, State> {
this.refs.position.value = utils.formatInterval(new Interval(r.start, r.stop));

if (this.props.contigList) {
var contigIdx = this.props.contigList.indexOf(r.contig);
var contigIdx = _.findIndex(this.props.contigList, ref => utils.isChrMatch(r.contig, ref.contig));

this.refs.contig.selectedIndex = contigIdx;
}
}
Expand Down Expand Up @@ -137,7 +139,7 @@ class Controls extends React.Component<Props, State> {

render(): any {
var contigOptions = this.props.contigList
? this.props.contigList.map((contig, i) => <option key={i}>{contig}</option>)
? this.props.contigList.map((contig, i) => <option key={i}>{contig.contig}</option>)
: null;

// Note: input values are set in componentDidUpdate.
Expand All @@ -161,6 +163,20 @@ class Controls extends React.Component<Props, State> {
if (!_.isEqual(prevProps.range, this.props.range)) {
this.updateRangeUI();
}
// Update slider if we have collected new information about a contig.
if (!_.isEqual(prevProps.contigList, this.props.contigList)) {
if (this.props.range != undefined) {
var range = this.props.range; // flow
if (range.contig != undefined) {
var newInterval = _.find(this.props.contigList, ref => utils.isChrMatch(range.contig, ref.contig));

if (newInterval != undefined) {
this.refs.slider.min = Math.ceil(-Math.log2(newInterval.stop()) + 1);
this.updateSlider(new Interval(range.start, range.stop));
}
}
}
}
}

componentDidMount() {
Expand Down
86 changes: 76 additions & 10 deletions src/main/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import type {GenomeRange} from './types';
import type {TwoBitSource} from './sources/TwoBitDataSource';
import type {VisualizedTrack, VizWithOptions} from './types';

import type ContigInterval from './ContigInterval';
import utils from './utils';
import _ from 'underscore';

import React from 'react';
Expand All @@ -22,7 +23,7 @@ type Props = {
};

type State = {
contigList: string[];
contigList: ContigInterval<string>[];
range: ?GenomeRange;
settingsMenuKey: ?string;
updateSize: boolean;
Expand Down Expand Up @@ -50,9 +51,7 @@ class Root extends React.Component<Props, State> {

componentDidMount() {
this.props.referenceSource.on('contigs', () => {
this.setState({
contigList: this.props.referenceSource.contigList(),
});
this.updateOutOfBoundsChromosome();
});

if (!this.state.range) {
Expand All @@ -63,13 +62,33 @@ class Root extends React.Component<Props, State> {
}

handleRangeChange(newRange: GenomeRange) {

// copy over range so you don't modify
// this.state.range, which is bound to handleRangeChange
var modifiedRange = {
contig: newRange.contig,
start: newRange.start,
stop: newRange.stop,
};

// Do not propagate negative ranges
if (newRange.start < 0) {
newRange.start = 0;
if (modifiedRange.start < 0) {
modifiedRange.start = 0;
}
// Do not propogate ranges exceeding contig limit
var contigInfo = _.find(this.state.contigList, ref => utils.isChrMatch(modifiedRange.contig, ref.contig));

if (contigInfo != undefined) {
if (modifiedRange.stop > contigInfo.stop()) {
modifiedRange.stop = contigInfo.stop();
if (modifiedRange.start > modifiedRange.stop) {
modifiedRange.start = 0;
}
}
}
this.props.referenceSource.normalizeRange(newRange).then(range => {
this.setState({range: range});

this.props.referenceSource.normalizeRange(modifiedRange).then(range => {
this.setState({range: range});
// Inform all the sources of the range change (including referenceSource).
this.props.tracks.forEach(track => {
track.source.rangeChanged(range);
Expand Down Expand Up @@ -160,7 +179,7 @@ class Root extends React.Component<Props, State> {
</div>
);
}

var className = ['track', track.visualization.component.displayName || '', track.track.cssClass || ''].join(' ');

return (
Expand Down Expand Up @@ -198,13 +217,60 @@ class Root extends React.Component<Props, State> {
);
}

updateOutOfBoundsChromosome(): any {
// We don't want to allow users to go to regions that extend past the end of
// a contig. This function truncates queries past the ends of a contig
// and updates the required states.

var current_contig = this.props.initialRange.contig;
if (this.state.range) {
current_contig = this.state.range.contig;
}

var oldContig = _.find(this.state.contigList, ref =>
utils.isChrMatch(current_contig,
ref.contig));

var contigList = this.props.referenceSource.contigList();

var newContig = _.find(contigList, ref => utils.isChrMatch(current_contig, ref.contig));

// only update if the current contig has new information regarding
// the end of the chromosome AND the current range is out of bounds
// with respect to chromosome length
if (this.state.contigList.length == 0) {
this.setState({
contigList: contigList
});
}

if (newContig && oldContig) {
if (!_.isEqual(oldContig, newContig)) {
// only trigger state if current contig changed
this.setState({
contigList: contigList
});
if (this.state.range !== null && this.state.range !== undefined) {
if (this.state.range.stop > newContig.stop()) {
// $FlowIgnore: TODO remove flow suppression
this.handleRangeChange(this.state.range);
}
}
}
}
}

componentDidUpdate(prevProps: Props, prevState: Object) {
if (this.state.updateSize) {
for (var i=0;i<this.props.tracks.length;i++) {
this.trackReactElements[i].setState({updateSize:this.state.updateSize});
}
this.state.updateSize=false;
}

this.props.referenceSource.on('contigs', () => {
this.updateOutOfBoundsChromosome();
});
}

}
Expand Down
50 changes: 37 additions & 13 deletions src/main/data/TwoBit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
'use strict';

import type RemoteFile from '../RemoteFile';
import ContigInterval from '../ContigInterval';

import Q from 'q';
import _ from 'underscore';
Expand Down Expand Up @@ -206,9 +207,14 @@ function retryRemoteGet(remoteFile: RemoteFile, start: number, size: number, unt
class TwoBit {
remoteFile: RemoteFile;
header: Q.Promise<TwoBitHeader>;
// Stores sequence records already seen.
// Used to keep track of lengths of each contig.
traversedSequenceRecords: {[key:string]: SequenceRecord};


constructor(remoteFile: RemoteFile) {
this.remoteFile = remoteFile;
this.traversedSequenceRecords = {};
var deferredHeader = Q.defer();
this.header = deferredHeader.promise;
retryRemoteGet(
Expand Down Expand Up @@ -250,8 +256,17 @@ class TwoBit {
}

// Returns a list of contig names.
getContigList(): Q.Promise<string[]> {
return this.header.then(header => header.sequences.map(seq => seq.name));
getContigList(): Q.Promise<ContigInterval<string>[]> {
return this.header.then(header => {
return header.sequences.map(seq => {
// fill in numBases if collected
var numBases = Number.MAX_VALUE;
if (this.traversedSequenceRecords[seq.name]) {
numBases = this.traversedSequenceRecords[seq.name].numBases;
}
return new ContigInterval(seq.name, 0, numBases);
});
});
}

_getSequenceHeader(contig: string): Q.Promise<SequenceRecord> {
Expand All @@ -263,17 +278,26 @@ class TwoBit {
}
var seq = maybeSeq; // for flow, see facebook/flow#266

return retryRemoteGet(
this.remoteFile,
seq.offset,
FIRST_SEQUENCE_CHUNKSIZE,
MAX_CHUNKSIZE,
buffer => {
return parseWithException(() => {
return parseSequenceRecord(buffer, seq.offset);
});
}
);
if (contig in this.traversedSequenceRecords) {
return Q.when(this.traversedSequenceRecords[contig]);
} else {
return retryRemoteGet(
this.remoteFile,
seq.offset,
FIRST_SEQUENCE_CHUNKSIZE,
MAX_CHUNKSIZE,
buffer => {
return parseWithException(() => {

if (!this.traversedSequenceRecords[seq.name]) {
this.traversedSequenceRecords[contig] =
parseSequenceRecord(buffer, seq.offset);
}
return this.traversedSequenceRecords[contig];
});
}
);
}
}).then(p => {
return p;
});
Expand Down
27 changes: 20 additions & 7 deletions src/main/sources/TwoBitDataSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export type TwoBitSource = {
rangeChanged: (newRange: GenomeRange) => void;
getRange: (range: GenomeRange) => {[key:string]: ?string};
getRangeAsString: (range: GenomeRange) => string;
contigList: () => string[];
contigList: () => ContigInterval<string>[];
normalizeRange: (range: GenomeRange) => Q.Promise<GenomeRange>;
on: (event: string, handler: Function) => void;
once: (event: string, handler: Function) => void;
Expand All @@ -62,9 +62,13 @@ var createFromTwoBitFile = function(remoteSource: TwoBit): TwoBitSource {
function fetch(range: ContigInterval<string>) {
var span = range.length();
if (span > MAX_BASE_PAIRS_TO_FETCH) {
//inform that we won't fetch the data
o.trigger('newdatarefused', range);
return Q.when(); // empty promise
// trigger header collection to get information about this chromosome,
// but don't fetch the actual data.
return remoteSource._getSequenceHeader(range.contig).then(header => remoteSource.getContigList()).then(c => {
contigList = c;
o.trigger('contigs', contigList);
o.trigger('newdatarefused', range);
}).done();
}
//now we can add region to covered regions
//doing it earlier would provide inconsistency
Expand All @@ -81,18 +85,27 @@ var createFromTwoBitFile = function(remoteSource: TwoBit): TwoBitSource {
range.start() + letters.length - 1);
}
store.setRange(range, letters);
}).then(() => {
}).then(() => remoteSource.getContigList()).then(c => {
contigList = c;
o.trigger('contigs', contigList);
o.trigger('newdata', range);
}).done();
}

// This either adds or removes a 'chr' as needed.
function normalizeRangeSync(range: GenomeRange): GenomeRange {
if (contigList.indexOf(range.contig) >= 0) {

// check for direct match
var contigIdx = _.findIndex(contigList, ref => range.contig == ref.contig);

if (contigIdx >= 0) {
return range;
}
var altContig = utils.altContigName(range.contig);
if (contigList.indexOf(altContig) >= 0) {

contigIdx = _.findIndex(contigList, ref => altContig == ref.contig);

if (contigIdx >= 0) {
return {
contig: altContig,
start: range.start,
Expand Down
2 changes: 1 addition & 1 deletion src/test/FakeAlignment.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ var fakeSource = {
rangeChanged: dieFn,
getRange: function(): any { return {}; },
getRangeAsString: function(): string { return ''; },
contigList: function(): string[] { return []; },
contigList: function(): ContigInterval<string>[] { return []; },
normalizeRange: function(range: GenomeRange): Q.Promise<GenomeRange> { return Q.when(range); },
on: dieFn,
off: dieFn,
Expand Down
32 changes: 32 additions & 0 deletions src/test/FakeTwoBit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* fake TwoBit file.
* Used to query regions that extend the bounds in
* the test twobit files.
*
* @flow
*/
import Q from 'q';
import TwoBit from '../main/data/TwoBit';
import RemoteFile from '../main/RemoteFile';


class FakeTwoBit extends TwoBit {
deferred: Object;

constructor(remoteFile: RemoteFile) {
super(remoteFile);
this.deferred = Q.defer();
}

getFeaturesInRange(contig: string, start: number, stop: number): Q.Promise<string> {
return this.deferred.promise;
}

release(sequence: string) {
this.deferred.resolve(sequence);
}
}

module.exports = {
FakeTwoBit
};

0 comments on commit 2e557d3

Please sign in to comment.