diff --git a/.gitignore b/.gitignore index 4ce2e5c..3b88e33 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ build/ node_modules/ config.js dev.config.js + +src/\.DS_Store diff --git a/example.config.js b/example.config.js index ee5b1c6..852b944 100644 --- a/example.config.js +++ b/example.config.js @@ -7,7 +7,7 @@ module.exports = { SOURCES_EXT: '/api/example/export_sources/deepids', TAGS_EXT: '/api/example/export_tags/tree', SITES_EXT: '/api/example/export_sites/rows', - MAP_ANCHOR: [31.356397, 34.784818], + SHAPES_EXT: '/api/example/export_shapes/columns', INCOMING_DATETIME_FORMAT: '%m/%d/%YT%H:%M', MAPBOX_TOKEN: 'pk.EXAMPLE_MAPBOX_TOKEN', features: { @@ -15,7 +15,26 @@ module.exports = { USE_SEARCH: false, USE_SITES: true, USE_SOURCES: true, + USE_SHAPES: true, CATEGORIES_AS_TAGS: true + }, + store: { + app: { + mapAnchor: [31.356397, 34.784818], + filters: { + // timerange: [ + // new Date(2015, 7, 9), + // new Date(2015, 10, 6, 23) + // ] + } + }, + ui: { + style: { + categories: {}, + shapes: {}, + narratives: {} + } + } } } diff --git a/src/actions/index.js b/src/actions/index.js index cad3fa1..c2f6041 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -1,12 +1,14 @@ import { urlFromEnv } from '../js/utilities' -const EVENT_DATA_URL = urlFromEnv('EVENT_EXT'); -const CATEGORY_URL = urlFromEnv('CATEGORY_EXT'); -const TAGS_URL = urlFromEnv('TAGS_EXT'); -const SOURCES_URL = urlFromEnv('SOURCES_EXT'); -const NARRATIVE_URL = urlFromEnv('NARRATIVE_EXT'); -const SITES_URL = urlFromEnv('SITES_EXT'); -const eventUrlMap = (event) => `${process.env.SERVER_ROOT}${process.env.EVENT_DESC_ROOT}/${(event.id) ? event.id : event}`; +// TODO: relegate these URLs entirely to environment variables +const EVENT_DATA_URL = urlFromEnv('EVENT_EXT') +const CATEGORY_URL = urlFromEnv('CATEGORY_EXT') +const TAGS_URL = urlFromEnv('TAGS_EXT') +const SOURCES_URL = urlFromEnv('SOURCES_EXT') +const NARRATIVE_URL = urlFromEnv('NARRATIVE_EXT') +const SITES_URL = urlFromEnv('SITES_EXT') +const SHAPES_URL = urlFromEnv('SHAPES_EXT') +const eventUrlMap = (event) => `${process.env.SERVER_ROOT}${process.env.EVENT_DESC_ROOT}/${(event.id) ? event.id : event}` const domainMsg = (domainType) => `Something went wrong fetching ${domainType}. Check the URL or try disabling them in the config file.` @@ -67,13 +69,21 @@ export function fetchDomain () { } } + let shapesPromise = Promise.resolve([]) + if (process.env.features.USE_SHAPES) { + shapesPromise = fetch(SHAPES_URL) + .then(response => response.json()) + .catch(() => handleError(domainMsg('shapes'))) + } + return Promise.all([ eventPromise, catPromise, narPromise, sitesPromise, tagsPromise, - sourcesPromise + sourcesPromise, + shapesPromise ]) .then(response => { const result = { @@ -83,6 +93,7 @@ export function fetchDomain () { sites: response[3], tags: response[4], sources: response[5], + shapes: response[6], notifications } if (Object.values(result).some(resp => resp.hasOwnProperty('error'))) { @@ -96,7 +107,7 @@ export function fetchDomain () { // TODO: handle this appropriately in React hierarchy alert(err.message) }) - }; + } } export const FETCH_ERROR = 'FETCH_ERROR' @@ -189,7 +200,7 @@ export function updateTimeRange(timerange) { } } -export const UPDATE_NARRATIVE = 'UPDATE_NARRATIVE'; +export const UPDATE_NARRATIVE = 'UPDATE_NARRATIVE' export function updateNarrative(narrative) { return { type: UPDATE_NARRATIVE, @@ -197,14 +208,14 @@ export function updateNarrative(narrative) { } } -export const INCREMENT_NARRATIVE_CURRENT = 'INCREMENT_NARRATIVE_CURRENT'; +export const INCREMENT_NARRATIVE_CURRENT = 'INCREMENT_NARRATIVE_CURRENT' export function incrementNarrativeCurrent() { return { type: INCREMENT_NARRATIVE_CURRENT } } -export const DECREMENT_NARRATIVE_CURRENT = 'DECREMENT_NARRATIVE_CURRENT'; +export const DECREMENT_NARRATIVE_CURRENT = 'DECREMENT_NARRATIVE_CURRENT' export function decrementNarrativeCurrent() { return { type: DECREMENT_NARRATIVE_CURRENT @@ -249,7 +260,7 @@ export function toggleFetchingSources() { } } -export const TOGGLE_LANGUAGE = 'TOGGLE_LANGUAGE'; +export const TOGGLE_LANGUAGE = 'TOGGLE_LANGUAGE' export function toggleLanguage(language) { return { type: TOGGLE_LANGUAGE, @@ -257,21 +268,21 @@ export function toggleLanguage(language) { } } -export const CLOSE_TOOLBAR = 'CLOSE_TOOLBAR'; +export const CLOSE_TOOLBAR = 'CLOSE_TOOLBAR' export function closeToolbar() { return { type: CLOSE_TOOLBAR } } -export const TOGGLE_INFOPOPUP = 'TOGGLE_INFOPOPUP'; +export const TOGGLE_INFOPOPUP = 'TOGGLE_INFOPOPUP' export function toggleInfoPopup() { return { type: TOGGLE_INFOPOPUP } } -export const TOGGLE_MAPVIEW = 'TOGGLE_MAPVIEW'; +export const TOGGLE_MAPVIEW = 'TOGGLE_MAPVIEW' export function toggleMapView(layer) { return { type: TOGGLE_MAPVIEW, diff --git a/src/components/.DS_Store b/src/components/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/src/components/.DS_Store differ diff --git a/src/components/Card.jsx b/src/components/Card.jsx index 2e996e9..b77e31c 100644 --- a/src/components/Card.jsx +++ b/src/components/Card.jsx @@ -7,14 +7,14 @@ import { import React from 'react' import Spinner from './presentational/Spinner' -import CardTimestamp from './presentational/CardTimestamp' -import CardLocation from './presentational/CardLocation' -import CardCaret from './presentational/CardCaret' -import CardTags from './presentational/CardTags' -import CardSummary from './presentational/CardSummary' -import CardSource from './presentational/CardSource' -import CardCategory from './presentational/CardCategory' -import CardNarrative from './presentational/CardNarrative' +import CardTimestamp from './presentational/Card/Timestamp' +import CardLocation from './presentational/Card/Location' +import CardCaret from './presentational/Card/Caret' +import CardTags from './presentational/Card/Tags' +import CardSummary from './presentational/Card/Summary' +import CardSource from './presentational/Card/Source' +import CardCategory from './presentational/Card/Category' +import CardNarrative from './presentational/Card/Narrative' class Card extends React.Component { diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 5817f83..7465bb7 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -9,7 +9,7 @@ import LoadingOverlay from './presentational/LoadingOverlay' import Map from './Map.jsx' import Toolbar from './Toolbar.jsx' import CardStack from './CardStack.jsx' -import NarrativeControls from './presentational/NarrativeControls.js' +import NarrativeControls from './presentational/Narrative/Controls.js' import InfoPopUp from './InfoPopup.jsx' import Timeline from './Timeline.jsx' import Notification from './Notification.jsx' diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 8b0dd40..d5c8d85 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -1,26 +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 MapSites from './MapSites.jsx'; -import MapEvents from './MapEvents.jsx'; -import MapSelectedEvents from './MapSelectedEvents.jsx'; -import MapNarratives from './MapNarratives.jsx'; -import MapDefsMarkers from './MapDefsMarkers.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(); - this.svgRef = React.createRef(); - this.map = null; + super() + this.projectPoint = this.projectPoint.bind(this) + this.svgRef = React.createRef() + this.map = null this.state = { mapTransformX: 0, mapTransformY: 0 @@ -30,76 +35,76 @@ 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); + // Set appropriate zoom for narrative + 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]) } - } + } } - } + } initializeMap() { /** * 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/styles/v1/${this.props.ui.tiles}/tiles/{z}/{x}/{y}?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({ @@ -108,8 +113,16 @@ class Map extends React.Component { }) } + projectPoint(location) { + const latLng = new L.LatLng(location[0], location[1]) + return { + x: this.map.latLngToLayerPoint(latLng).x + this.state.mapTransformX, + y: this.map.latLngToLayerPoint(latLng).y + this.state.mapTransformY + } + } + 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, @@ -118,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 ( @@ -132,35 +145,42 @@ class Map extends React.Component { > - ); + ) } renderSites() { return ( - - ); + ) + } + + renderShapes() { + return ( + + ) } renderNarratives() { return ( - - ); + ) } /** @@ -182,39 +202,35 @@ class Map extends React.Component { renderEvents() { return ( - - ); + ) } renderSelected() { return ( - - ); + ) } renderMarkers() { return ( - + ) } @@ -222,19 +238,25 @@ 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()} + {this.renderMarkers()} + {isShowingSites ? this.renderSites() : null} + {this.renderShapes()} + {this.renderEvents()} + {this.renderNarratives()} + {this.renderSelected()} + + ) : null return (
- {(this.map !== null) ? this.renderTiles() : ''} - {(this.map !== null) ? this.renderMarkers() : ''} - {(this.map !== null) && isShowingSites ? this.renderSites() : ''} - {(this.map !== null) ? this.renderEvents() : ''} - {(this.map !== null) ? this.renderNarratives() : ''} - {(this.map !== null) ? this.renderSelected() : ''} + {innerMap}
- ); + ) } } @@ -244,22 +266,24 @@ function mapStateToProps(state) { locations: selectors.selectLocations(state), narratives: selectors.selectNarratives(state), categories: selectors.selectCategories(state), - sites: selectors.getSites(state) + sites: selectors.getSites(state), + shapes: selectors.getShapes(state) }, app: { 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 + narratives: state.ui.style.narratives, + shapes: state.ui.style.shapes } } } diff --git a/src/components/MapSites.jsx b/src/components/MapSites.jsx deleted file mode 100644 index 16d4a92..0000000 --- a/src/components/MapSites.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; - -class MapSites extends React.Component { - - projectPoint(location) { - const latLng = new L.LatLng(location[0], location[1]); - return { - x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX, - y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY - }; - } - - renderSite(site) { - const { x, y } = this.projectPoint([site.latitude, site.longitude]); - - return (
- {site.site} -
- ); - } - - render () { - if (!this.props.sites || !this.props.sites.length) return
; - - return ( -
- {this.props.sites.map(site => { return this.renderSite(site); })} -
- ) - } - -} - -export default MapSites; \ No newline at end of file diff --git a/src/components/SourceOverlay.jsx b/src/components/SourceOverlay.jsx index 9191307..ea1f08d 100644 --- a/src/components/SourceOverlay.jsx +++ b/src/components/SourceOverlay.jsx @@ -145,7 +145,7 @@ class SourceOverlay extends React.Component {
this.onShiftGallery(-1)}>
this.onShiftGallery(1)}>
- ); + ); } return (
@@ -175,7 +175,7 @@ class SourceOverlay extends React.Component {
-

{`${this.state.idx+1} / ${paths.length}`}

+ {/*

{`${this.state.idx+1} / ${paths.length}`}

*/} {title?

{title}

: null}
{desc}
diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 8d7104b..840f05f 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -1,98 +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/TimelineHeader'; -import TimelineAxis from './TimelineAxis.jsx'; -import TimelineClip from './presentational/TimelineClip'; -import TimelineHandles from './presentational/TimelineHandles.js'; -import TimelineZoomControls from './presentational/TimelineZoomControls.js'; -import TimelineLabels from './presentational/TimelineLabels.js'; -import TimelineMarkers from './presentational/TimelineMarkers.js' -import TimelineEvents from './presentational/TimelineEvents.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: { - height: 140, - width: 0, - width_controls: 100, - height_controls: 115, - margin_left: 120, - margin_top: 20, - trackHeight: 80 - }, + 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() }) } } @@ -101,25 +93,31 @@ 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: Object.assign({}, this.state.dims, { width: boundingClient.width }) - }, () => { this.setState({ scaleX: this.makeScaleX() }) - }); + dims: { + ...this.state.dims, + width: boundingClient.width + } + }, + () => { + this.setState({ scaleX: this.makeScaleX() + }) + }) } } @@ -128,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) + }) } /** @@ -164,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 }) } /** @@ -172,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) { @@ -245,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} />
@@ -265,43 +264,44 @@ 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) }} /> - - - */} + -
- ); + ) } } @@ -328,10 +328,9 @@ function mapStateToProps(state) { narratives: state.domain.narratives }, app: { - timerange: selectors.getTimeRange(state), selected: state.app.selected, language: state.app.language, - zoomLevels: state.app.zoomLevels, + timeline: state.app.timeline, narrative: state.app.narrative }, ui: { @@ -340,4 +339,4 @@ function mapStateToProps(state) { } } -export default connect(mapStateToProps)(Timeline); +export default connect(mapStateToProps)(Timeline) diff --git a/src/components/TimelineCategories.jsx b/src/components/TimelineCategories.jsx index 25dce29..6ecdad4 100644 --- a/src/components/TimelineCategories.jsx +++ b/src/components/TimelineCategories.jsx @@ -24,14 +24,10 @@ class TimelineCategories extends React.Component { } } - getY(idx) { - return (idx + 1) * this.props.dims.trackHeight / this.props.categories.length + 7.5; - } - renderCategory(category, idx) { const dims = this.props.dims; return ( - + {category.category} diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index 1596f29..9129ea6 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -1,15 +1,15 @@ -import React from 'react'; +import React from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' import * as actions from '../actions' import * as selectors from '../selectors' -import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; -import Search from './Search.jsx'; -import TagListPanel from './TagListPanel.jsx'; -import ToolbarBottomActions from './ToolbarBottomActions.jsx'; -import copy from '../js/data/copy.json'; -import { trimAndEllipse } from '../js/utilities.js'; +import { Tab, Tabs, TabList, TabPanel } from 'react-tabs' +import Search from './Search.jsx' +import TagListPanel from './TagListPanel.jsx' +import ToolbarBottomActions from './ToolbarBottomActions.jsx' +import copy from '../js/data/copy.json' +import { trimAndEllipse } from '../js/utilities.js' class Toolbar extends React.Component { constructor(props) { @@ -19,7 +19,7 @@ class Toolbar extends React.Component { selectTab(selected) { const _selected = (this.state._selected === selected) ? -1 : selected - this.setState({ _selected }); + this.setState({ _selected }) } renderClosePanel() { @@ -27,7 +27,7 @@ class Toolbar extends React.Component {
this.selectTab(-1)}>
- ); + ) } renderSearch() { @@ -49,7 +49,7 @@ class Toolbar extends React.Component { goToNarrative(narrative) { this.selectTab(-1) // set all unselected within this component - this.props.methods.onSelectNarrative(narrative); + this.props.methods.onSelectNarrative(narrative) } renderToolbarNarrativePanel() { @@ -68,7 +68,7 @@ class Toolbar extends React.Component { ) })} - ); + ) } renderToolbarTagPanel() { @@ -88,23 +88,23 @@ class Toolbar extends React.Component { ) } - return ''; + return '' } renderToolbarTab(_selected, label, icon_key) { - const isActive = (this.state._selected === _selected); - let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'; + const isActive = (this.state._selected === _selected) + let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab' return ( -
{ this.selectTab(_selected); }}> +
{ this.selectTab(_selected) }}> {icon_key}
{label}
- ); + ) } renderToolbarPanels() { - let classes = (this.state._selected >= 0) ? 'toolbar-panels' : 'toolbar-panels folded'; + let classes = (this.state._selected >= 0) ? 'toolbar-panels' : 'toolbar-panels folded' return (
{this.renderClosePanel()} @@ -119,25 +119,26 @@ class Toolbar extends React.Component { renderToolbarNavs() { if (this.props.narratives) { return this.props.narratives.map((nar, idx) => { - const isActive = (idx === this.state._selected); + const isActive = (idx === this.state._selected) - let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'; + let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab' return ( -
{ this.selectTab(idx); }}> +
{ this.selectTab(idx) }}>
{nar.label}
- ); + ) }) } - return ''; + return '' } renderToolbarTabs() { - const title = copy[this.props.language].toolbar.title; - const narratives_label = copy[this.props.language].toolbar.narratives_label; - const tags_label = copy[this.props.language].toolbar.tags_label; - const isTags = this.props.tags && this.props.tags.children; + let title = copy[this.props.language].toolbar.title + if (process.env.title) title = process.env.title + const narratives_label = copy[this.props.language].toolbar.narratives_label + const tags_label = copy[this.props.language].toolbar.tags_label + const isTags = this.props.tags && this.props.tags.children return (
@@ -165,7 +166,7 @@ class Toolbar extends React.Component { {this.renderToolbarTabs()} {this.renderToolbarPanels()}
- ); + ) } } diff --git a/src/components/ToolbarBottomActions.jsx b/src/components/ToolbarBottomActions.jsx index e20a4be..b35990d 100644 --- a/src/components/ToolbarBottomActions.jsx +++ b/src/components/ToolbarBottomActions.jsx @@ -13,10 +13,10 @@ function ToolbarBottomActions (props) { {/* onClick={(view) => this.toggleMapViews(view)} */} {/* isEnabled={this.props.viewFilters.routes} */} {/* /> */} - + {process.env.features.USE_SITES ? : null} {/* this.toggleMapViews(view)} */} {/* isEnabled={this.props.viewFilters.coevents} */} diff --git a/src/components/presentational/.DS_Store b/src/components/presentational/.DS_Store new file mode 100644 index 0000000..1e61c25 Binary files /dev/null and b/src/components/presentational/.DS_Store differ diff --git a/src/components/presentational/CardCaret.js b/src/components/presentational/Card/Caret.js similarity index 100% rename from src/components/presentational/CardCaret.js rename to src/components/presentational/Card/Caret.js diff --git a/src/components/presentational/CardCategory.js b/src/components/presentational/Card/Category.js similarity index 84% rename from src/components/presentational/CardCategory.js rename to src/components/presentational/Card/Category.js index 20b396d..ab6eb4c 100644 --- a/src/components/presentational/CardCategory.js +++ b/src/components/presentational/Card/Category.js @@ -1,6 +1,6 @@ import React from 'react'; -import { capitalizeFirstLetter } from '../../js/utilities.js'; +import { capitalizeFirstLetter } from '../../../js/utilities.js'; const CardCategory = ({ categoryTitle, categoryLabel, color }) => (
diff --git a/src/components/presentational/CardLocation.js b/src/components/presentational/Card/Location.js similarity index 84% rename from src/components/presentational/CardLocation.js rename to src/components/presentational/Card/Location.js index e9598fa..5025c5b 100644 --- a/src/components/presentational/CardLocation.js +++ b/src/components/presentational/Card/Location.js @@ -1,7 +1,7 @@ import React from 'react'; -import copy from '../../js/data/copy.json'; -import { isNotNullNorUndefined } from '../../js/utilities'; +import copy from '../../../js/data/copy.json'; +import { isNotNullNorUndefined } from '../../../js/utilities'; const CardLocation = ({ language, location }) => { diff --git a/src/components/presentational/CardNarrative.js b/src/components/presentational/Card/Narrative.js similarity index 86% rename from src/components/presentational/CardNarrative.js rename to src/components/presentational/Card/Narrative.js index 49f590d..adc88f2 100644 --- a/src/components/presentational/CardNarrative.js +++ b/src/components/presentational/Card/Narrative.js @@ -1,6 +1,6 @@ import React from 'react'; -import CardNarrativeLink from './CardNarrativeLink'; +import CardNarrativeLink from './NarrativeLink'; const CardNarrative = (props) => (
diff --git a/src/components/presentational/CardNarrativeLink.js b/src/components/presentational/Card/NarrativeLink.js similarity index 100% rename from src/components/presentational/CardNarrativeLink.js rename to src/components/presentational/Card/NarrativeLink.js diff --git a/src/components/presentational/CardSource.js b/src/components/presentational/Card/Source.js similarity index 96% rename from src/components/presentational/CardSource.js rename to src/components/presentational/Card/Source.js index 9b3da5b..7447ca6 100644 --- a/src/components/presentational/CardSource.js +++ b/src/components/presentational/Card/Source.js @@ -1,9 +1,9 @@ import React from 'react' import PropTypes from 'prop-types' -import Spinner from './Spinner' import Img from 'react-image' -import copy from '../../js/data/copy.json' +import Spinner from '../Spinner' +import copy from '../../../js/data/copy.json' const CardSource = ({ source, isLoading, onClickHandler }) => { function renderIconText(type) { diff --git a/src/components/presentational/CardSummary.js b/src/components/presentational/Card/Summary.js similarity index 88% rename from src/components/presentational/CardSummary.js rename to src/components/presentational/Card/Summary.js index 3e0cdbf..ac5bbd2 100644 --- a/src/components/presentational/CardSummary.js +++ b/src/components/presentational/Card/Summary.js @@ -1,6 +1,6 @@ import React from 'react'; -import copy from '../../js/data/copy.json'; +import copy from '../../../js/data/copy.json'; const CardSummary = ({ language, description, isHighlighted }) => { diff --git a/src/components/presentational/CardTags.js b/src/components/presentational/Card/Tags.js similarity index 94% rename from src/components/presentational/CardTags.js rename to src/components/presentational/Card/Tags.js index 6841cd4..2bc4cc4 100644 --- a/src/components/presentational/CardTags.js +++ b/src/components/presentational/Card/Tags.js @@ -1,6 +1,6 @@ import React from 'react'; -import copy from '../../js/data/copy.json'; +import copy from '../../../js/data/copy.json'; const CardTags = ({ tags, language }) => { const tags_lang = copy[language].cardstack.tags; diff --git a/src/components/presentational/CardTimestamp.js b/src/components/presentational/Card/Timestamp.js similarity index 87% rename from src/components/presentational/CardTimestamp.js rename to src/components/presentational/Card/Timestamp.js index 0317896..6ac69e6 100644 --- a/src/components/presentational/CardTimestamp.js +++ b/src/components/presentational/Card/Timestamp.js @@ -1,7 +1,7 @@ import React from 'react'; -import copy from '../../js/data/copy.json'; -import { isNotNullNorUndefined } from '../../js/utilities'; +import copy from '../../../js/data/copy.json'; +import { isNotNullNorUndefined } from '../../../js/utilities'; const CardTimestamp = ({ makeTimelabel, language, timestamp }) => { diff --git a/src/components/MapDefsMarkers.jsx b/src/components/presentational/Map/DefsMarkers.jsx similarity index 100% rename from src/components/MapDefsMarkers.jsx rename to src/components/presentational/Map/DefsMarkers.jsx diff --git a/src/components/MapEvents.jsx b/src/components/presentational/Map/Events.jsx similarity index 55% rename from src/components/MapEvents.jsx rename to src/components/presentational/Map/Events.jsx index 68c4fe4..39a7901 100644 --- a/src/components/MapEvents.jsx +++ b/src/components/presentational/Map/Events.jsx @@ -1,19 +1,10 @@ import React from 'react'; import { Portal } from 'react-portal'; -class MapEvents extends React.Component { - - projectPoint(location) { - const latLng = new L.LatLng(location[0], location[1]); - return { - x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX, - y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY - }; - } - - getLocationEventsDistribution(location) { +function MapEvents ({ getCategoryColor, categories, projectPoint, styleLocation, narrative, onSelect, svg, locations }){ + function getLocationEventsDistribution(location) { const eventCount = {}; - const categories = this.props.categories; + const categories = categories; categories.forEach(cat => { eventCount[cat.category] = []; @@ -26,7 +17,7 @@ class MapEvents extends React.Component { return eventCount; } - renderLocation(location) { + function renderLocation(location) { /** { events: [...], @@ -35,23 +26,23 @@ class MapEvents extends React.Component { longitude: '32.2' } */ - const { x, y } = this.projectPoint([location.latitude, location.longitude]); - // const eventsByCategory = this.getLocationEventsDistribution(location); + const { x, y } = projectPoint([location.latitude, location.longitude]); + // const eventsByCategory = getLocationEventsDistribution(location); const locCategory = location.events.length > 0 ? location.events[0].category : 'default' - const customStyles = this.props.styleLocation ? this.props.styleLocation(location) : null + const customStyles = styleLocation ? styleLocation(location) : null const extraStyles = customStyles[0] const extraRender = customStyles[1] const styles = ({ - fill: this.props.getCategoryColor(locCategory), + fill: getCategoryColor(locCategory), fillOpacity: 1, ...customStyles[0] }) // in narrative mode, only render events in narrative - if (this.props.narrative) { - const { steps } = this.props.narrative + if (narrative) { + const { steps } = narrative const onlyIfInNarrative = e => steps.map(s => s.id).includes(e.id) const eventsInNarrative = location.events.filter(onlyIfInNarrative) @@ -64,7 +55,7 @@ class MapEvents extends React.Component { this.props.onSelect(location.events)} + onClick={() => onSelect(location.events)} > - {this.props.locations.map(loc => this.renderLocation(loc))} - - ); - } + return ( + + {locations.map(renderLocation)} + + ); } export default MapEvents; diff --git a/src/components/MapNarratives.jsx b/src/components/presentational/Map/Narratives.jsx similarity index 50% rename from src/components/MapNarratives.jsx rename to src/components/presentational/Map/Narratives.jsx index 9d7248b..84452e8 100644 --- a/src/components/MapNarratives.jsx +++ b/src/components/presentational/Map/Narratives.jsx @@ -1,76 +1,67 @@ import React from 'react' import { Portal } from 'react-portal' -class MapNarratives extends React.Component { - - projectPoint(location) { - const latLng = new L.LatLng(location[0], location[1]) - return { - x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX, - y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY - } - } - - getNarrativeStyle(narrativeId) { - const styleName = (narrativeId && narrativeId in this.props.narrativeProps) +function MapNarratives ({ styles, onSelectNarrative, svg, narrative, narratives, projectPoint }) { + function getNarrativeStyle(narrativeId) { + const styleName = (narrativeId && narrativeId in styles) ? narrativeId : 'default' - return this.props.narrativeProps[styleName] + return styles[styleName] } - getStepStyle(name) { + function getStepStyle(name) { if (name === 'None') return null - return this.props.narrativeProps.stepStyles[name] + return styles.stepStyles[name] } - hasNoLocation(step) { + function hasNoLocation(step) { return (step.latitude === '' || step.longitude === '') } - renderNarrativeStep(idx, n) { + function renderNarrativeStep(idx, n) { const step = n.steps[idx] const step2 = n.steps[idx + 1] // don't draw if one of the steps has no location - if (this.hasNoLocation(step) || this.hasNoLocation(step2)) + if (hasNoLocation(step) || hasNoLocation(step2)) return null // 0 if not in narrative mode, 1 if active narrative, 0.1 if inactive let styles = { strokeOpacity: (n === null) ? 0 - : (step && (n.id === this.props.narrative.id)) ? 1 : 0.1, + : (step && (n.id === narrative.id)) ? 1 : 0.1, strokeWidth: 0, strokeDasharray: 'none', stroke: 'none' } - const p1 = this.projectPoint([step.latitude, step.longitude]) - const p2 = this.projectPoint([step2.latitude, step2.longitude]) + const p1 = projectPoint([step.latitude, step.longitude]) + const p2 = projectPoint([step2.latitude, step2.longitude]) if (step) { if (process.env.features.NARRATIVE_STEP_STYLES) { const _idx = step.narratives.indexOf(n.id) const stepStyle = step.narrative___stepStyles[_idx] - return this._renderNarrativeStep( + return _renderNarrativeStep( p1, p2, - { ...styles, ...this.getStepStyle(stepStyle) } + { ...styles, ...getStepStyle(stepStyle) } ) // otherwise steps are styled per narrative } else { styles = { ...styles, - ...this.getNarrativeStyle(n.id) + ...getNarrativeStyle(n.id) } - return this._renderNarrativeStep(p1,p2,styles) + return _renderNarrativeStep(p1,p2,styles) } } } - _renderNarrativeStep(p1, p2, styles) { + function _renderNarrativeStep(p1, p2, styles) { const { stroke, strokeWidth, strokeDasharray, strokeOpacity } = styles return ( this.props.onSelectNarrative(n)} + onClick={() => onSelectNarrative(n)} style={{ strokeWidth, strokeDasharray, @@ -93,25 +84,23 @@ class MapNarratives extends React.Component { } - renderNarrative(n) { + function renderNarrative(n) { const steps = n.steps.slice(0, n.steps.length - 1) return ( - {steps.map((s, idx) => this.renderNarrativeStep(idx, n))} + {steps.map((s, idx) => renderNarrativeStep(idx, n))} ) } - render() { - if (this.props.narrative === null) return (
) + if (narrative === null) return (
) - return ( - - {this.props.narratives.map(n => this.renderNarrative(n))} - - ) - } + return ( + + {narratives.map(n => renderNarrative(n))} + + ) } export default MapNarratives diff --git a/src/components/MapSelectedEvents.jsx b/src/components/presentational/Map/SelectedEvents.jsx similarity index 66% rename from src/components/MapSelectedEvents.jsx rename to src/components/presentational/Map/SelectedEvents.jsx index 8c48bc8..88c3036 100644 --- a/src/components/MapSelectedEvents.jsx +++ b/src/components/presentational/Map/SelectedEvents.jsx @@ -2,17 +2,8 @@ import React from 'react'; import { Portal } from 'react-portal'; class MapSelectedEvents extends React.Component { - - projectPoint(location) { - const latLng = new L.LatLng(location[0], location[1]); - return { - x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX, - y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY - }; - } - renderMarker (event) { - const { x, y } = this.projectPoint([event.latitude, event.longitude]); + const { x, y } = this.props.projectPoint([event.latitude, event.longitude]); return ( { + if (idx < shape.points.length - 1) { + const p2 = points[idx+1] + lineCoords.push({ + x1: p1.x, + y1: p1.y, + x2: p2.x, + y2: p2.y + }) + } + }) + + return lineCoords.map(coords => { + const shapeStyles = (shape.name in styles) + ? styles[shape.name] + : styles.default + + return ( + + + ) + }) + } + + if (!shapes || !shapes.length) return null + + return ( + + + {shapes.map(renderShape)} + + + ) + +} + +export default MapShapes diff --git a/src/components/presentational/Map/Sites.jsx b/src/components/presentational/Map/Sites.jsx new file mode 100644 index 0000000..9d49885 --- /dev/null +++ b/src/components/presentational/Map/Sites.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +function MapSites({ sites, projectPoint }) { + function renderSite(site) { + const { x, y } = projectPoint([site.latitude, site.longitude]); + + return (
+ {site.site} +
+ ); + } + + if (!sites || !sites.length) return null; + + return ( +
+ {sites.map(renderSite)} +
+ ) + +} + +export default MapSites; diff --git a/src/components/presentational/NarrativeAdjust.js b/src/components/presentational/Narrative/Adjust.js similarity index 100% rename from src/components/presentational/NarrativeAdjust.js rename to src/components/presentational/Narrative/Adjust.js diff --git a/src/components/presentational/NarrativeCard.js b/src/components/presentational/Narrative/Card.js similarity index 94% rename from src/components/presentational/NarrativeCard.js rename to src/components/presentational/Narrative/Card.js index b47e10c..f71a28f 100644 --- a/src/components/presentational/NarrativeCard.js +++ b/src/components/presentational/Narrative/Card.js @@ -1,6 +1,6 @@ import React from 'react' import { connect } from 'react-redux' -import { selectActiveNarrative } from '../../selectors' +import { selectActiveNarrative } from '../../../selectors' function NarrativeCard ({ narrative }) { // no display if no narrative diff --git a/src/components/presentational/NarrativeClose.js b/src/components/presentational/Narrative/Close.js similarity index 100% rename from src/components/presentational/NarrativeClose.js rename to src/components/presentational/Narrative/Close.js diff --git a/src/components/presentational/NarrativeControls.js b/src/components/presentational/Narrative/Controls.js similarity index 71% rename from src/components/presentational/NarrativeControls.js rename to src/components/presentational/Narrative/Controls.js index 5a120dd..f80bf63 100644 --- a/src/components/presentational/NarrativeControls.js +++ b/src/components/presentational/Narrative/Controls.js @@ -1,7 +1,7 @@ import React from 'react' -import NarrativeCard from './NarrativeCard' -import NarrativeAdjust from './NarrativeAdjust' -import NarrativeClose from './NarrativeClose' +import Card from './Card' +import Adjust from './Adjust' +import Close from './Close' export default ({ narrative, methods }) => { if (!narrative) return null @@ -12,18 +12,18 @@ export default ({ narrative, methods }) => { return ( - - + - - methods.onSelectNarrative(null)} closeMsg='-- exit from narrative --' /> diff --git a/src/components/presentational/TimelineClip.js b/src/components/presentational/Timeline/Clip.js similarity index 87% rename from src/components/presentational/TimelineClip.js rename to src/components/presentational/Timeline/Clip.js index 29491da..11f4c79 100644 --- a/src/components/presentational/TimelineClip.js +++ b/src/components/presentational/Timeline/Clip.js @@ -3,8 +3,8 @@ import React from 'react'; const TimelineClip = ({ dims }) => ( diff --git a/src/components/presentational/DatetimeDot.js b/src/components/presentational/Timeline/DatetimeDot.js similarity index 100% rename from src/components/presentational/DatetimeDot.js rename to src/components/presentational/Timeline/DatetimeDot.js diff --git a/src/components/presentational/TimelineEvents.js b/src/components/presentational/Timeline/Events.js similarity index 100% rename from src/components/presentational/TimelineEvents.js rename to src/components/presentational/Timeline/Events.js diff --git a/src/components/presentational/TimelineHandles.js b/src/components/presentational/Timeline/Handles.js similarity index 86% rename from src/components/presentational/TimelineHandles.js rename to src/components/presentational/Timeline/Handles.js index 3e65f9b..12e9da9 100644 --- a/src/components/presentational/TimelineHandles.js +++ b/src/components/presentational/Timeline/Handles.js @@ -5,14 +5,14 @@ const TimelineHandles = ({ dims, onMoveTime }) => { return ( onMoveTime('backwards')} > onMoveTime('forward')} > @@ -23,4 +23,4 @@ const TimelineHandles = ({ dims, onMoveTime }) => { } -export default TimelineHandles; \ No newline at end of file +export default TimelineHandles; diff --git a/src/components/presentational/TimelineHeader.js b/src/components/presentational/Timeline/Header.js similarity index 100% rename from src/components/presentational/TimelineHeader.js rename to src/components/presentational/Timeline/Header.js diff --git a/src/components/presentational/Timeline/Labels.js b/src/components/presentational/Timeline/Labels.js new file mode 100644 index 0000000..60e260e --- /dev/null +++ b/src/components/presentational/Timeline/Labels.js @@ -0,0 +1,44 @@ +import React from 'react'; + +import { formatterWithYear } from '../../../js/utilities.js'; + +const TimelineLabels = ({ dims, timelabels }) => { + + return ( + + + + + + + {formatterWithYear(timelabels[0])} + + + {formatterWithYear(timelabels[1])} + + + ) +} + +export default TimelineLabels; diff --git a/src/components/presentational/TimelineMarkers.js b/src/components/presentational/Timeline/Markers.js similarity index 100% rename from src/components/presentational/TimelineMarkers.js rename to src/components/presentational/Timeline/Markers.js diff --git a/src/components/presentational/TimelineZoomControls.js b/src/components/presentational/Timeline/ZoomControls.js similarity index 100% rename from src/components/presentational/TimelineZoomControls.js rename to src/components/presentational/Timeline/ZoomControls.js diff --git a/src/components/presentational/TimelineLabels.js b/src/components/presentational/TimelineLabels.js deleted file mode 100644 index 36466ba..0000000 --- a/src/components/presentational/TimelineLabels.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; - -import { formatterWithYear } from '../../js/utilities.js'; - -const TimelineLabels = ({ dims, timelabels }) => { - - return ( - - - - - - {/* */} - {/* {formatterWithYear(timelabels[0])} */} - {/* */} - {/* */} - {/* {formatterWithYear(timelabels[1])} */} - {/* */} - - ) -} - -export default TimelineLabels; diff --git a/src/reducers/app.js b/src/reducers/app.js index 43e39aa..bc79b05 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] @@ -156,11 +156,13 @@ function updateCategoryFilters(appState, action) { } function updateTimeRange(appState, action) { // XXX - return Object.assign({}, appState, { - filters: Object.assign({}, appState.filters, { - timerange: action.timerange - }), - }) + return { + ...appState, + timeline: { + ...appState.timeline, + range: action.timerange + }, + } } function resetAllFilters(appState) { // XXX diff --git a/src/reducers/schema/shapeSchema.js b/src/reducers/schema/shapeSchema.js new file mode 100644 index 0000000..d02079f --- /dev/null +++ b/src/reducers/schema/shapeSchema.js @@ -0,0 +1,8 @@ +import Joi from 'joi' + +const shapeSchema = Joi.object().keys({ + name: Joi.string().required(), + items: Joi.array().required() +}) + +export default shapeSchema diff --git a/src/reducers/utils/validators.js b/src/reducers/utils/validators.js index 42bf3d3..cbd13fd 100644 --- a/src/reducers/utils/validators.js +++ b/src/reducers/utils/validators.js @@ -1,12 +1,13 @@ -import Joi from 'joi'; +import Joi from 'joi' -import eventSchema from '../schema/eventSchema'; -import categorySchema from '../schema/categorySchema'; -import siteSchema from '../schema/siteSchema'; -import narrativeSchema from '../schema/narrativeSchema'; +import eventSchema from '../schema/eventSchema' +import categorySchema from '../schema/categorySchema' +import siteSchema from '../schema/siteSchema' +import narrativeSchema from '../schema/narrativeSchema' import sourceSchema from '../schema/sourceSchema' +import shapeSchema from '../schema/shapeSchema' -import { capitalize } from './helpers.js'; +import { capitalize } from './helpers.js' /* * Create an error notification object @@ -21,8 +22,8 @@ function makeError(type, id, message) { } -const isLeaf = node => (Object.keys(node.children).length === 0); -const isDuplicate = (node, set) => { return (set.has(node.key)); }; +const isLeaf = node => (Object.keys(node.children).length === 0) +const isDuplicate = (node, set) => { return (set.has(node.key)) } /* @@ -61,8 +62,9 @@ export function validateDomain (domain) { sites: [], narratives: [], sources: {}, + tags: {}, + shapes: [], notifications: domain.notifications, - tags: {} } const discardedDomain = { @@ -71,18 +73,19 @@ export function validateDomain (domain) { sites: [], narratives: [], sources: [], + shapes: [] } function validateArrayItem(item, domainKey, schema) { - const result = Joi.validate(item, schema); + const result = Joi.validate(item, schema) if (result.error !== null) { - const id = item.id || '-'; - const domainStr = capitalize(domainKey); - const error = makeError(domainStr, id, result.error.message); + const id = item.id || '-' + const domainStr = capitalize(domainKey) + const error = makeError(domainStr, id, result.error.message) - discardedDomain[domainKey].push(Object.assign(item, { error })); + discardedDomain[domainKey].push(Object.assign(item, { error })) } else { - sanitizedDomain[domainKey].push(item); + sanitizedDomain[domainKey].push(item) } } @@ -109,29 +112,38 @@ export function validateDomain (domain) { }) } - validateArray(domain.events, 'events', eventSchema); - validateArray(domain.categories, 'categories', categorySchema); - validateArray(domain.sites, 'sites', siteSchema); - validateArray(domain.narratives, 'narratives', narrativeSchema); - validateObject(domain.sources, 'sources', sourceSchema); + validateArray(domain.events, 'events', eventSchema) + validateArray(domain.categories, 'categories', categorySchema) + validateArray(domain.sites, 'sites', siteSchema) + validateArray(domain.narratives, 'narratives', narrativeSchema) + validateObject(domain.sources, 'sources', sourceSchema) + validateObject(domain.shapes, 'shapes', shapeSchema) + // NB: [lat, lon] array is best format for projecting into map + sanitizedDomain.shapes = sanitizedDomain.shapes.map(shape => ({ + name: shape.name, + points: shape.items.map(coords => ( + coords.replace(/\s/g, '').split(',') + )) + }) + ) // Message the number of failed items in domain Object.keys(discardedDomain).forEach(disc => { - const len = discardedDomain[disc].length; + const len = discardedDomain[disc].length if (len) { sanitizedDomain.notifications.push({ message: `${len} invalid ${disc} not displayed.`, items: discardedDomain[disc], type: 'error' - }); + }) } - }); + }) // Validate uniqueness of tags - const tagSet = new Set([]); - const duplicateTags = []; - validateTree(domain.tags, {}, tagSet, duplicateTags); + const tagSet = new Set([]) + const duplicateTags = [] + validateTree(domain.tags, {}, tagSet, duplicateTags) // Duplicated tags if (duplicateTags.length > 0) { @@ -139,9 +151,9 @@ export function validateDomain (domain) { message: `Tags are required to be unique. Ignoring duplicates for now.`, items: duplicateTags, type: 'error' - }); + }) } - sanitizedDomain.tags = domain.tags; + sanitizedDomain.tags = domain.tags - return sanitizedDomain; + return sanitizedDomain } diff --git a/src/scss/map.scss b/src/scss/map.scss index a1d90b2..054895e 100644 --- a/src/scss/map.scss +++ b/src/scss/map.scss @@ -16,11 +16,11 @@ .leaflet-container { height: 100%; - img.leaflet-tile { - -webkit-filter: contrast(120%) brightness(115%) grayscale(95%); /* Webkit */ - filter: gray; /* IE6-9 */ - filter: contrast(120%) brightness(115%) grayscale(95%); /* W3C */ - } + // img.leaflet-tile { + // -webkit-filter: contrast(120%) brightness(115%) grayscale(95%); /* Webkit */ + // filter: gray; /* IE6-9 */ + // filter: contrast(120%) brightness(115%) grayscale(95%); /* W3C */ + // } } &.hidden { @@ -65,14 +65,14 @@ } } - .sites-layer { + .sites-layer, .shapes-layer { position: fixed; top: 0px; left: 110px; } &.narrative-mode { - .sites-layer { + .sites-layer, .shapes-layer { position: fixed; top: 0px; left: 0px; diff --git a/src/selectors/index.js b/src/selectors/index.js index 53cae3f..6494b4a 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -15,13 +15,17 @@ export const getSites = (state) => { } export const getSources = state => { if (process.env.features.USE_SOURCES) return state.domain.sources + return {} +} +export const getShapes = state => { + if (process.env.features.USE_SHAPES) return state.domain.shapes return [] } 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 9cb4a2a..32e5c6f 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -39,32 +39,48 @@ 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], - zoomLevels: [ - { label: '3 years', duration: 1576800 }, - { label: '3 months', duration: 129600 }, - { label: '3 days', duration: 4320 }, - { label: '12 hours', duration: 720 }, - { label: '2 hours', duration: 120 }, - { label: '30 min', duration: 30 }, - { label: '10 min', duration: 10 } - ], + map: { + anchor: [31.356397, 34.784818], + startZoom: 11, + minZoom: 7, + maxZoom: 18, + bounds: null, + maxBounds: [[180, -180], [-180, 180]] + }, + timeline: { + dimensions: { + height: 140, + width: 0, + width_controls: 100, + height_controls: 115, + margin_left: 200, + 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 }, + { label: '3 days', duration: 4320 }, + { label: '12 hours', duration: 720 }, + { label: '2 hours', duration: 120 }, + { label: '30 min', duration: 30 }, + { label: '10 min', duration: 10 } + ], + }, flags: { isFetchingDomain: false, isFetchingSources: false, @@ -81,6 +97,7 @@ const initial = { * as well as dom elements to attach SVG */ ui: { + tiles: 'openstreetmap', // ['openstreetmap', 'streets', 'satellite'] style: { categories: { default: '#f3de2c', @@ -91,6 +108,13 @@ const initial = { stroke: 'red', strokeWidth: 3 } + }, + shapes: { + default: { + stroke: 'blue', + strokeWidth: 3, + opacity: 0.9 + } } }, dom: { @@ -109,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