Browser.tsx 11 KB
Newer Older
1
import React, { FunctionComponent, useRef, useEffect, useState } from 'react';
2
import { withRouter, RouteComponentProps } from 'react-router-dom';
3
import { connect } from 'react-redux';
4
import { replace, Replace } from 'connected-react-router';
5
6
import { useSpring, animated } from 'react-spring';
import { Link } from 'react-router-dom';
Andrey Azov's avatar
Andrey Azov committed
7
8
9
10
import find from 'lodash/find';
import isEqual from 'lodash/isEqual';
import upperFirst from 'lodash/upperFirst';

11
12
13
14
import BrowserBar from './browser-bar/BrowserBar';
import BrowserImage from './browser-image/BrowserImage';
import BrowserNavBar from './browser-nav/BrowserNavBar';
import TrackPanel from './track-panel/TrackPanel';
15
import AppBar from 'src/shared/app-bar/AppBar';
16

Andrey Azov's avatar
Andrey Azov committed
17
import { RootState } from 'src/store';
Andrey Azov's avatar
Andrey Azov committed
18
import { ChrLocation } from './browserState';
19
import {
20
  changeBrowserLocation,
Andrey Azov's avatar
Andrey Azov committed
21
22
  setDataFromUrlAndSave,
  ParsedUrlPayload
23
} from './browserActions';
24
import {
25
  getBrowserNavOpened,
26
  getChrLocation,
Dan Sheppard's avatar
Dan Sheppard committed
27
  getGenomeSelectorActive,
28
29
30
  getBrowserActivated,
  getBrowserActiveGenomeId,
  getBrowserQueryParams,
Andrey Azov's avatar
Andrey Azov committed
31
32
33
  getBrowserActiveEnsObjectId,
  getBrowserActiveEnsObjectIds,
  getAllChrLocations
34
} from './browserSelectors';
35
36
import { getLaunchbarExpanded } from 'src/header/headerSelectors';
import { getTrackPanelOpened } from './track-panel/trackPanelSelectors';
37
38
import { getChrLocationFromStr, getChrLocationStr } from './browserHelper';
import { getDrawerOpened } from './drawer/drawerSelectors';
39
40
41
import { getEnabledCommittedSpecies } from 'src/content/app/species-selector/state/speciesSelectorSelectors';
import { CommittedItem } from 'src/content/app/species-selector/types/species-search';
import { getExampleEnsObjects } from 'src/ens-object/ensObjectSelectors';
Andrey Azov's avatar
Andrey Azov committed
42
import { EnsObject } from 'src/ens-object/ensObjectTypes';
43

Andrey Azov's avatar
Andrey Azov committed
44
import { fetchGenomeData } from 'src/genome/genomeActions';
45
import { toggleDrawer } from './drawer/drawerActions';
46

47
48
49
50
51
52
import browserStorageService from './browser-storage-service';
import { TrackStates } from './track-panel/trackPanelConfig';
import { AppName } from 'src/global/globalConfig';

import * as urlFor from 'src/shared/helpers/urlHelper';

53
import styles from './Browser.scss';
54

55
56
57
import 'static/browser/browser.js';

type StateProps = {
Andrey Azov's avatar
Andrey Azov committed
58
59
60
  activeGenomeId: string | null;
  activeEnsObjectId: string | null;
  allActiveEnsObjectIds: { [genomeId: string]: string };
61
  browserActivated: boolean;
62
  browserNavOpened: boolean;
63
  browserQueryParams: { [key: string]: string };
Andrey Azov's avatar
Andrey Azov committed
64
65
  chrLocation: ChrLocation | null;
  allChrLocations: { [genomeId: string]: ChrLocation };
66
  drawerOpened: boolean;
67
  genomeSelectorActive: boolean;
68
69
  trackPanelOpened: boolean;
  launchbarExpanded: boolean;
Andrey Azov's avatar
Andrey Azov committed
70
  exampleEnsObjects: EnsObject[];
71
  committedSpecies: CommittedItem[];
72
73
74
};

type DispatchProps = {
Andrey Azov's avatar
Andrey Azov committed
75
  changeBrowserLocation: (genomeId: string, chrLocation: ChrLocation) => void;
Andrey Azov's avatar
Andrey Azov committed
76
  fetchGenomeData: (genomeId: string) => void;
77
  replace: Replace;
78
  toggleDrawer: (drawerOpened: boolean) => void;
Andrey Azov's avatar
Andrey Azov committed
79
  setDataFromUrlAndSave: (payload: ParsedUrlPayload) => void;
80
};
81

82
83
type OwnProps = {};

84
type MatchParams = {
85
  genomeId: string;
86
87
88
89
90
91
};

type BrowserProps = RouteComponentProps<MatchParams> &
  StateProps &
  DispatchProps &
  OwnProps;
92

93
94
95
export const Browser: FunctionComponent<BrowserProps> = (
  props: BrowserProps
) => {
96
  const browserRef: React.RefObject<HTMLDivElement> = useRef(null);
97
98
99
  const [trackStatesFromStorage, setTrackStatesFromStorage] = useState<
    TrackStates
  >({});
Andrey Azov's avatar
Andrey Azov committed
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
  const lastGenomeIdRef = useRef(props.activeGenomeId);

  const setDataFromUrl = () => {
    const { genomeId = null } = props.match.params;
    const { focus = null, location = null } = props.browserQueryParams;
    const chrLocation = location ? getChrLocationFromStr(location) : null;

    const lastGenomeId = lastGenomeIdRef.current;

    /*
      before committing url changes to redux:
      - make sure we don't already have these same values in redux store;
      - if we already have these values, it's possible that this is because
        the user is switching back to a previously viewed species;
        so check whether the genome id has changed from the previous render
        (that's the reason for lastGenomeIdRef here)

      TODO: after both genome browser and browser chrome are updated so that
      we do not update url location while moving or zooming the image; we can
      remove the genomeId === lastGenomeId check in the if-statement below
      and move dispatchBrowserLocation(genomeId, chrLocation) above the if-statement
    */
    if (
      !genomeId ||
      (genomeId === lastGenomeId &&
        genomeId === props.activeGenomeId &&
        focus === props.activeEnsObjectId &&
        isEqual(chrLocation, props.chrLocation))
    ) {
      return;
    }
131

Andrey Azov's avatar
Andrey Azov committed
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
    const payload = {
      activeGenomeId: genomeId,
      activeEnsObjectId: focus || null,
      chrLocation
    };

    props.setDataFromUrlAndSave(payload);

    chrLocation && dispatchBrowserLocation(genomeId, chrLocation);
    lastGenomeIdRef.current = genomeId;
  };

  const dispatchBrowserLocation = (
    genomeId: string,
    chrLocation: ChrLocation
  ) => {
Andrey Azov's avatar
Andrey Azov committed
148
    props.changeBrowserLocation(genomeId, chrLocation);
149
150
  };

151
  const changeSelectedSpecies = (genomeId: string) => {
Andrey Azov's avatar
Andrey Azov committed
152
153
154
155
156
157
158
159
160
161
162
    const { allChrLocations, allActiveEnsObjectIds } = props;
    const chrLocation = allChrLocations[genomeId];
    const activeEnsObjectId = allActiveEnsObjectIds[genomeId];

    const params = {
      genomeId,
      focus: activeEnsObjectId,
      location: chrLocation ? getChrLocationStr(chrLocation) : null
    };

    props.replace(urlFor.browser(params));
163
164
  };

Andrey Azov's avatar
Andrey Azov committed
165
  // handle url changes
166
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
167
    // handle navigation to /app/browser
168
    if (!props.match.params.genomeId) {
Andrey Azov's avatar
Andrey Azov committed
169
170
      // select either the species that the user viewed during the previous visit,
      // of the first selected species
171
172
      const { activeGenomeId, committedSpecies } = props;
      if (
Andrey Azov's avatar
Andrey Azov committed
173
        activeGenomeId &&
174
175
176
177
178
179
180
        find(
          committedSpecies,
          ({ genome_id }: CommittedItem) => genome_id === activeGenomeId
        )
      ) {
        changeSelectedSpecies(activeGenomeId);
      } else {
181
182
183
        if (committedSpecies[0]) {
          changeSelectedSpecies(committedSpecies[0].genome_id);
        }
184
      }
Andrey Azov's avatar
Andrey Azov committed
185
186
187
    } else {
      // handle navigation to /app/browser/:genomeId?focus=:focus&location=:location
      setDataFromUrl();
188
    }
Andrey Azov's avatar
Andrey Azov committed
189
  }, [props.match.params.genomeId, props.location.search]);
190

191
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
192
193
194
    const { activeGenomeId, fetchGenomeData } = props;
    activeGenomeId && fetchGenomeData(activeGenomeId);
  }, [props.activeGenomeId]);
195

196
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
197
198
    setTrackStatesFromStorage(browserStorageService.getTrackStates());
  }, [props.activeGenomeId, props.activeEnsObjectId]);
199

Dan Sheppard's avatar
Dan Sheppard committed
200
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
201
202
203
204
205
206
207
    const {
      match: {
        params: { genomeId }
      },
      browserQueryParams: { location }
    } = props;
    const chrLocation = location ? getChrLocationFromStr(location) : null;
208

Andrey Azov's avatar
Andrey Azov committed
209
210
    if (props.browserActivated && genomeId && chrLocation) {
      dispatchBrowserLocation(genomeId, chrLocation);
Dan Sheppard's avatar
Dan Sheppard committed
211
    }
212
  }, [props.browserActivated]);
Dan Sheppard's avatar
Dan Sheppard committed
213

214
  const closeTrack = () => {
215
    if (props.drawerOpened === false) {
216
217
      return;
    }
218
    props.toggleDrawer(false);
219
  };
220

221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
  const [trackAnimation, setTrackAnimation] = useSpring(() => ({
    config: { tension: 280, friction: 45 },
    height: '100%',
    width: 'calc(-36px + 100vw )'
  }));

  const getBrowserWidth = (): string => {
    if (props.drawerOpened) {
      return 'calc(41px + 0vw)';
    }
    return props.trackPanelOpened
      ? 'calc(-356px + 100vw)'
      : 'calc(-36px + 100vw)';
  };

  useEffect(() => {
    setTrackAnimation({
      width: getBrowserWidth()
    });
  }, [props.drawerOpened, props.trackPanelOpened]);

  const getHeightClass = (launchbarExpanded: boolean): string => {
    return launchbarExpanded ? styles.shorter : styles.taller;
  };

Andrey Azov's avatar
Andrey Azov committed
246
  return props.activeGenomeId ? (
247
248
249
250
251
252
253
    <>
      <AppBar
        currentAppName={AppName.GENOME_BROWSER}
        activeGenomeId={props.activeGenomeId}
        onTabSelect={changeSelectedSpecies}
      />

Andrey Azov's avatar
Andrey Azov committed
254
      {!props.browserQueryParams.focus && (
255
256
        <section className={styles.browser}>
          <BrowserBar dispatchBrowserLocation={dispatchBrowserLocation} />
Andrey Azov's avatar
Andrey Azov committed
257
          <ExampleObjectLinks {...props} />
258
259
        </section>
      )}
Andrey Azov's avatar
Andrey Azov committed
260
      {props.browserQueryParams.focus && (
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
        <section className={styles.browser}>
          <BrowserBar dispatchBrowserLocation={dispatchBrowserLocation} />
          {props.genomeSelectorActive && (
            <div className={styles.browserOverlay} />
          )}
          <div
            className={`${styles.browserInnerWrapper} ${getHeightClass(
              props.launchbarExpanded
            )}`}
          >
            <animated.div style={trackAnimation}>
              <div className={styles.browserImageWrapper} onClick={closeTrack}>
                {props.browserNavOpened &&
                !props.drawerOpened &&
                browserRef.current ? (
Andrey Azov's avatar
Andrey Azov committed
276
                  <BrowserNavBar />
277
278
279
280
281
282
283
                ) : null}
                <BrowserImage
                  browserRef={browserRef}
                  trackStates={trackStatesFromStorage}
                />
              </div>
            </animated.div>
Andrey Azov's avatar
Andrey Azov committed
284
            <TrackPanel />
285
          </div>
286
287
288
        </section>
      )}
    </>
Andrey Azov's avatar
Andrey Azov committed
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
  ) : null;
};

const ExampleObjectLinks = (props: BrowserProps) => {
  const { activeGenomeId } = props;
  if (!activeGenomeId) {
    return null;
  }
  const links = props.exampleEnsObjects.map((exampleObject: EnsObject) => {
    const location = `${exampleObject.location.chromosome}:${exampleObject.location.start}-${exampleObject.location.end}`;
    const path = urlFor.browser({
      genomeId: activeGenomeId,
      focus: exampleObject.ensembl_object_id,
      location
    });

    return (
      <div key={exampleObject.ensembl_object_id} className={styles.exampleLink}>
        <Link to={path}>
          <span className={styles.objectType}>
            {upperFirst(exampleObject.object_type)}
          </span>
          <span className={styles.objectLabel}>{exampleObject.label}</span>
        </Link>
      </div>
    );
  });

  return <div className={styles.exampleLinks}>{links}</div>;
318
};
319

320
const mapStateToProps = (state: RootState): StateProps => ({
321
322
  activeGenomeId: getBrowserActiveGenomeId(state),
  activeEnsObjectId: getBrowserActiveEnsObjectId(state),
Andrey Azov's avatar
Andrey Azov committed
323
  allActiveEnsObjectIds: getBrowserActiveEnsObjectIds(state),
324
  browserActivated: getBrowserActivated(state),
325
  browserNavOpened: getBrowserNavOpened(state),
326
  browserQueryParams: getBrowserQueryParams(state),
327
  chrLocation: getChrLocation(state),
Andrey Azov's avatar
Andrey Azov committed
328
  allChrLocations: getAllChrLocations(state),
329
  drawerOpened: getDrawerOpened(state),
330
331
  genomeSelectorActive: getGenomeSelectorActive(state),
  trackPanelOpened: getTrackPanelOpened(state),
332
333
334
  launchbarExpanded: getLaunchbarExpanded(state),
  exampleEnsObjects: getExampleEnsObjects(state),
  committedSpecies: getEnabledCommittedSpecies(state)
335
336
});

337
const mapDispatchToProps: DispatchProps = {
338
  changeBrowserLocation,
Andrey Azov's avatar
Andrey Azov committed
339
  fetchGenomeData,
340
  replace,
341
  toggleDrawer,
Andrey Azov's avatar
Andrey Azov committed
342
  setDataFromUrlAndSave
343
344
};

345
346
347
348
349
350
export default withRouter(
  connect(
    mapStateToProps,
    mapDispatchToProps
  )(Browser)
);