Browser.tsx 11.2 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';
Imran Salam's avatar
Imran Salam committed
18
import { ChrLocation, ChrLocations } 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
import { getLaunchbarExpanded } from 'src/header/headerSelectors';
Imran Salam's avatar
Imran Salam committed
36
import { getIsTrackPanelOpened } from './track-panel/trackPanelSelectors';
37
import { getChrLocationFromStr, getChrLocationStr } from './browserHelper';
Imran Salam's avatar
Imran Salam committed
38
import { getIsDrawerOpened } 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';
Imran Salam's avatar
Imran Salam committed
45
46
47
48
49
import {
  changeDrawerView,
  closeDrawer,
  toggleDrawer
} from './drawer/drawerActions';
50

51
52
53
54
55
56
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';

57
import styles from './Browser.scss';
58

59
60
61
import 'static/browser/browser.js';

type StateProps = {
Andrey Azov's avatar
Andrey Azov committed
62
63
64
  activeGenomeId: string | null;
  activeEnsObjectId: string | null;
  allActiveEnsObjectIds: { [genomeId: string]: string };
Imran Salam's avatar
Imran Salam committed
65
  allChrLocations: ChrLocations;
66
  browserActivated: boolean;
67
  browserNavOpened: boolean;
68
  browserQueryParams: { [key: string]: string };
Andrey Azov's avatar
Andrey Azov committed
69
  chrLocation: ChrLocation | null;
70
  genomeSelectorActive: boolean;
Imran Salam's avatar
Imran Salam committed
71
72
  isDrawerOpened: boolean;
  isTrackPanelOpened: boolean;
73
  launchbarExpanded: boolean;
Andrey Azov's avatar
Andrey Azov committed
74
  exampleEnsObjects: EnsObject[];
75
  committedSpecies: CommittedItem[];
76
77
78
};

type DispatchProps = {
Andrey Azov's avatar
Andrey Azov committed
79
  changeBrowserLocation: (genomeId: string, chrLocation: ChrLocation) => void;
Imran Salam's avatar
Imran Salam committed
80
81
  changeDrawerView: (drawerView: string) => void;
  closeDrawer: () => void;
Andrey Azov's avatar
Andrey Azov committed
82
  fetchGenomeData: (genomeId: string) => void;
83
  replace: Replace;
Imran Salam's avatar
Imran Salam committed
84
  toggleDrawer: (isDrawerOpened: boolean) => void;
Andrey Azov's avatar
Andrey Azov committed
85
  setDataFromUrlAndSave: (payload: ParsedUrlPayload) => void;
86
};
87

88
89
type OwnProps = {};

90
type MatchParams = {
91
  genomeId: string;
92
93
94
95
96
97
};

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

99
100
101
export const Browser: FunctionComponent<BrowserProps> = (
  props: BrowserProps
) => {
102
  const browserRef: React.RefObject<HTMLDivElement> = useRef(null);
103
104
105
  const [trackStatesFromStorage, setTrackStatesFromStorage] = useState<
    TrackStates
  >({});
Andrey Azov's avatar
Andrey Azov committed
106
107
  const lastGenomeIdRef = useRef(props.activeGenomeId);

Imran Salam's avatar
Imran Salam committed
108
109
  const { isDrawerOpened, closeDrawer } = props;

Andrey Azov's avatar
Andrey Azov committed
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
  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;
    }
139

Andrey Azov's avatar
Andrey Azov committed
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
    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
156
    props.changeBrowserLocation(genomeId, chrLocation);
157
158
  };

159
  const changeSelectedSpecies = (genomeId: string) => {
Andrey Azov's avatar
Andrey Azov committed
160
161
162
163
164
165
166
167
168
169
170
    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));
171
172
  };

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

199
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
200
201
202
    const { activeGenomeId, fetchGenomeData } = props;
    activeGenomeId && fetchGenomeData(activeGenomeId);
  }, [props.activeGenomeId]);
203

204
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
205
206
    setTrackStatesFromStorage(browserStorageService.getTrackStates());
  }, [props.activeGenomeId, props.activeEnsObjectId]);
207

Dan Sheppard's avatar
Dan Sheppard committed
208
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
209
210
211
212
213
214
215
    const {
      match: {
        params: { genomeId }
      },
      browserQueryParams: { location }
    } = props;
    const chrLocation = location ? getChrLocationFromStr(location) : null;
216

Andrey Azov's avatar
Andrey Azov committed
217
218
    if (props.browserActivated && genomeId && chrLocation) {
      dispatchBrowserLocation(genomeId, chrLocation);
Dan Sheppard's avatar
Dan Sheppard committed
219
    }
220
  }, [props.browserActivated]);
Dan Sheppard's avatar
Dan Sheppard committed
221

222
  const closeTrack = () => {
Imran Salam's avatar
Imran Salam committed
223
    if (!isDrawerOpened) {
224
225
      return;
    }
Imran Salam's avatar
Imran Salam committed
226
227

    closeDrawer();
228
  };
229

230
231
232
233
234
235
236
  const [trackAnimation, setTrackAnimation] = useSpring(() => ({
    config: { tension: 280, friction: 45 },
    height: '100%',
    width: 'calc(-36px + 100vw )'
  }));

  const getBrowserWidth = (): string => {
Imran Salam's avatar
Imran Salam committed
237
238
    if (isDrawerOpened) {
      return 'calc(41px + 0vw)'; // this format must be used for the react-spring animation to function properly
239
    }
Imran Salam's avatar
Imran Salam committed
240
    return props.isTrackPanelOpened
241
242
243
244
245
246
247
248
      ? 'calc(-356px + 100vw)'
      : 'calc(-36px + 100vw)';
  };

  useEffect(() => {
    setTrackAnimation({
      width: getBrowserWidth()
    });
Imran Salam's avatar
Imran Salam committed
249
  }, [isDrawerOpened, props.isTrackPanelOpened]);
250
251
252
253
254

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

Imran Salam's avatar
Imran Salam committed
255
256
257
258
  const BrowserBarNode = (
    <BrowserBar dispatchBrowserLocation={dispatchBrowserLocation} />
  );

Andrey Azov's avatar
Andrey Azov committed
259
  return props.activeGenomeId ? (
260
261
262
263
264
265
266
    <>
      <AppBar
        currentAppName={AppName.GENOME_BROWSER}
        activeGenomeId={props.activeGenomeId}
        onTabSelect={changeSelectedSpecies}
      />

Andrey Azov's avatar
Andrey Azov committed
267
      {!props.browserQueryParams.focus && (
268
        <section className={styles.browser}>
Imran Salam's avatar
Imran Salam committed
269
          {BrowserBarNode}
Andrey Azov's avatar
Andrey Azov committed
270
          <ExampleObjectLinks {...props} />
271
272
        </section>
      )}
Andrey Azov's avatar
Andrey Azov committed
273
      {props.browserQueryParams.focus && (
274
        <section className={styles.browser}>
Imran Salam's avatar
Imran Salam committed
275
          {BrowserBarNode}
276
277
278
279
280
281
282
283
284
285
286
          {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 &&
Imran Salam's avatar
Imran Salam committed
287
                !isDrawerOpened &&
288
                browserRef.current ? (
Andrey Azov's avatar
Andrey Azov committed
289
                  <BrowserNavBar />
290
291
292
293
294
295
296
                ) : null}
                <BrowserImage
                  browserRef={browserRef}
                  trackStates={trackStatesFromStorage}
                />
              </div>
            </animated.div>
Andrey Azov's avatar
Andrey Azov committed
297
            <TrackPanel />
298
          </div>
299
300
301
        </section>
      )}
    </>
Andrey Azov's avatar
Andrey Azov committed
302
303
304
305
306
307
308
309
310
311
312
313
  ) : 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,
314
      focus: exampleObject.object_id,
Andrey Azov's avatar
Andrey Azov committed
315
316
317
318
      location
    });

    return (
319
      <div key={exampleObject.object_id} className={styles.exampleLink}>
Andrey Azov's avatar
Andrey Azov committed
320
321
322
323
324
325
326
327
328
329
330
        <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>;
331
};
332

333
const mapStateToProps = (state: RootState): StateProps => ({
334
335
  activeGenomeId: getBrowserActiveGenomeId(state),
  activeEnsObjectId: getBrowserActiveEnsObjectId(state),
Andrey Azov's avatar
Andrey Azov committed
336
  allActiveEnsObjectIds: getBrowserActiveEnsObjectIds(state),
Imran Salam's avatar
Imran Salam committed
337
  allChrLocations: getAllChrLocations(state),
338
  browserActivated: getBrowserActivated(state),
339
  browserNavOpened: getBrowserNavOpened(state),
340
  browserQueryParams: getBrowserQueryParams(state),
341
  chrLocation: getChrLocation(state),
342
  genomeSelectorActive: getGenomeSelectorActive(state),
Imran Salam's avatar
Imran Salam committed
343
344
  isDrawerOpened: getIsDrawerOpened(state),
  isTrackPanelOpened: getIsTrackPanelOpened(state),
345
346
347
  launchbarExpanded: getLaunchbarExpanded(state),
  exampleEnsObjects: getExampleEnsObjects(state),
  committedSpecies: getEnabledCommittedSpecies(state)
348
349
});

350
const mapDispatchToProps: DispatchProps = {
351
  changeBrowserLocation,
Imran Salam's avatar
Imran Salam committed
352
353
  changeDrawerView,
  closeDrawer,
Andrey Azov's avatar
Andrey Azov committed
354
  fetchGenomeData,
355
  replace,
356
  toggleDrawer,
Andrey Azov's avatar
Andrey Azov committed
357
  setDataFromUrlAndSave
358
359
};

360
361
362
363
364
365
export default withRouter(
  connect(
    mapStateToProps,
    mapDispatchToProps
  )(Browser)
);