diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 5849087..8b0dd40 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -35,16 +35,21 @@ class Map extends React.Component { } componentWillReceiveProps(nextProps) { - if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) { - - // Fly to first of events selected - const eventPoint = (nextProps.app.selected.length > 0) ? nextProps.app.selected[0] : null; - - if (eventPoint !== null && eventPoint.latitude && eventPoint.longitude) { - this.map.setView([eventPoint.latitude, eventPoint.longitude]); - } + // Set appropriate zoom for narrative + if (hash(nextProps.app.mapBounds) !== hash(this.props.app.mapBounds) + && nextProps.app.mapBounds !== null) { + this.map.fitBounds(nextProps.app.mapBounds); + } else { + if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) { + // Fly to first of events selected + const eventPoint = (nextProps.app.selected.length > 0) ? nextProps.app.selected[0] : null; + + if (eventPoint !== null && eventPoint.latitude && eventPoint.longitude) { + this.map.setView([eventPoint.latitude, eventPoint.longitude]); + } + } } - } + } initializeMap() { /** @@ -246,6 +251,7 @@ function mapStateToProps(state) { selected: state.app.selected, highlighted: state.app.highlighted, mapAnchor: state.app.mapAnchor, + mapBounds: state.app.filters.mapBounds, narrative: state.app.narrative, flags: { isShowingSites: state.app.flags.isShowingSites diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index fc3f15b..535fa27 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -58,10 +58,21 @@ class Timeline extends React.Component { scaleY: this.makeScaleY(nextProps.domain.categories) }); } + + if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) { + if (!!nextProps.app.selected && nextProps.app.selected.length > 0) { + this.onCenterTime(parseDate(nextProps.app.selected[0].timestamp)); + } + } } addEventListeners() { window.addEventListener('resize', () => { this.computeDims(); }); + let element = document.querySelector('.timeline-wrapper'); + element.addEventListener("transitionend", (event) => { + this.computeDims(); + }, { once: true }); + } makeScaleX() { @@ -103,7 +114,8 @@ class Timeline extends React.Component { const boundingClient = document.querySelector(`#${dom}`).getBoundingClientRect(); this.setState({ - dims: Object.assign({}, this.state.dims, { width: boundingClient.width }) + dims: Object.assign({}, this.state.dims, { width: boundingClient.width }) + }, () => { this.setState({ scaleX: this.makeScaleX() }) }); } } @@ -132,6 +144,17 @@ class Timeline extends React.Component { }); } + onCenterTime(newCentralTime) { + const extent = this.getTimeScaleExtent(); + + const domain0 = d3.timeMinute.offset(newCentralTime, -extent/2); + const domainF = d3.timeMinute.offset(newCentralTime, +extent/2); + + this.setState({ timerange: [domain0, domainF] }, () => { + this.props.methods.onUpdateTimerange(this.state.timerange); + }); + } + /** * Change display of time range * WITHOUT updating the store, or data shown. diff --git a/src/components/presentational/TimelineEvents.js b/src/components/presentational/TimelineEvents.js index 035ab64..8c9678f 100644 --- a/src/components/presentational/TimelineEvents.js +++ b/src/components/presentational/TimelineEvents.js @@ -35,7 +35,15 @@ const TimelineEvents = ({ function renderDatetime(datetime) { if (narrative) { const { steps } = narrative - const isInNarrative = steps.map(s => s.id).includes(event.id) + // check all events in the datetime before rendering in narrative + let isInNarrative = false + for (let i = 0; i < datetime.events.length; i++) { + const event = datetime.events[i] + if (steps.map(s => s.id).includes(event.id)) { + isInNarrative = true + break; + } + } if (!isInNarrative) { return null diff --git a/src/reducers/app.js b/src/reducers/app.js index 8140d7a..a17ec9a 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -36,11 +36,61 @@ function updateSelected(appState, action) { } function updateNarrative(appState, action) { + let minTime = appState.filters.timerange[0]; + let maxTime = appState.filters.timerange[1]; + + let cornerBound0 = [180, 180]; + let cornerBound1 = [-180, -180]; + + // Compute narrative time range and map bounds + if (!!action.narrative) { + minTime = parseDate('2100-01-01T00:00:00'); + maxTime = parseDate('1900-01-01T00:00:00'); + + // Find max and mins coordinates of narrative events + action.narrative.steps.forEach(step => { + const stepTime = parseDate(step.timestamp); + if (stepTime < minTime) minTime = stepTime; + if (stepTime > maxTime) maxTime = stepTime; + + if (!!step.longitude && !!step.latitude) { + if (+step.longitude < cornerBound0[1]) cornerBound0[1] = +step.longitude; + if (+step.longitude > cornerBound1[1]) cornerBound1[1] = +step.longitude; + if (+step.latitude < cornerBound0[0]) cornerBound0[0] = +step.latitude; + if (+step.latitude > cornerBound1[0]) cornerBound1[0] = +step.latitude; + } + }); + // Adjust bounds to center around first event, while keeping visible all others + // Takes first event, finds max ditance with first attempt bounds, and use this max distance + // on the other side, both in latitude and longitude + const first = action.narrative.steps[0]; + if (!!first.longitude && !!first.latitude) { + const firstToLong0 = Math.abs(+first.longitude - cornerBound0[1]); + const firstToLong1 = Math.abs(+first.longitude - cornerBound1[1]); + const firstToLat0 = Math.abs(+first.latitude - cornerBound0[0]); + const firstToLat1 = Math.abs(+first.latitude - cornerBound1[0]); + + if (firstToLong0 > firstToLong1) cornerBound1[1] = +first.longitude + firstToLong0; + if (firstToLong0 < firstToLong1) cornerBound0[1] = +first.longitude - firstToLong1; + if (firstToLat0 > firstToLat1) cornerBound1[0] = +first.latitude + firstToLat0; + if (firstToLat0 < firstToLat1) cornerBound0[0] = +first.latitude - firstToLat1; + } + + // Add some buffer on both sides of the time extent + minTime = new Date(minTime.getTime() - Math.abs((maxTime - minTime) / 10)); + maxTime = new Date(maxTime.getTime() + Math.abs((maxTime - minTime) / 10)); + } + return { ...appState, narrative: action.narrative, narrativeState: { current: !!action.narrative ? 0 : null + }, + filters: { + ...appState.filters, + timerange: [minTime, maxTime], + mapBounds: (action.narrative) ? [cornerBound0, cornerBound1] : null } } } diff --git a/src/store/initial.js b/src/store/initial.js index ba29a5d..dbfbd19 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -41,6 +41,7 @@ const initial = { d3.timeParse("%Y-%m-%dT%H:%M:%S")("2013-02-23T12:00:00"), d3.timeParse("%Y-%m-%dT%H:%M:%S")("2016-02-23T12:00:00") ], + mapBounds: null, tags: [], categories: [], views: {