Browser.tsx 11.4 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/components/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
import analyticsTracking from 'src/services/analytics-service';
44

Andrey Azov's avatar
Andrey Azov committed
45
import { fetchGenomeData } from 'src/genome/genomeActions';
Imran Salam's avatar
Imran Salam committed
46
47
48
49
50
import {
  changeDrawerView,
  closeDrawer,
  toggleDrawer
} from './drawer/drawerActions';
51

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

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

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

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

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

89
90
type OwnProps = {};

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

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

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

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

Andrey Azov's avatar
Andrey Azov committed
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
139
  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;
    }
140

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

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

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

200
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
201
    const { activeGenomeId, fetchGenomeData } = props;
202
203
204
205
206
    if (!activeGenomeId) {
      return;
    }
    fetchGenomeData(activeGenomeId);
    analyticsTracking.setSpeciesDimension(activeGenomeId);
Andrey Azov's avatar
Andrey Azov committed
207
  }, [props.activeGenomeId]);
208

209
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
210
211
    setTrackStatesFromStorage(browserStorageService.getTrackStates());
  }, [props.activeGenomeId, props.activeEnsObjectId]);
212

Dan Sheppard's avatar
Dan Sheppard committed
213
  useEffect(() => {
Andrey Azov's avatar
Andrey Azov committed
214
215
216
217
218
219
220
    const {
      match: {
        params: { genomeId }
      },
      browserQueryParams: { location }
    } = props;
    const chrLocation = location ? getChrLocationFromStr(location) : null;
221

Andrey Azov's avatar
Andrey Azov committed
222
223
    if (props.browserActivated && genomeId && chrLocation) {
      dispatchBrowserLocation(genomeId, chrLocation);
Dan Sheppard's avatar
Dan Sheppard committed
224
    }
225
  }, [props.browserActivated]);
Dan Sheppard's avatar
Dan Sheppard committed
226

227
  const closeTrack = () => {
Imran Salam's avatar
Imran Salam committed
228
    if (!isDrawerOpened) {
229
230
      return;
    }
Imran Salam's avatar
Imran Salam committed
231
232

    closeDrawer();
233
  };
234

235
236
237
238
239
240
241
  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
242
243
    if (isDrawerOpened) {
      return 'calc(41px + 0vw)'; // this format must be used for the react-spring animation to function properly
244
    }
Imran Salam's avatar
Imran Salam committed
245
    return props.isTrackPanelOpened
246
247
248
249
250
251
252
253
      ? 'calc(-356px + 100vw)'
      : 'calc(-36px + 100vw)';
  };

  useEffect(() => {
    setTrackAnimation({
      width: getBrowserWidth()
    });
Imran Salam's avatar
Imran Salam committed
254
  }, [isDrawerOpened, props.isTrackPanelOpened]);
255
256
257
258
259

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

Imran Salam's avatar
Imran Salam committed
260
261
262
263
  const BrowserBarNode = (
    <BrowserBar dispatchBrowserLocation={dispatchBrowserLocation} />
  );

Andrey Azov's avatar
Andrey Azov committed
264
  return props.activeGenomeId ? (
265
266
267
268
269
270
271
    <>
      <AppBar
        currentAppName={AppName.GENOME_BROWSER}
        activeGenomeId={props.activeGenomeId}
        onTabSelect={changeSelectedSpecies}
      />

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

    return (
324
      <div key={exampleObject.object_id} className={styles.exampleLink}>
Andrey Azov's avatar
Andrey Azov committed
325
326
327
328
329
330
331
332
333
334
335
        <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>;
336
};
337

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

355
const mapDispatchToProps: DispatchProps = {
356
  changeBrowserLocation,
Imran Salam's avatar
Imran Salam committed
357
358
  changeDrawerView,
  closeDrawer,
Andrey Azov's avatar
Andrey Azov committed
359
  fetchGenomeData,
360
  replace,
361
  toggleDrawer,
Andrey Azov's avatar
Andrey Azov committed
362
  setDataFromUrlAndSave
363
364
};

365
366
367
368
369
370
export default withRouter(
  connect(
    mapStateToProps,
    mapDispatchToProps
  )(Browser)
);