mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 21:38:35 +03:00
refactor timeline config to redux state
This commit is contained in:
@@ -1,27 +1,31 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { Portal } from 'react-portal';
|
import { Portal } from 'react-portal'
|
||||||
|
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import * as selectors from '../selectors'
|
import * as selectors from '../selectors'
|
||||||
|
|
||||||
import hash from 'object-hash';
|
import hash from 'object-hash'
|
||||||
import 'leaflet';
|
import 'leaflet'
|
||||||
|
|
||||||
import { isNotNullNorUndefined } from '../js/utilities';
|
import { isNotNullNorUndefined } from '../js/utilities'
|
||||||
|
|
||||||
import Sites from './presentational/Map/Sites.jsx';
|
import Sites from './presentational/Map/Sites.jsx'
|
||||||
import Shapes from './presentational/Map/Shapes.jsx';
|
import Shapes from './presentational/Map/Shapes.jsx'
|
||||||
import Events from './presentational/Map/Events.jsx';
|
import Events from './presentational/Map/Events.jsx'
|
||||||
import SelectedEvents from './presentational/Map/SelectedEvents.jsx';
|
import SelectedEvents from './presentational/Map/SelectedEvents.jsx'
|
||||||
import Narratives from './presentational/Map/Narratives.jsx';
|
import Narratives from './presentational/Map/Narratives.jsx'
|
||||||
import DefsMarkers from './presentational/Map/DefsMarkers.jsx';
|
import DefsMarkers from './presentational/Map/DefsMarkers.jsx'
|
||||||
|
|
||||||
|
// NB: important constants for map, TODO: make statics
|
||||||
|
const supportedMapboxMap = ['streets', 'satellite']
|
||||||
|
const defaultToken = 'your_token'
|
||||||
|
|
||||||
class Map extends React.Component {
|
class Map extends React.Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super()
|
||||||
this.projectPoint = this.projectPoint.bind(this)
|
this.projectPoint = this.projectPoint.bind(this)
|
||||||
this.svgRef = React.createRef();
|
this.svgRef = React.createRef()
|
||||||
this.map = null;
|
this.map = null
|
||||||
this.state = {
|
this.state = {
|
||||||
mapTransformX: 0,
|
mapTransformX: 0,
|
||||||
mapTransformY: 0
|
mapTransformY: 0
|
||||||
@@ -31,22 +35,23 @@ class Map extends React.Component {
|
|||||||
|
|
||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
if (this.map === null) {
|
if (this.map === null) {
|
||||||
this.initializeMap();
|
this.initializeMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
// Set appropriate zoom for narrative
|
// Set appropriate zoom for narrative
|
||||||
if (hash(nextProps.app.mapBounds) !== hash(this.props.app.mapBounds)
|
const { bounds } = nextProps.app.map
|
||||||
&& nextProps.app.mapBounds !== null) {
|
if (hash(bounds) !== hash(this.props.app.map.bounds)
|
||||||
this.map.fitBounds(nextProps.app.mapBounds);
|
&& bounds !== null) {
|
||||||
|
this.map.fitBounds(bounds)
|
||||||
} else {
|
} else {
|
||||||
if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) {
|
if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) {
|
||||||
// Fly to first of events selected
|
// Fly to first of events selected
|
||||||
const eventPoint = (nextProps.app.selected.length > 0) ? nextProps.app.selected[0] : null;
|
const eventPoint = (nextProps.app.selected.length > 0) ? nextProps.app.selected[0] : null
|
||||||
|
|
||||||
if (eventPoint !== null && eventPoint.latitude && eventPoint.longitude) {
|
if (eventPoint !== null && eventPoint.latitude && eventPoint.longitude) {
|
||||||
this.map.setView([eventPoint.latitude, eventPoint.longitude]);
|
this.map.setView([eventPoint.latitude, eventPoint.longitude])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,51 +61,50 @@ class Map extends React.Component {
|
|||||||
/**
|
/**
|
||||||
* Creates a Leaflet map and a tilelayer for the map background
|
* Creates a Leaflet map and a tilelayer for the map background
|
||||||
*/
|
*/
|
||||||
|
const { map: mapConf } = this.props.app
|
||||||
const map =
|
const map =
|
||||||
L.map(this.props.ui.dom.map)
|
L.map(this.props.ui.dom.map)
|
||||||
.setView(this.props.app.mapAnchor, 14)
|
.setView(mapConf.anchor, mapConf.startZoom)
|
||||||
.setMinZoom(7)
|
.setMinZoom(mapConf.minZoom)
|
||||||
.setMaxZoom(18)
|
.setMaxZoom(mapConf.maxZoom)
|
||||||
.setMaxBounds([[180, -180], [-180, 180]])
|
.setMaxBounds(mapConf.maxBounds)
|
||||||
|
|
||||||
let s;
|
let s
|
||||||
if (process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== 'your_token') {
|
|
||||||
|
if ((supportedMapboxMap.indexOf(this.props.ui.tiles) !== -1) && process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== defaultToken) {
|
||||||
s = L.tileLayer(
|
s = L.tileLayer(
|
||||||
`http://a.tiles.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.png?access_token=${process.env.MAPBOX_TOKEN}`
|
`http://a.tiles.mapbox.com/v4/mapbox.${this.props.ui.tiles}/{z}/{x}/{y}@2x.png?access_token=${process.env.MAPBOX_TOKEN}`
|
||||||
);
|
)
|
||||||
|
} else if (process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== defaultToken) {
|
||||||
|
s = L.tileLayer(
|
||||||
|
`http://a.tiles.mapbox.com/v4/${this.props.ui.tiles}/{z}/{x}/{y}@2x.png?access_token=${process.env.MAPBOX_TOKEN}`
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line
|
s = L.tileLayer(
|
||||||
alert(`No mapbox token specified in config.
|
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
||||||
Timemap does not currently support any other tiling layer,
|
)
|
||||||
so you will need to sign up for one at:
|
|
||||||
|
|
||||||
https://www.mapbox.com/
|
|
||||||
|
|
||||||
Stop and start the development process in terminal after you have added your token to config.js`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
s = s.addTo(map);
|
s = s.addTo(map)
|
||||||
|
|
||||||
map.keyboard.disable();
|
map.keyboard.disable()
|
||||||
|
|
||||||
map.on('move zoomend viewreset moveend', () => this.alignLayers());
|
map.on('move zoomend viewreset moveend', () => this.alignLayers())
|
||||||
map.on('zoomstart', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.add('hide') });
|
map.on('zoomstart', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.add('hide') })
|
||||||
map.on('zoomend', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.remove('hide'); });
|
map.on('zoomend', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.remove('hide') })
|
||||||
window.addEventListener('resize', () => { this.alignLayers(); });
|
window.addEventListener('resize', () => { this.alignLayers() })
|
||||||
|
|
||||||
this.map = map;
|
this.map = map
|
||||||
}
|
}
|
||||||
|
|
||||||
alignLayers() {
|
alignLayers() {
|
||||||
const mapNode = document.querySelector('.leaflet-map-pane');
|
const mapNode = document.querySelector('.leaflet-map-pane')
|
||||||
if (mapNode === null) return { transformX: 0, transformY: 0 };
|
if (mapNode === null) return { transformX: 0, transformY: 0 }
|
||||||
|
|
||||||
// We'll get the transform of the leaflet container,
|
// We'll get the transform of the leaflet container,
|
||||||
// which will let us offset the SVG by the same quantity
|
// which will let us offset the SVG by the same quantity
|
||||||
const transform = window
|
const transform = window
|
||||||
.getComputedStyle(mapNode)
|
.getComputedStyle(mapNode)
|
||||||
.getPropertyValue('transform');
|
.getPropertyValue('transform')
|
||||||
|
|
||||||
// Offset with leaflet map transform boundaries
|
// Offset with leaflet map transform boundaries
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -118,7 +122,7 @@ class Map extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getClientDims() {
|
getClientDims() {
|
||||||
const boundingClient = document.querySelector(`#${this.props.ui.dom.map}`).getBoundingClientRect();
|
const boundingClient = document.querySelector(`#${this.props.ui.dom.map}`).getBoundingClientRect()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: boundingClient.width,
|
width: boundingClient.width,
|
||||||
@@ -127,8 +131,8 @@ class Map extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTiles() {
|
renderTiles() {
|
||||||
const pane = this.map.getPanes().overlayPane;
|
const pane = this.map.getPanes().overlayPane
|
||||||
const { width, height } = this.getClientDims();
|
const { width, height } = this.getClientDims()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal node={pane}>
|
<Portal node={pane}>
|
||||||
@@ -141,7 +145,7 @@ class Map extends React.Component {
|
|||||||
>
|
>
|
||||||
</svg>
|
</svg>
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSites() {
|
renderSites() {
|
||||||
@@ -151,7 +155,7 @@ class Map extends React.Component {
|
|||||||
projectPoint={this.projectPoint}
|
projectPoint={this.projectPoint}
|
||||||
isEnabled={this.props.app.views.sites}
|
isEnabled={this.props.app.views.sites}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderShapes() {
|
renderShapes() {
|
||||||
@@ -176,7 +180,7 @@ class Map extends React.Component {
|
|||||||
onSelect={this.props.methods.onSelect}
|
onSelect={this.props.methods.onSelect}
|
||||||
onSelectNarrative={this.props.methods.onSelectNarrative}
|
onSelectNarrative={this.props.methods.onSelectNarrative}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -209,7 +213,7 @@ class Map extends React.Component {
|
|||||||
onSelectNarrative={this.props.methods.onSelectNarrative}
|
onSelectNarrative={this.props.methods.onSelectNarrative}
|
||||||
getCategoryColor={this.props.methods.getCategoryColor}
|
getCategoryColor={this.props.methods.getCategoryColor}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSelected() {
|
renderSelected() {
|
||||||
@@ -219,7 +223,7 @@ class Map extends React.Component {
|
|||||||
selected={this.props.app.selected}
|
selected={this.props.app.selected}
|
||||||
projectPoint={this.projectPoint}
|
projectPoint={this.projectPoint}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -234,7 +238,7 @@ class Map extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isShowingSites } = this.props.app.flags
|
const { isShowingSites } = this.props.app.flags
|
||||||
const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper';
|
const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper'
|
||||||
const innerMap = !!this.map ? (
|
const innerMap = !!this.map ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{this.renderTiles()}
|
{this.renderTiles()}
|
||||||
@@ -252,7 +256,7 @@ class Map extends React.Component {
|
|||||||
<div id={this.props.ui.dom.map} />
|
<div id={this.props.ui.dom.map} />
|
||||||
{innerMap}
|
{innerMap}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,14 +273,14 @@ function mapStateToProps(state) {
|
|||||||
views: state.app.filters.views,
|
views: state.app.filters.views,
|
||||||
selected: state.app.selected,
|
selected: state.app.selected,
|
||||||
highlighted: state.app.highlighted,
|
highlighted: state.app.highlighted,
|
||||||
mapAnchor: state.app.mapAnchor,
|
map: state.app.map,
|
||||||
mapBounds: state.app.filters.mapBounds,
|
|
||||||
narrative: state.app.narrative,
|
narrative: state.app.narrative,
|
||||||
flags: {
|
flags: {
|
||||||
isShowingSites: state.app.flags.isShowingSites
|
isShowingSites: state.app.flags.isShowingSites
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
|
tiles: state.ui.tiles,
|
||||||
dom: state.ui.dom,
|
dom: state.ui.dom,
|
||||||
narratives: state.ui.style.narratives,
|
narratives: state.ui.style.narratives,
|
||||||
shapes: state.ui.style.shapes
|
shapes: state.ui.style.shapes
|
||||||
|
|||||||
@@ -1,90 +1,90 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux'
|
||||||
import * as selectors from '../selectors';
|
import * as selectors from '../selectors'
|
||||||
import hash from 'object-hash';
|
import hash from 'object-hash'
|
||||||
|
|
||||||
import copy from '../js/data/copy.json';
|
import copy from '../js/data/copy.json'
|
||||||
import { formatterWithYear, parseDate } from '../js/utilities';
|
import { formatterWithYear, parseDate } from '../js/utilities'
|
||||||
import TimelineHeader from './presentational/Timeline/Header';
|
import Header from './presentational/Timeline/Header'
|
||||||
import TimelineAxis from './TimelineAxis.jsx';
|
import Axis from './TimelineAxis.jsx'
|
||||||
import TimelineClip from './presentational/Timeline/Clip';
|
import Clip from './presentational/Timeline/Clip'
|
||||||
import TimelineHandles from './presentational/Timeline/Handles.js';
|
import Handles from './presentational/Timeline/Handles.js'
|
||||||
import TimelineZoomControls from './presentational/Timeline/ZoomControls.js';
|
import ZoomControls from './presentational/Timeline/ZoomControls.js'
|
||||||
import TimelineLabels from './presentational/Timeline/Labels.js';
|
import Labels from './presentational/Timeline/Labels.js'
|
||||||
import TimelineMarkers from './presentational/Timeline/Markers.js'
|
import Markers from './presentational/Timeline/Markers.js'
|
||||||
import TimelineEvents from './presentational/Timeline/Events.js';
|
import Events from './presentational/Timeline/Events.js'
|
||||||
import TimelineCategories from './TimelineCategories.jsx';
|
import Categories from './TimelineCategories.jsx'
|
||||||
|
|
||||||
class Timeline extends React.Component {
|
class Timeline extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props)
|
||||||
this.styleDatetime = this.styleDatetime.bind(this)
|
this.styleDatetime = this.styleDatetime.bind(this)
|
||||||
this.getDatetimeX = this.getDatetimeX.bind(this)
|
this.getDatetimeX = this.getDatetimeX.bind(this)
|
||||||
this.onApplyZoom = this.onApplyZoom.bind(this)
|
this.onApplyZoom = this.onApplyZoom.bind(this)
|
||||||
this.svgRef = React.createRef()
|
this.svgRef = React.createRef()
|
||||||
this.state = {
|
this.state = {
|
||||||
isFolded: false,
|
isFolded: false,
|
||||||
dims: props.app.dims,
|
dims: props.app.timeline.dimensions,
|
||||||
scaleX: null,
|
scaleX: null,
|
||||||
scaleY: null,
|
scaleY: null,
|
||||||
timerange: [null, null],
|
timerange: [null, null],
|
||||||
dragPos0: null,
|
dragPos0: null,
|
||||||
transitionDuration: 300
|
transitionDuration: 300
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.computeDims();
|
this.computeDims()
|
||||||
this.addEventListeners();
|
this.addEventListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (hash(nextProps) !== hash(this.props)) {
|
if (hash(nextProps) !== hash(this.props)) {
|
||||||
this.setState({
|
this.setState({
|
||||||
timerange: nextProps.app.timerange,
|
timerange: nextProps.app.timeline.range,
|
||||||
scaleX: this.makeScaleX()
|
scaleX: this.makeScaleX()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hash(nextProps.domain.categories) !== hash(this.props.domain.categories)) {
|
if (hash(nextProps.domain.categories) !== hash(this.props.domain.categories)) {
|
||||||
this.setState({
|
this.setState({
|
||||||
scaleY: this.makeScaleY(nextProps.domain.categories)
|
scaleY: this.makeScaleY(nextProps.domain.categories)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) {
|
if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) {
|
||||||
if (!!nextProps.app.selected && nextProps.app.selected.length > 0) {
|
if (!!nextProps.app.selected && nextProps.app.selected.length > 0) {
|
||||||
this.onCenterTime(parseDate(nextProps.app.selected[0].timestamp));
|
this.onCenterTime(parseDate(nextProps.app.selected[0].timestamp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListeners() {
|
addEventListeners() {
|
||||||
window.addEventListener('resize', () => { this.computeDims(); });
|
window.addEventListener('resize', () => { this.computeDims() })
|
||||||
let element = document.querySelector('.timeline-wrapper');
|
let element = document.querySelector('.timeline-wrapper')
|
||||||
element.addEventListener("transitionend", (event) => {
|
element.addEventListener("transitionend", (event) => {
|
||||||
this.computeDims();
|
this.computeDims()
|
||||||
});
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
makeScaleX() {
|
makeScaleX() {
|
||||||
return d3.scaleTime()
|
return d3.scaleTime()
|
||||||
.domain(this.state.timerange)
|
.domain(this.state.timerange)
|
||||||
.range([this.state.dims.margin_left, this.state.dims.width - this.state.dims.width_controls]);
|
.range([this.state.dims.margin_left, this.state.dims.width - this.state.dims.width_controls])
|
||||||
}
|
}
|
||||||
|
|
||||||
makeScaleY(categories) {
|
makeScaleY(categories) {
|
||||||
const tickHeight = 15;
|
const tickHeight = 15
|
||||||
const catsYpos = categories.map((g, i) => (i + 1) * this.state.dims.trackHeight / categories.length + tickHeight / 2);
|
const catsYpos = categories.map((g, i) => (i + 1) * this.state.dims.trackHeight / categories.length + tickHeight / 2)
|
||||||
return d3.scaleOrdinal()
|
return d3.scaleOrdinal()
|
||||||
.domain(categories)
|
.domain(categories)
|
||||||
.range(catsYpos);
|
.range(catsYpos)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (prevState.timerange !== this.state.timerange) {
|
if (prevState.timerange !== this.state.timerange) {
|
||||||
this.setState({ scaleX: this.makeScaleX() });
|
this.setState({ scaleX: this.makeScaleX() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,20 +93,20 @@ class Timeline extends React.Component {
|
|||||||
*/
|
*/
|
||||||
getTimeScaleExtent() {
|
getTimeScaleExtent() {
|
||||||
if (!this.state.scaleX) return 0
|
if (!this.state.scaleX) return 0
|
||||||
const timeDomain = this.state.scaleX.domain();
|
const timeDomain = this.state.scaleX.domain()
|
||||||
return (timeDomain[1].getTime() - timeDomain[0].getTime()) / 60000;
|
return (timeDomain[1].getTime() - timeDomain[0].getTime()) / 60000
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickArrow() {
|
onClickArrow() {
|
||||||
this.setState((prevState, props) => {
|
this.setState((prevState, props) => {
|
||||||
return {isFolded: !prevState.isFolded};
|
return {isFolded: !prevState.isFolded}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
computeDims() {
|
computeDims() {
|
||||||
const dom = this.props.ui.dom.timeline;
|
const dom = this.props.ui.dom.timeline
|
||||||
if (document.querySelector(`#${dom}`) !== null) {
|
if (document.querySelector(`#${dom}`) !== null) {
|
||||||
const boundingClient = document.querySelector(`#${dom}`).getBoundingClientRect();
|
const boundingClient = document.querySelector(`#${dom}`).getBoundingClientRect()
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
dims: {
|
dims: {
|
||||||
@@ -117,7 +117,7 @@ class Timeline extends React.Component {
|
|||||||
() => {
|
() => {
|
||||||
this.setState({ scaleX: this.makeScaleX()
|
this.setState({ scaleX: this.makeScaleX()
|
||||||
})
|
})
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,34 +126,34 @@ class Timeline extends React.Component {
|
|||||||
* @param {String} direction: 'forward' / 'backwards'
|
* @param {String} direction: 'forward' / 'backwards'
|
||||||
*/
|
*/
|
||||||
onMoveTime(direction) {
|
onMoveTime(direction) {
|
||||||
this.props.methods.onSelect();
|
this.props.methods.onSelect()
|
||||||
const extent = this.getTimeScaleExtent();
|
const extent = this.getTimeScaleExtent()
|
||||||
const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2);
|
const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2)
|
||||||
|
|
||||||
// if forward
|
// if forward
|
||||||
let domain0 = newCentralTime;
|
let domain0 = newCentralTime
|
||||||
let domainF = d3.timeMinute.offset(newCentralTime, extent);
|
let domainF = d3.timeMinute.offset(newCentralTime, extent)
|
||||||
|
|
||||||
// if backwards
|
// if backwards
|
||||||
if (direction === 'backwards') {
|
if (direction === 'backwards') {
|
||||||
domain0 = d3.timeMinute.offset(newCentralTime, -extent);
|
domain0 = d3.timeMinute.offset(newCentralTime, -extent)
|
||||||
domainF = newCentralTime;
|
domainF = newCentralTime
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ timerange: [domain0, domainF] }, () => {
|
this.setState({ timerange: [domain0, domainF] }, () => {
|
||||||
this.props.methods.onUpdateTimerange(this.state.timerange);
|
this.props.methods.onUpdateTimerange(this.state.timerange)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onCenterTime(newCentralTime) {
|
onCenterTime(newCentralTime) {
|
||||||
const extent = this.getTimeScaleExtent();
|
const extent = this.getTimeScaleExtent()
|
||||||
|
|
||||||
const domain0 = d3.timeMinute.offset(newCentralTime, -extent/2);
|
const domain0 = d3.timeMinute.offset(newCentralTime, -extent/2)
|
||||||
const domainF = d3.timeMinute.offset(newCentralTime, +extent/2);
|
const domainF = d3.timeMinute.offset(newCentralTime, +extent/2)
|
||||||
|
|
||||||
this.setState({ timerange: [domain0, domainF] }, () => {
|
this.setState({ timerange: [domain0, domainF] }, () => {
|
||||||
this.props.methods.onUpdateTimerange(this.state.timerange);
|
this.props.methods.onUpdateTimerange(this.state.timerange)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,7 +162,7 @@ class Timeline extends React.Component {
|
|||||||
* Used for updates in the middle of a transition, for performance purposes
|
* Used for updates in the middle of a transition, for performance purposes
|
||||||
*/
|
*/
|
||||||
onSoftTimeRangeUpdate(timerange) {
|
onSoftTimeRangeUpdate(timerange) {
|
||||||
this.setState({ timerange });
|
this.setState({ timerange })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,54 +170,55 @@ class Timeline extends React.Component {
|
|||||||
* @param {object} zoom: zoom level from zoomLevels
|
* @param {object} zoom: zoom level from zoomLevels
|
||||||
*/
|
*/
|
||||||
onApplyZoom(zoom) {
|
onApplyZoom(zoom) {
|
||||||
const extent = this.getTimeScaleExtent();
|
const extent = this.getTimeScaleExtent()
|
||||||
const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2);
|
const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2)
|
||||||
|
|
||||||
this.setState({ timerange: [
|
this.setState({ timerange: [
|
||||||
d3.timeMinute.offset(newCentralTime, -zoom.duration / 2),
|
d3.timeMinute.offset(newCentralTime, -zoom.duration / 2),
|
||||||
d3.timeMinute.offset(newCentralTime, zoom.duration / 2)
|
d3.timeMinute.offset(newCentralTime, zoom.duration / 2)
|
||||||
]}, () => {
|
]}, () => {
|
||||||
this.props.methods.onUpdateTimerange(this.state.timerange);
|
this.props.methods.onUpdateTimerange(this.state.timerange)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTransition(isTransition) {
|
toggleTransition(isTransition) {
|
||||||
this.setState({ transitionDuration: (isTransition) ? 300 : 0 });
|
this.setState({ transitionDuration: (isTransition) ? 300 : 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Setup drag behavior
|
* Setup drag behavior
|
||||||
*/
|
*/
|
||||||
onDragStart() {
|
onDragStart() {
|
||||||
d3.event.sourceEvent.stopPropagation();
|
d3.event.sourceEvent.stopPropagation()
|
||||||
this.setState({
|
this.setState({
|
||||||
dragPos0: d3.event.x
|
dragPos0: d3.event.x
|
||||||
}, () => {
|
}, () => {
|
||||||
this.toggleTransition(false);
|
this.toggleTransition(false)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Drag and update
|
* Drag and update
|
||||||
*/
|
*/
|
||||||
onDrag() {
|
onDrag() {
|
||||||
const drag0 = this.state.scaleX.invert(this.state.dragPos0).getTime();
|
const drag0 = this.state.scaleX.invert(this.state.dragPos0).getTime()
|
||||||
const dragNow = this.state.scaleX.invert(d3.event.x).getTime();
|
const dragNow = this.state.scaleX.invert(d3.event.x).getTime()
|
||||||
const timeShift = (drag0 - dragNow) / 1000;
|
const timeShift = (drag0 - dragNow) / 1000
|
||||||
|
|
||||||
const newDomain0 = d3.timeSecond.offset(this.props.app.timerange[0], timeShift);
|
const { range } = this.props.app.timeline
|
||||||
const newDomainF = d3.timeSecond.offset(this.props.app.timerange[1], timeShift);
|
const newDomain0 = d3.timeSecond.offset(range[0], timeShift)
|
||||||
|
const newDomainF = d3.timeSecond.offset(range[1], timeShift)
|
||||||
|
|
||||||
// Updates components without updating timerange
|
// Updates components without updating timerange
|
||||||
this.onSoftTimeRangeUpdate([newDomain0, newDomainF]);
|
this.onSoftTimeRangeUpdate([newDomain0, newDomainF])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop dragging and update data
|
* Stop dragging and update data
|
||||||
*/
|
*/
|
||||||
onDragEnd() {
|
onDragEnd() {
|
||||||
this.toggleTransition(true);
|
this.toggleTransition(true)
|
||||||
this.props.methods.onUpdateTimerange(this.state.timerange);
|
this.props.methods.onUpdateTimerange(this.state.timerange)
|
||||||
}
|
}
|
||||||
|
|
||||||
getDatetimeX(dt) {
|
getDatetimeX(dt) {
|
||||||
@@ -243,17 +244,17 @@ class Timeline extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isNarrative, app, ui } = this.props
|
const { isNarrative, app, ui } = this.props
|
||||||
let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`;
|
let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`
|
||||||
classes += (app.narrative !== null) ? ' narrative-mode' : '';
|
classes += (app.narrative !== null) ? ' narrative-mode' : ''
|
||||||
const dims = this.state.dims;
|
const { dims } = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<TimelineHeader
|
<Header
|
||||||
title={copy[this.props.app.language].timeline.info}
|
title={copy[this.props.app.language].timeline.info}
|
||||||
date0={formatterWithYear(this.state.timerange[0])}
|
date0={formatterWithYear(this.state.timerange[0])}
|
||||||
date1={formatterWithYear(this.state.timerange[1])}
|
date1={formatterWithYear(this.state.timerange[1])}
|
||||||
onClick={() => { this.onClickArrow(); }}
|
onClick={() => { this.onClickArrow() }}
|
||||||
hideInfo={isNarrative}
|
hideInfo={isNarrative}
|
||||||
/>
|
/>
|
||||||
<div className="timeline-content">
|
<div className="timeline-content">
|
||||||
@@ -263,43 +264,43 @@ class Timeline extends React.Component {
|
|||||||
width={dims.width}
|
width={dims.width}
|
||||||
height={dims.height}
|
height={dims.height}
|
||||||
>
|
>
|
||||||
<TimelineClip
|
<Clip
|
||||||
dims={dims}
|
dims={dims}
|
||||||
/>
|
/>
|
||||||
<TimelineAxis
|
<Axis
|
||||||
dims={dims}
|
dims={dims}
|
||||||
timerange={this.props.app.timerange}
|
timerange={this.props.app.timerange}
|
||||||
transitionDuration={this.state.transitionDuration}
|
transitionDuration={this.state.transitionDuration}
|
||||||
scaleX={this.state.scaleX}
|
scaleX={this.state.scaleX}
|
||||||
/>
|
/>
|
||||||
<TimelineCategories
|
<Categories
|
||||||
dims={dims}
|
dims={dims}
|
||||||
onDragStart={() => { this.onDragStart() }}
|
onDragStart={() => { this.onDragStart() }}
|
||||||
onDrag={() => { this.onDrag() }}
|
onDrag={() => { this.onDrag() }}
|
||||||
onDragEnd={() => { this.onDragEnd() }}
|
onDragEnd={() => { this.onDragEnd() }}
|
||||||
categories={this.props.domain.categories}
|
categories={this.props.domain.categories}
|
||||||
/>
|
/>
|
||||||
<TimelineHandles
|
<Handles
|
||||||
dims={dims}
|
dims={dims}
|
||||||
onMoveTime={(dir) => { this.onMoveTime(dir) }}
|
onMoveTime={(dir) => { this.onMoveTime(dir) }}
|
||||||
/>
|
/>
|
||||||
<TimelineZoomControls
|
<ZoomControls
|
||||||
extent={this.getTimeScaleExtent()}
|
extent={this.getTimeScaleExtent()}
|
||||||
zoomLevels={this.props.app.zoomLevels}
|
zoomLevels={this.props.app.timeline.zoomLevels}
|
||||||
dims={dims}
|
dims={dims}
|
||||||
onApplyZoom={this.onApplyZoom}
|
onApplyZoom={this.onApplyZoom}
|
||||||
/>
|
/>
|
||||||
<TimelineLabels
|
<Labels
|
||||||
dims={dims}
|
dims={dims}
|
||||||
timelabels={this.state.timerange}
|
timelabels={this.state.timerange}
|
||||||
/>
|
/>
|
||||||
<TimelineMarkers
|
<Markers
|
||||||
selected={this.props.app.selected}
|
selected={this.props.app.selected}
|
||||||
getEventX={this.getDatetimeX}
|
getEventX={this.getDatetimeX}
|
||||||
getCategoryY={this.state.scaleY}
|
getCategoryY={this.state.scaleY}
|
||||||
transitionDuration={this.state.transitionDuration}
|
transitionDuration={this.state.transitionDuration}
|
||||||
/>
|
/>
|
||||||
<TimelineEvents
|
<Events
|
||||||
datetimes={this.props.domain.datetimes}
|
datetimes={this.props.domain.datetimes}
|
||||||
styleDatetime={this.styleDatetime}
|
styleDatetime={this.styleDatetime}
|
||||||
narrative={this.props.app.narrative}
|
narrative={this.props.app.narrative}
|
||||||
@@ -313,7 +314,7 @@ class Timeline extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,11 +327,9 @@ function mapStateToProps(state) {
|
|||||||
narratives: state.domain.narratives
|
narratives: state.domain.narratives
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
timerange: selectors.getTimeRange(state),
|
|
||||||
dims: state.app.timeline.dimensions,
|
|
||||||
selected: state.app.selected,
|
selected: state.app.selected,
|
||||||
language: state.app.language,
|
language: state.app.language,
|
||||||
zoomLevels: state.app.timeline.zoomLevels,
|
timeline: state.app.timeline,
|
||||||
narrative: state.app.narrative
|
narrative: state.app.narrative
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
@@ -339,4 +338,4 @@ function mapStateToProps(state) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Timeline);
|
export default connect(mapStateToProps)(Timeline)
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ function updateSelected(appState, action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateNarrative(appState, action) {
|
function updateNarrative(appState, action) {
|
||||||
let minTime = appState.filters.timerange[0]
|
let minTime = appState.timeline.range[0]
|
||||||
let maxTime = appState.filters.timerange[1]
|
let maxTime = appState.timeline.range[1]
|
||||||
|
|
||||||
let cornerBound0 = [180, 180]
|
let cornerBound0 = [180, 180]
|
||||||
let cornerBound1 = [-180, -180]
|
let cornerBound1 = [-180, -180]
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const getNotifications = state => state.domain.notifications
|
|||||||
export const getTagTree = state => state.domain.tags
|
export const getTagTree = state => state.domain.tags
|
||||||
export const getTagsFilter = state => state.app.filters.tags
|
export const getTagsFilter = state => state.app.filters.tags
|
||||||
export const getCategoriesFilter = state => state.app.filters.categories
|
export const getCategoriesFilter = state => state.app.filters.categories
|
||||||
export const getTimeRange = state => state.app.filters.timerange
|
export const getTimeRange = state => state.app.timeline.range
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,23 +39,24 @@ const initial = {
|
|||||||
current: null
|
current: null
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
timerange: [
|
|
||||||
new Date(2013, 2, 23, 12),
|
|
||||||
new Date(2016, 2, 23, 12)
|
|
||||||
],
|
|
||||||
mapBounds: null,
|
|
||||||
tags: [],
|
tags: [],
|
||||||
categories: [],
|
categories: [],
|
||||||
views: {
|
views: {
|
||||||
events: true,
|
events: true,
|
||||||
coevents: false,
|
|
||||||
routes: false,
|
routes: false,
|
||||||
sites: true
|
sites: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
isMobile: (/Mobi/.test(navigator.userAgent)),
|
isMobile: (/Mobi/.test(navigator.userAgent)),
|
||||||
language: 'en-US',
|
language: 'en-US',
|
||||||
mapAnchor: [31.356397, 34.784818],
|
map: {
|
||||||
|
anchor: [31.356397, 34.784818],
|
||||||
|
startZoom: 10,
|
||||||
|
minZoom: 7,
|
||||||
|
maxZoom: 18,
|
||||||
|
bounds: null,
|
||||||
|
maxBounds: [[180, -180], [-180, 180]]
|
||||||
|
},
|
||||||
timeline: {
|
timeline: {
|
||||||
dimensions: {
|
dimensions: {
|
||||||
height: 140,
|
height: 140,
|
||||||
@@ -66,6 +67,10 @@ const initial = {
|
|||||||
margin_top: 20,
|
margin_top: 20,
|
||||||
trackHeight: 80
|
trackHeight: 80
|
||||||
},
|
},
|
||||||
|
range: [
|
||||||
|
new Date(2013, 2, 23, 12),
|
||||||
|
new Date(2016, 2, 23, 12)
|
||||||
|
],
|
||||||
zoomLevels: [
|
zoomLevels: [
|
||||||
{ label: '3 years', duration: 1576800 },
|
{ label: '3 years', duration: 1576800 },
|
||||||
{ label: '3 months', duration: 129600 },
|
{ label: '3 months', duration: 129600 },
|
||||||
@@ -92,6 +97,7 @@ const initial = {
|
|||||||
* as well as dom elements to attach SVG
|
* as well as dom elements to attach SVG
|
||||||
*/
|
*/
|
||||||
ui: {
|
ui: {
|
||||||
|
tiles: 'openstreetmap', // ['openstreetmap', 'streets', 'satellite']
|
||||||
style: {
|
style: {
|
||||||
categories: {
|
categories: {
|
||||||
default: '#f3de2c',
|
default: '#f3de2c',
|
||||||
@@ -127,7 +133,7 @@ if (process.env.store) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NB: config.js dates get implicitly converted to strings in mergeDeepLeft
|
// NB: config.js dates get implicitly converted to strings in mergeDeepLeft
|
||||||
appStore.app.filters.timerange[0] = new Date(appStore.app.filters.timerange[0])
|
appStore.app.timeline.range[0] = new Date(appStore.app.timeline.range[0])
|
||||||
appStore.app.filters.timerange[1] = new Date(appStore.app.filters.timerange[1])
|
appStore.app.timeline.range[1] = new Date(appStore.app.timeline.range[1])
|
||||||
|
|
||||||
export default appStore
|
export default appStore
|
||||||
|
|||||||
Reference in New Issue
Block a user