diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 97a4160..aa40290 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -1,27 +1,31 @@ -import React from 'react'; -import { Portal } from 'react-portal'; +import React from 'react' +import { Portal } from 'react-portal' import { connect } from 'react-redux' import * as selectors from '../selectors' -import hash from 'object-hash'; -import 'leaflet'; +import hash from 'object-hash' +import 'leaflet' -import { isNotNullNorUndefined } from '../js/utilities'; +import { isNotNullNorUndefined } from '../js/utilities' -import Sites from './presentational/Map/Sites.jsx'; -import Shapes from './presentational/Map/Shapes.jsx'; -import Events from './presentational/Map/Events.jsx'; -import SelectedEvents from './presentational/Map/SelectedEvents.jsx'; -import Narratives from './presentational/Map/Narratives.jsx'; -import DefsMarkers from './presentational/Map/DefsMarkers.jsx'; +import Sites from './presentational/Map/Sites.jsx' +import Shapes from './presentational/Map/Shapes.jsx' +import Events from './presentational/Map/Events.jsx' +import SelectedEvents from './presentational/Map/SelectedEvents.jsx' +import Narratives from './presentational/Map/Narratives.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 { constructor() { - super(); + super() this.projectPoint = this.projectPoint.bind(this) - this.svgRef = React.createRef(); - this.map = null; + this.svgRef = React.createRef() + this.map = null this.state = { mapTransformX: 0, mapTransformY: 0 @@ -31,22 +35,23 @@ class Map extends React.Component { componentDidMount(){ if (this.map === null) { - this.initializeMap(); + this.initializeMap() } } componentWillReceiveProps(nextProps) { // 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); + const { bounds } = nextProps.app.map + if (hash(bounds) !== hash(this.props.app.map.bounds) + && bounds !== null) { + this.map.fitBounds(bounds) } 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; + 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]); + 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 */ + const { map: mapConf } = this.props.app const map = L.map(this.props.ui.dom.map) - .setView(this.props.app.mapAnchor, 14) - .setMinZoom(7) - .setMaxZoom(18) - .setMaxBounds([[180, -180], [-180, 180]]) + .setView(mapConf.anchor, mapConf.startZoom) + .setMinZoom(mapConf.minZoom) + .setMaxZoom(mapConf.maxZoom) + .setMaxBounds(mapConf.maxBounds) - let s; - if (process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== 'your_token') { + let s + + if ((supportedMapboxMap.indexOf(this.props.ui.tiles) !== -1) && process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== defaultToken) { 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 { - // eslint-disable-next-line - alert(`No mapbox token specified in config. - 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 = L.tileLayer( + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' + ) } - s = s.addTo(map); + s = s.addTo(map) - map.keyboard.disable(); + map.keyboard.disable() - map.on('move zoomend viewreset moveend', () => this.alignLayers()); - 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'); }); - window.addEventListener('resize', () => { 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('zoomend', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.remove('hide') }) + window.addEventListener('resize', () => { this.alignLayers() }) - this.map = map; + this.map = map } alignLayers() { - const mapNode = document.querySelector('.leaflet-map-pane'); - if (mapNode === null) return { transformX: 0, transformY: 0 }; + const mapNode = document.querySelector('.leaflet-map-pane') + if (mapNode === null) return { transformX: 0, transformY: 0 } // We'll get the transform of the leaflet container, // which will let us offset the SVG by the same quantity const transform = window .getComputedStyle(mapNode) - .getPropertyValue('transform'); + .getPropertyValue('transform') // Offset with leaflet map transform boundaries this.setState({ @@ -118,7 +122,7 @@ class Map extends React.Component { } getClientDims() { - const boundingClient = document.querySelector(`#${this.props.ui.dom.map}`).getBoundingClientRect(); + const boundingClient = document.querySelector(`#${this.props.ui.dom.map}`).getBoundingClientRect() return { width: boundingClient.width, @@ -127,8 +131,8 @@ class Map extends React.Component { } renderTiles() { - const pane = this.map.getPanes().overlayPane; - const { width, height } = this.getClientDims(); + const pane = this.map.getPanes().overlayPane + const { width, height } = this.getClientDims() return ( @@ -141,7 +145,7 @@ class Map extends React.Component { > - ); + ) } renderSites() { @@ -151,7 +155,7 @@ class Map extends React.Component { projectPoint={this.projectPoint} isEnabled={this.props.app.views.sites} /> - ); + ) } renderShapes() { @@ -176,7 +180,7 @@ class Map extends React.Component { onSelect={this.props.methods.onSelect} onSelectNarrative={this.props.methods.onSelectNarrative} /> - ); + ) } /** @@ -209,7 +213,7 @@ class Map extends React.Component { onSelectNarrative={this.props.methods.onSelectNarrative} getCategoryColor={this.props.methods.getCategoryColor} /> - ); + ) } renderSelected() { @@ -219,7 +223,7 @@ class Map extends React.Component { selected={this.props.app.selected} projectPoint={this.projectPoint} /> - ); + ) } @@ -234,7 +238,7 @@ class Map extends React.Component { render() { 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 ? ( {this.renderTiles()} @@ -252,7 +256,7 @@ class Map extends React.Component {
{innerMap}
- ); + ) } } @@ -269,14 +273,14 @@ function mapStateToProps(state) { views: state.app.filters.views, selected: state.app.selected, highlighted: state.app.highlighted, - mapAnchor: state.app.mapAnchor, - mapBounds: state.app.filters.mapBounds, + map: state.app.map, narrative: state.app.narrative, flags: { isShowingSites: state.app.flags.isShowingSites } }, ui: { + tiles: state.ui.tiles, dom: state.ui.dom, narratives: state.ui.style.narratives, shapes: state.ui.style.shapes diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index ac49152..a7ebdba 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -1,90 +1,90 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import * as selectors from '../selectors'; -import hash from 'object-hash'; +import React from 'react' +import { connect } from 'react-redux' +import * as selectors from '../selectors' +import hash from 'object-hash' -import copy from '../js/data/copy.json'; -import { formatterWithYear, parseDate } from '../js/utilities'; -import TimelineHeader from './presentational/Timeline/Header'; -import TimelineAxis from './TimelineAxis.jsx'; -import TimelineClip from './presentational/Timeline/Clip'; -import TimelineHandles from './presentational/Timeline/Handles.js'; -import TimelineZoomControls from './presentational/Timeline/ZoomControls.js'; -import TimelineLabels from './presentational/Timeline/Labels.js'; -import TimelineMarkers from './presentational/Timeline/Markers.js' -import TimelineEvents from './presentational/Timeline/Events.js'; -import TimelineCategories from './TimelineCategories.jsx'; +import copy from '../js/data/copy.json' +import { formatterWithYear, parseDate } from '../js/utilities' +import Header from './presentational/Timeline/Header' +import Axis from './TimelineAxis.jsx' +import Clip from './presentational/Timeline/Clip' +import Handles from './presentational/Timeline/Handles.js' +import ZoomControls from './presentational/Timeline/ZoomControls.js' +import Labels from './presentational/Timeline/Labels.js' +import Markers from './presentational/Timeline/Markers.js' +import Events from './presentational/Timeline/Events.js' +import Categories from './TimelineCategories.jsx' class Timeline extends React.Component { constructor(props) { - super(props); + super(props) this.styleDatetime = this.styleDatetime.bind(this) this.getDatetimeX = this.getDatetimeX.bind(this) this.onApplyZoom = this.onApplyZoom.bind(this) this.svgRef = React.createRef() this.state = { isFolded: false, - dims: props.app.dims, + dims: props.app.timeline.dimensions, scaleX: null, scaleY: null, timerange: [null, null], dragPos0: null, transitionDuration: 300 - }; + } } componentDidMount() { - this.computeDims(); - this.addEventListeners(); + this.computeDims() + this.addEventListeners() } componentWillReceiveProps(nextProps) { if (hash(nextProps) !== hash(this.props)) { this.setState({ - timerange: nextProps.app.timerange, + timerange: nextProps.app.timeline.range, scaleX: this.makeScaleX() - }); + }) } if (hash(nextProps.domain.categories) !== hash(this.props.domain.categories)) { this.setState({ 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)); + this.onCenterTime(parseDate(nextProps.app.selected[0].timestamp)) } } } addEventListeners() { - window.addEventListener('resize', () => { this.computeDims(); }); - let element = document.querySelector('.timeline-wrapper'); + window.addEventListener('resize', () => { this.computeDims() }) + let element = document.querySelector('.timeline-wrapper') element.addEventListener("transitionend", (event) => { - this.computeDims(); - }); + this.computeDims() + }) } makeScaleX() { return d3.scaleTime() .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) { - const tickHeight = 15; - const catsYpos = categories.map((g, i) => (i + 1) * this.state.dims.trackHeight / categories.length + tickHeight / 2); + const tickHeight = 15 + const catsYpos = categories.map((g, i) => (i + 1) * this.state.dims.trackHeight / categories.length + tickHeight / 2) return d3.scaleOrdinal() .domain(categories) - .range(catsYpos); + .range(catsYpos) } componentDidUpdate(prevProps, prevState) { 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() { if (!this.state.scaleX) return 0 - const timeDomain = this.state.scaleX.domain(); - return (timeDomain[1].getTime() - timeDomain[0].getTime()) / 60000; + const timeDomain = this.state.scaleX.domain() + return (timeDomain[1].getTime() - timeDomain[0].getTime()) / 60000 } onClickArrow() { this.setState((prevState, props) => { - return {isFolded: !prevState.isFolded}; - }); + return {isFolded: !prevState.isFolded} + }) } computeDims() { - const dom = this.props.ui.dom.timeline; + const dom = this.props.ui.dom.timeline if (document.querySelector(`#${dom}`) !== null) { - const boundingClient = document.querySelector(`#${dom}`).getBoundingClientRect(); + const boundingClient = document.querySelector(`#${dom}`).getBoundingClientRect() this.setState({ dims: { @@ -117,7 +117,7 @@ class Timeline extends React.Component { () => { this.setState({ scaleX: this.makeScaleX() }) - }); + }) } } @@ -126,34 +126,34 @@ class Timeline extends React.Component { * @param {String} direction: 'forward' / 'backwards' */ onMoveTime(direction) { - this.props.methods.onSelect(); - const extent = this.getTimeScaleExtent(); - const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2); + this.props.methods.onSelect() + const extent = this.getTimeScaleExtent() + const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2) // if forward - let domain0 = newCentralTime; - let domainF = d3.timeMinute.offset(newCentralTime, extent); + let domain0 = newCentralTime + let domainF = d3.timeMinute.offset(newCentralTime, extent) // if backwards if (direction === 'backwards') { - domain0 = d3.timeMinute.offset(newCentralTime, -extent); - domainF = newCentralTime; + domain0 = d3.timeMinute.offset(newCentralTime, -extent) + domainF = newCentralTime } this.setState({ timerange: [domain0, domainF] }, () => { - this.props.methods.onUpdateTimerange(this.state.timerange); - }); + this.props.methods.onUpdateTimerange(this.state.timerange) + }) } onCenterTime(newCentralTime) { - const extent = this.getTimeScaleExtent(); + const extent = this.getTimeScaleExtent() - const domain0 = d3.timeMinute.offset(newCentralTime, -extent/2); - const domainF = d3.timeMinute.offset(newCentralTime, +extent/2); + 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); - }); + 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 */ onSoftTimeRangeUpdate(timerange) { - this.setState({ timerange }); + this.setState({ timerange }) } /** @@ -170,54 +170,55 @@ class Timeline extends React.Component { * @param {object} zoom: zoom level from zoomLevels */ onApplyZoom(zoom) { - const extent = this.getTimeScaleExtent(); - const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2); + const extent = this.getTimeScaleExtent() + const newCentralTime = d3.timeMinute.offset(this.state.scaleX.domain()[0], extent / 2) this.setState({ timerange: [ 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) { - this.setState({ transitionDuration: (isTransition) ? 300 : 0 }); + this.setState({ transitionDuration: (isTransition) ? 300 : 0 }) } /* * Setup drag behavior */ onDragStart() { - d3.event.sourceEvent.stopPropagation(); + d3.event.sourceEvent.stopPropagation() this.setState({ dragPos0: d3.event.x }, () => { - this.toggleTransition(false); - }); + this.toggleTransition(false) + }) } /* * Drag and update */ onDrag() { - const drag0 = this.state.scaleX.invert(this.state.dragPos0).getTime(); - const dragNow = this.state.scaleX.invert(d3.event.x).getTime(); - const timeShift = (drag0 - dragNow) / 1000; + const drag0 = this.state.scaleX.invert(this.state.dragPos0).getTime() + const dragNow = this.state.scaleX.invert(d3.event.x).getTime() + const timeShift = (drag0 - dragNow) / 1000 - const newDomain0 = d3.timeSecond.offset(this.props.app.timerange[0], timeShift); - const newDomainF = d3.timeSecond.offset(this.props.app.timerange[1], timeShift); + const { range } = this.props.app.timeline + const newDomain0 = d3.timeSecond.offset(range[0], timeShift) + const newDomainF = d3.timeSecond.offset(range[1], timeShift) // Updates components without updating timerange - this.onSoftTimeRangeUpdate([newDomain0, newDomainF]); + this.onSoftTimeRangeUpdate([newDomain0, newDomainF]) } /** * Stop dragging and update data */ onDragEnd() { - this.toggleTransition(true); - this.props.methods.onUpdateTimerange(this.state.timerange); + this.toggleTransition(true) + this.props.methods.onUpdateTimerange(this.state.timerange) } getDatetimeX(dt) { @@ -243,17 +244,17 @@ class Timeline extends React.Component { render() { const { isNarrative, app, ui } = this.props - let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}`; - classes += (app.narrative !== null) ? ' narrative-mode' : ''; - const dims = this.state.dims; + let classes = `timeline-wrapper ${(this.state.isFolded) ? ' folded' : ''}` + classes += (app.narrative !== null) ? ' narrative-mode' : '' + const { dims } = this.state return (
- { this.onClickArrow(); }} + onClick={() => { this.onClickArrow() }} hideInfo={isNarrative} />
@@ -263,43 +264,43 @@ class Timeline extends React.Component { width={dims.width} height={dims.height} > - - - { this.onDragStart() }} onDrag={() => { this.onDrag() }} onDragEnd={() => { this.onDragEnd() }} categories={this.props.domain.categories} /> - { this.onMoveTime(dir) }} /> - - - -
- ); + ) } } @@ -326,11 +327,9 @@ function mapStateToProps(state) { narratives: state.domain.narratives }, app: { - timerange: selectors.getTimeRange(state), - dims: state.app.timeline.dimensions, selected: state.app.selected, language: state.app.language, - zoomLevels: state.app.timeline.zoomLevels, + timeline: state.app.timeline, narrative: state.app.narrative }, ui: { @@ -339,4 +338,4 @@ function mapStateToProps(state) { } } -export default connect(mapStateToProps)(Timeline); +export default connect(mapStateToProps)(Timeline) diff --git a/src/reducers/app.js b/src/reducers/app.js index 43e39aa..f17afac 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -36,8 +36,8 @@ function updateSelected(appState, action) { } function updateNarrative(appState, action) { - let minTime = appState.filters.timerange[0] - let maxTime = appState.filters.timerange[1] + let minTime = appState.timeline.range[0] + let maxTime = appState.timeline.range[1] let cornerBound0 = [180, 180] let cornerBound1 = [-180, -180] diff --git a/src/selectors/index.js b/src/selectors/index.js index e9dfab2..6494b4a 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -25,7 +25,7 @@ export const getNotifications = state => state.domain.notifications export const getTagTree = state => state.domain.tags export const getTagsFilter = state => state.app.filters.tags export const getCategoriesFilter = state => state.app.filters.categories -export const getTimeRange = state => state.app.filters.timerange +export const getTimeRange = state => state.app.timeline.range /** diff --git a/src/store/initial.js b/src/store/initial.js index 9c3326a..ad7f021 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -39,23 +39,24 @@ const initial = { current: null }, filters: { - timerange: [ - new Date(2013, 2, 23, 12), - new Date(2016, 2, 23, 12) - ], - mapBounds: null, tags: [], categories: [], views: { events: true, - coevents: false, routes: false, sites: true }, }, isMobile: (/Mobi/.test(navigator.userAgent)), 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: { dimensions: { height: 140, @@ -66,6 +67,10 @@ const initial = { margin_top: 20, trackHeight: 80 }, + range: [ + new Date(2013, 2, 23, 12), + new Date(2016, 2, 23, 12) + ], zoomLevels: [ { label: '3 years', duration: 1576800 }, { label: '3 months', duration: 129600 }, @@ -92,6 +97,7 @@ const initial = { * as well as dom elements to attach SVG */ ui: { + tiles: 'openstreetmap', // ['openstreetmap', 'streets', 'satellite'] style: { categories: { default: '#f3de2c', @@ -127,7 +133,7 @@ if (process.env.store) { } // 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.filters.timerange[1] = new Date(appStore.app.filters.timerange[1]) +appStore.app.timeline.range[0] = new Date(appStore.app.timeline.range[0]) +appStore.app.timeline.range[1] = new Date(appStore.app.timeline.range[1]) export default appStore