Merge pull request #89 from forensic-architecture/topic/shapes-support

Topic/shapes support
This commit is contained in:
Lachlan Kermode
2019-01-18 18:13:24 +00:00
committed by GitHub
48 changed files with 607 additions and 496 deletions

2
.gitignore vendored
View File

@@ -2,3 +2,5 @@ build/
node_modules/ node_modules/
config.js config.js
dev.config.js dev.config.js
src/\.DS_Store

View File

@@ -7,7 +7,7 @@ module.exports = {
SOURCES_EXT: '/api/example/export_sources/deepids', SOURCES_EXT: '/api/example/export_sources/deepids',
TAGS_EXT: '/api/example/export_tags/tree', TAGS_EXT: '/api/example/export_tags/tree',
SITES_EXT: '/api/example/export_sites/rows', 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', INCOMING_DATETIME_FORMAT: '%m/%d/%YT%H:%M',
MAPBOX_TOKEN: 'pk.EXAMPLE_MAPBOX_TOKEN', MAPBOX_TOKEN: 'pk.EXAMPLE_MAPBOX_TOKEN',
features: { features: {
@@ -15,7 +15,26 @@ module.exports = {
USE_SEARCH: false, USE_SEARCH: false,
USE_SITES: true, USE_SITES: true,
USE_SOURCES: true, USE_SOURCES: true,
USE_SHAPES: true,
CATEGORIES_AS_TAGS: 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: {}
}
}
} }
} }

View File

@@ -1,12 +1,14 @@
import { urlFromEnv } from '../js/utilities' import { urlFromEnv } from '../js/utilities'
const EVENT_DATA_URL = urlFromEnv('EVENT_EXT'); // TODO: relegate these URLs entirely to environment variables
const CATEGORY_URL = urlFromEnv('CATEGORY_EXT'); const EVENT_DATA_URL = urlFromEnv('EVENT_EXT')
const TAGS_URL = urlFromEnv('TAGS_EXT'); const CATEGORY_URL = urlFromEnv('CATEGORY_EXT')
const SOURCES_URL = urlFromEnv('SOURCES_EXT'); const TAGS_URL = urlFromEnv('TAGS_EXT')
const NARRATIVE_URL = urlFromEnv('NARRATIVE_EXT'); const SOURCES_URL = urlFromEnv('SOURCES_EXT')
const SITES_URL = urlFromEnv('SITES_EXT'); const NARRATIVE_URL = urlFromEnv('NARRATIVE_EXT')
const eventUrlMap = (event) => `${process.env.SERVER_ROOT}${process.env.EVENT_DESC_ROOT}/${(event.id) ? event.id : event}`; 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.` 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([ return Promise.all([
eventPromise, eventPromise,
catPromise, catPromise,
narPromise, narPromise,
sitesPromise, sitesPromise,
tagsPromise, tagsPromise,
sourcesPromise sourcesPromise,
shapesPromise
]) ])
.then(response => { .then(response => {
const result = { const result = {
@@ -83,6 +93,7 @@ export function fetchDomain () {
sites: response[3], sites: response[3],
tags: response[4], tags: response[4],
sources: response[5], sources: response[5],
shapes: response[6],
notifications notifications
} }
if (Object.values(result).some(resp => resp.hasOwnProperty('error'))) { if (Object.values(result).some(resp => resp.hasOwnProperty('error'))) {
@@ -96,7 +107,7 @@ export function fetchDomain () {
// TODO: handle this appropriately in React hierarchy // TODO: handle this appropriately in React hierarchy
alert(err.message) alert(err.message)
}) })
}; }
} }
export const FETCH_ERROR = 'FETCH_ERROR' 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) { export function updateNarrative(narrative) {
return { return {
type: UPDATE_NARRATIVE, 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() { export function incrementNarrativeCurrent() {
return { return {
type: INCREMENT_NARRATIVE_CURRENT type: INCREMENT_NARRATIVE_CURRENT
} }
} }
export const DECREMENT_NARRATIVE_CURRENT = 'DECREMENT_NARRATIVE_CURRENT'; export const DECREMENT_NARRATIVE_CURRENT = 'DECREMENT_NARRATIVE_CURRENT'
export function decrementNarrativeCurrent() { export function decrementNarrativeCurrent() {
return { return {
type: DECREMENT_NARRATIVE_CURRENT 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) { export function toggleLanguage(language) {
return { return {
type: TOGGLE_LANGUAGE, 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() { export function closeToolbar() {
return { return {
type: CLOSE_TOOLBAR type: CLOSE_TOOLBAR
} }
} }
export const TOGGLE_INFOPOPUP = 'TOGGLE_INFOPOPUP'; export const TOGGLE_INFOPOPUP = 'TOGGLE_INFOPOPUP'
export function toggleInfoPopup() { export function toggleInfoPopup() {
return { return {
type: TOGGLE_INFOPOPUP type: TOGGLE_INFOPOPUP
} }
} }
export const TOGGLE_MAPVIEW = 'TOGGLE_MAPVIEW'; export const TOGGLE_MAPVIEW = 'TOGGLE_MAPVIEW'
export function toggleMapView(layer) { export function toggleMapView(layer) {
return { return {
type: TOGGLE_MAPVIEW, type: TOGGLE_MAPVIEW,

BIN
src/components/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -7,14 +7,14 @@ import {
import React from 'react' import React from 'react'
import Spinner from './presentational/Spinner' import Spinner from './presentational/Spinner'
import CardTimestamp from './presentational/CardTimestamp' import CardTimestamp from './presentational/Card/Timestamp'
import CardLocation from './presentational/CardLocation' import CardLocation from './presentational/Card/Location'
import CardCaret from './presentational/CardCaret' import CardCaret from './presentational/Card/Caret'
import CardTags from './presentational/CardTags' import CardTags from './presentational/Card/Tags'
import CardSummary from './presentational/CardSummary' import CardSummary from './presentational/Card/Summary'
import CardSource from './presentational/CardSource' import CardSource from './presentational/Card/Source'
import CardCategory from './presentational/CardCategory' import CardCategory from './presentational/Card/Category'
import CardNarrative from './presentational/CardNarrative' import CardNarrative from './presentational/Card/Narrative'
class Card extends React.Component { class Card extends React.Component {

View File

@@ -9,7 +9,7 @@ import LoadingOverlay from './presentational/LoadingOverlay'
import Map from './Map.jsx' import Map from './Map.jsx'
import Toolbar from './Toolbar.jsx' import Toolbar from './Toolbar.jsx'
import CardStack from './CardStack.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 InfoPopUp from './InfoPopup.jsx'
import Timeline from './Timeline.jsx' import Timeline from './Timeline.jsx'
import Notification from './Notification.jsx' import Notification from './Notification.jsx'

View File

@@ -1,26 +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 MapSites from './MapSites.jsx'; import Sites from './presentational/Map/Sites.jsx'
import MapEvents from './MapEvents.jsx'; import Shapes from './presentational/Map/Shapes.jsx'
import MapSelectedEvents from './MapSelectedEvents.jsx'; import Events from './presentational/Map/Events.jsx'
import MapNarratives from './MapNarratives.jsx'; import SelectedEvents from './presentational/Map/SelectedEvents.jsx'
import MapDefsMarkers from './MapDefsMarkers.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 { class Map extends React.Component {
constructor() { constructor() {
super(); super()
this.svgRef = React.createRef(); this.projectPoint = this.projectPoint.bind(this)
this.map = null; this.svgRef = React.createRef()
this.map = null
this.state = { this.state = {
mapTransformX: 0, mapTransformX: 0,
mapTransformY: 0 mapTransformY: 0
@@ -30,76 +35,76 @@ 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])
} }
} }
} }
} }
initializeMap() { initializeMap() {
/** /**
* 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/styles/v1/${this.props.ui.tiles}/tiles/{z}/{x}/{y}?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({
@@ -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() { 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,
@@ -118,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}>
@@ -132,35 +145,42 @@ class Map extends React.Component {
> >
</svg> </svg>
</Portal> </Portal>
); )
} }
renderSites() { renderSites() {
return ( return (
<MapSites <Sites
sites={this.props.domain.sites} sites={this.props.domain.sites}
map={this.map} projectPoint={this.projectPoint}
mapTransformX={this.state.mapTransformX}
mapTransformY={this.state.mapTransformY}
isEnabled={this.props.app.views.sites} isEnabled={this.props.app.views.sites}
/> />
); )
}
renderShapes() {
return (
<Shapes
svg={this.svgRef.current}
shapes={this.props.domain.shapes}
projectPoint={this.projectPoint}
styles={this.props.ui.shapes}
/>
)
} }
renderNarratives() { renderNarratives() {
return ( return (
<MapNarratives <Narratives
svg={this.svgRef.current} svg={this.svgRef.current}
narratives={this.props.domain.narratives} narratives={this.props.domain.narratives}
map={this.map} projectPoint={this.projectPoint}
mapTransformX={this.state.mapTransformX}
mapTransformY={this.state.mapTransformY}
narrative={this.props.app.narrative} narrative={this.props.app.narrative}
narrativeProps={this.props.ui.narratives} styles={this.props.ui.narratives}
onSelect={this.props.methods.onSelect} onSelect={this.props.methods.onSelect}
onSelectNarrative={this.props.methods.onSelectNarrative} onSelectNarrative={this.props.methods.onSelectNarrative}
/> />
); )
} }
/** /**
@@ -182,39 +202,35 @@ class Map extends React.Component {
renderEvents() { renderEvents() {
return ( return (
<MapEvents <Events
svg={this.svgRef.current} svg={this.svgRef.current}
locations={this.props.domain.locations} locations={this.props.domain.locations}
styleLocation={this.styleLocation} styleLocation={this.styleLocation}
categories={this.props.domain.categories} categories={this.props.domain.categories}
map={this.map} projectPoint={this.projectPoint}
mapTransformX={this.state.mapTransformX}
mapTransformY={this.state.mapTransformY}
narrative={this.props.app.narrative} narrative={this.props.app.narrative}
onSelect={this.props.methods.onSelect} onSelect={this.props.methods.onSelect}
onSelectNarrative={this.props.methods.onSelectNarrative} onSelectNarrative={this.props.methods.onSelectNarrative}
getCategoryColor={this.props.methods.getCategoryColor} getCategoryColor={this.props.methods.getCategoryColor}
/> />
); )
} }
renderSelected() { renderSelected() {
return ( return (
<MapSelectedEvents <SelectedEvents
svg={this.svgRef.current} svg={this.svgRef.current}
selected={this.props.app.selected} selected={this.props.app.selected}
map={this.map} projectPoint={this.projectPoint}
mapTransformX={this.state.mapTransformX}
mapTransformY={this.state.mapTransformY}
/> />
); )
} }
renderMarkers() { renderMarkers() {
return ( return (
<Portal node={this.svgRef.current}> <Portal node={this.svgRef.current}>
<MapDefsMarkers /> <DefsMarkers />
</Portal> </Portal>
) )
} }
@@ -222,19 +238,25 @@ 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 ? (
<React.Fragment>
{this.renderTiles()}
{this.renderMarkers()}
{isShowingSites ? this.renderSites() : null}
{this.renderShapes()}
{this.renderEvents()}
{this.renderNarratives()}
{this.renderSelected()}
</React.Fragment>
) : null
return ( return (
<div className={classes}> <div className={classes}>
<div id={this.props.ui.dom.map} /> <div id={this.props.ui.dom.map} />
{(this.map !== null) ? this.renderTiles() : ''} {innerMap}
{(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() : ''}
</div> </div>
); )
} }
} }
@@ -244,22 +266,24 @@ function mapStateToProps(state) {
locations: selectors.selectLocations(state), locations: selectors.selectLocations(state),
narratives: selectors.selectNarratives(state), narratives: selectors.selectNarratives(state),
categories: selectors.selectCategories(state), categories: selectors.selectCategories(state),
sites: selectors.getSites(state) sites: selectors.getSites(state),
shapes: selectors.getShapes(state)
}, },
app: { app: {
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
} }
} }
} }

View File

@@ -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 (<div
className="leaflet-tooltip site-label leaflet-zoom-animated leaflet-tooltip-top"
style={{ opacity: 1, transform: `translate3d(calc(${x}px - 50%), ${y - 25}px, 0px)`}}>
{site.site}
</div>
);
}
render () {
if (!this.props.sites || !this.props.sites.length) return <div />;
return (
<div className="sites-layer">
{this.props.sites.map(site => { return this.renderSite(site); })}
</div>
)
}
}
export default MapSites;

View File

@@ -145,7 +145,7 @@ class SourceOverlay extends React.Component {
<div className="back" onClick={() => this.onShiftGallery(-1)}><svg><path d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z"></path></svg></div> <div className="back" onClick={() => this.onShiftGallery(-1)}><svg><path d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z"></path></svg></div>
<div className="next" onClick={() => this.onShiftGallery(1)}><svg><path d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z"></path></svg></div> <div className="next" onClick={() => this.onShiftGallery(1)}><svg><path d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z"></path></svg></div>
</div> </div>
); );
} }
return ( return (
<div className="media-gallery-controls"></div> <div className="media-gallery-controls"></div>
@@ -175,7 +175,7 @@ class SourceOverlay extends React.Component {
</div> </div>
<div className="mo-meta-container"> <div className="mo-meta-container">
<div className="mo-box-title"> <div className="mo-box-title">
<p>{`${this.state.idx+1} / ${paths.length}`}</p> {/* <p>{`${this.state.idx+1} / ${paths.length}`}</p> */}
{title? <p><b>{title}</b></p> : null} {title? <p><b>{title}</b></p> : null}
<div>{desc}</div> <div>{desc}</div>
</div> </div>

View File

@@ -1,98 +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/TimelineHeader'; import Header from './presentational/Timeline/Header'
import TimelineAxis from './TimelineAxis.jsx'; import Axis from './TimelineAxis.jsx'
import TimelineClip from './presentational/TimelineClip'; import Clip from './presentational/Timeline/Clip'
import TimelineHandles from './presentational/TimelineHandles.js'; import Handles from './presentational/Timeline/Handles.js'
import TimelineZoomControls from './presentational/TimelineZoomControls.js'; import ZoomControls from './presentational/Timeline/ZoomControls.js'
import TimelineLabels from './presentational/TimelineLabels.js'; import Labels from './presentational/Timeline/Labels.js'
import TimelineMarkers from './presentational/TimelineMarkers.js' import Markers from './presentational/Timeline/Markers.js'
import TimelineEvents from './presentational/TimelineEvents.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: { dims: props.app.timeline.dimensions,
height: 140,
width: 0,
width_controls: 100,
height_controls: 115,
margin_left: 120,
margin_top: 20,
trackHeight: 80
},
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() })
} }
} }
@@ -101,25 +93,31 @@ 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: Object.assign({}, this.state.dims, { width: boundingClient.width }) dims: {
}, () => { this.setState({ scaleX: this.makeScaleX() }) ...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' * @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)
}); })
} }
/** /**
@@ -164,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 })
} }
/** /**
@@ -172,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) {
@@ -245,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">
@@ -265,43 +264,44 @@ 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}
getCategoryY={this.state.scaleY}
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}
@@ -315,7 +315,7 @@ class Timeline extends React.Component {
</div> </div>
</div> </div>
</div> </div>
); )
} }
} }
@@ -328,10 +328,9 @@ function mapStateToProps(state) {
narratives: state.domain.narratives narratives: state.domain.narratives
}, },
app: { app: {
timerange: selectors.getTimeRange(state),
selected: state.app.selected, selected: state.app.selected,
language: state.app.language, language: state.app.language,
zoomLevels: state.app.zoomLevels, timeline: state.app.timeline,
narrative: state.app.narrative narrative: state.app.narrative
}, },
ui: { ui: {
@@ -340,4 +339,4 @@ function mapStateToProps(state) {
} }
} }
export default connect(mapStateToProps)(Timeline); export default connect(mapStateToProps)(Timeline)

View File

@@ -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) { renderCategory(category, idx) {
const dims = this.props.dims; const dims = this.props.dims;
return ( return (
<g class="tick" opacity="1" transform={`translate(0,${this.getY(idx)})`}> <g class="tick" opacity="1" transform={`translate(0,${this.props.getCategoryY(category.category)})`}>
<line x1={dims.margin_left} x2={dims.width - dims.width_controls}></line> <line x1={dims.margin_left} x2={dims.width - dims.width_controls}></line>
<text x={dims.margin_left - 5} dy="0.32em">{category.category}</text> <text x={dims.margin_left - 5} dy="0.32em">{category.category}</text>
</g> </g>

View File

@@ -1,15 +1,15 @@
import React from 'react'; import React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import * as actions from '../actions' import * as actions from '../actions'
import * as selectors from '../selectors' import * as selectors from '../selectors'
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'
import Search from './Search.jsx'; import Search from './Search.jsx'
import TagListPanel from './TagListPanel.jsx'; import TagListPanel from './TagListPanel.jsx'
import ToolbarBottomActions from './ToolbarBottomActions.jsx'; import ToolbarBottomActions from './ToolbarBottomActions.jsx'
import copy from '../js/data/copy.json'; import copy from '../js/data/copy.json'
import { trimAndEllipse } from '../js/utilities.js'; import { trimAndEllipse } from '../js/utilities.js'
class Toolbar extends React.Component { class Toolbar extends React.Component {
constructor(props) { constructor(props) {
@@ -19,7 +19,7 @@ class Toolbar extends React.Component {
selectTab(selected) { selectTab(selected) {
const _selected = (this.state._selected === selected) ? -1 : selected const _selected = (this.state._selected === selected) ? -1 : selected
this.setState({ _selected }); this.setState({ _selected })
} }
renderClosePanel() { renderClosePanel() {
@@ -27,7 +27,7 @@ class Toolbar extends React.Component {
<div className="panel-header" onClick={() => this.selectTab(-1)}> <div className="panel-header" onClick={() => this.selectTab(-1)}>
<div className="caret"></div> <div className="caret"></div>
</div> </div>
); )
} }
renderSearch() { renderSearch() {
@@ -49,7 +49,7 @@ class Toolbar extends React.Component {
goToNarrative(narrative) { goToNarrative(narrative) {
this.selectTab(-1) // set all unselected within this component this.selectTab(-1) // set all unselected within this component
this.props.methods.onSelectNarrative(narrative); this.props.methods.onSelectNarrative(narrative)
} }
renderToolbarNarrativePanel() { renderToolbarNarrativePanel() {
@@ -68,7 +68,7 @@ class Toolbar extends React.Component {
) )
})} })}
</TabPanel> </TabPanel>
); )
} }
renderToolbarTagPanel() { renderToolbarTagPanel() {
@@ -88,23 +88,23 @@ class Toolbar extends React.Component {
</TabPanel> </TabPanel>
) )
} }
return ''; return ''
} }
renderToolbarTab(_selected, label, icon_key) { renderToolbarTab(_selected, label, icon_key) {
const isActive = (this.state._selected === _selected); const isActive = (this.state._selected === _selected)
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'; let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab'
return ( return (
<div className={classes} onClick={() => { this.selectTab(_selected); }}> <div className={classes} onClick={() => { this.selectTab(_selected) }}>
<i className="material-icons">{icon_key}</i> <i className="material-icons">{icon_key}</i>
<div className="tab-caption">{label}</div> <div className="tab-caption">{label}</div>
</div> </div>
); )
} }
renderToolbarPanels() { renderToolbarPanels() {
let classes = (this.state._selected >= 0) ? 'toolbar-panels' : 'toolbar-panels folded'; let classes = (this.state._selected >= 0) ? 'toolbar-panels' : 'toolbar-panels folded'
return ( return (
<div className={classes}> <div className={classes}>
{this.renderClosePanel()} {this.renderClosePanel()}
@@ -119,25 +119,26 @@ class Toolbar extends React.Component {
renderToolbarNavs() { renderToolbarNavs() {
if (this.props.narratives) { if (this.props.narratives) {
return this.props.narratives.map((nar, idx) => { 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 ( return (
<div className={classes} onClick={() => { this.selectTab(idx); }}> <div className={classes} onClick={() => { this.selectTab(idx) }}>
<div className="tab-caption">{nar.label}</div> <div className="tab-caption">{nar.label}</div>
</div> </div>
); )
}) })
} }
return ''; return ''
} }
renderToolbarTabs() { renderToolbarTabs() {
const title = copy[this.props.language].toolbar.title; let title = copy[this.props.language].toolbar.title
const narratives_label = copy[this.props.language].toolbar.narratives_label; if (process.env.title) title = process.env.title
const tags_label = copy[this.props.language].toolbar.tags_label; const narratives_label = copy[this.props.language].toolbar.narratives_label
const isTags = this.props.tags && this.props.tags.children; const tags_label = copy[this.props.language].toolbar.tags_label
const isTags = this.props.tags && this.props.tags.children
return ( return (
<div className="toolbar"> <div className="toolbar">
@@ -165,7 +166,7 @@ class Toolbar extends React.Component {
{this.renderToolbarTabs()} {this.renderToolbarTabs()}
{this.renderToolbarPanels()} {this.renderToolbarPanels()}
</div> </div>
); )
} }
} }

View File

@@ -13,10 +13,10 @@ function ToolbarBottomActions (props) {
{/* onClick={(view) => this.toggleMapViews(view)} */} {/* onClick={(view) => this.toggleMapViews(view)} */}
{/* isEnabled={this.props.viewFilters.routes} */} {/* isEnabled={this.props.viewFilters.routes} */}
{/* /> */} {/* /> */}
<SitesIcon {process.env.features.USE_SITES ? <SitesIcon
isActive={props.sites.enabled} isActive={props.sites.enabled}
onClickHandler={props.sites.toggle} onClickHandler={props.sites.toggle}
/> /> : null}
{/* <CoeventIcon */} {/* <CoeventIcon */}
{/* onClick={(view) => this.toggleMapViews(view)} */} {/* onClick={(view) => this.toggleMapViews(view)} */}
{/* isEnabled={this.props.viewFilters.coevents} */} {/* isEnabled={this.props.viewFilters.coevents} */}

BIN
src/components/presentational/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { capitalizeFirstLetter } from '../../js/utilities.js'; import { capitalizeFirstLetter } from '../../../js/utilities.js';
const CardCategory = ({ categoryTitle, categoryLabel, color }) => ( const CardCategory = ({ categoryTitle, categoryLabel, color }) => (
<div className="card-row card-cell category"> <div className="card-row card-cell category">

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import copy from '../../js/data/copy.json'; import copy from '../../../js/data/copy.json';
import { isNotNullNorUndefined } from '../../js/utilities'; import { isNotNullNorUndefined } from '../../../js/utilities';
const CardLocation = ({ language, location }) => { const CardLocation = ({ language, location }) => {

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import CardNarrativeLink from './CardNarrativeLink'; import CardNarrativeLink from './NarrativeLink';
const CardNarrative = (props) => ( const CardNarrative = (props) => (
<div className="card-row"> <div className="card-row">

View File

@@ -1,9 +1,9 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Spinner from './Spinner'
import Img from 'react-image' 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 }) => { const CardSource = ({ source, isLoading, onClickHandler }) => {
function renderIconText(type) { function renderIconText(type) {

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import copy from '../../js/data/copy.json'; import copy from '../../../js/data/copy.json';
const CardSummary = ({ language, description, isHighlighted }) => { const CardSummary = ({ language, description, isHighlighted }) => {

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import copy from '../../js/data/copy.json'; import copy from '../../../js/data/copy.json';
const CardTags = ({ tags, language }) => { const CardTags = ({ tags, language }) => {
const tags_lang = copy[language].cardstack.tags; const tags_lang = copy[language].cardstack.tags;

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import copy from '../../js/data/copy.json'; import copy from '../../../js/data/copy.json';
import { isNotNullNorUndefined } from '../../js/utilities'; import { isNotNullNorUndefined } from '../../../js/utilities';
const CardTimestamp = ({ makeTimelabel, language, timestamp }) => { const CardTimestamp = ({ makeTimelabel, language, timestamp }) => {

View File

@@ -1,19 +1,10 @@
import React from 'react'; import React from 'react';
import { Portal } from 'react-portal'; import { Portal } from 'react-portal';
class MapEvents extends React.Component { function MapEvents ({ getCategoryColor, categories, projectPoint, styleLocation, narrative, onSelect, svg, locations }){
function getLocationEventsDistribution(location) {
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) {
const eventCount = {}; const eventCount = {};
const categories = this.props.categories; const categories = categories;
categories.forEach(cat => { categories.forEach(cat => {
eventCount[cat.category] = []; eventCount[cat.category] = [];
@@ -26,7 +17,7 @@ class MapEvents extends React.Component {
return eventCount; return eventCount;
} }
renderLocation(location) { function renderLocation(location) {
/** /**
{ {
events: [...], events: [...],
@@ -35,23 +26,23 @@ class MapEvents extends React.Component {
longitude: '32.2' longitude: '32.2'
} }
*/ */
const { x, y } = this.projectPoint([location.latitude, location.longitude]); const { x, y } = projectPoint([location.latitude, location.longitude]);
// const eventsByCategory = this.getLocationEventsDistribution(location); // const eventsByCategory = getLocationEventsDistribution(location);
const locCategory = location.events.length > 0 ? location.events[0].category : 'default' 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 extraStyles = customStyles[0]
const extraRender = customStyles[1] const extraRender = customStyles[1]
const styles = ({ const styles = ({
fill: this.props.getCategoryColor(locCategory), fill: getCategoryColor(locCategory),
fillOpacity: 1, fillOpacity: 1,
...customStyles[0] ...customStyles[0]
}) })
// in narrative mode, only render events in narrative // in narrative mode, only render events in narrative
if (this.props.narrative) { if (narrative) {
const { steps } = this.props.narrative const { steps } = narrative
const onlyIfInNarrative = e => steps.map(s => s.id).includes(e.id) const onlyIfInNarrative = e => steps.map(s => s.id).includes(e.id)
const eventsInNarrative = location.events.filter(onlyIfInNarrative) const eventsInNarrative = location.events.filter(onlyIfInNarrative)
@@ -64,7 +55,7 @@ class MapEvents extends React.Component {
<g <g
className="location" className="location"
transform={`translate(${x}, ${y})`} transform={`translate(${x}, ${y})`}
onClick={() => this.props.onSelect(location.events)} onClick={() => onSelect(location.events)}
> >
<circle <circle
className="location-event-marker" className="location-event-marker"
@@ -77,13 +68,11 @@ class MapEvents extends React.Component {
) )
} }
render() { return (
return ( <Portal node={svg}>
<Portal node={this.props.svg}> {locations.map(renderLocation)}
{this.props.locations.map(loc => this.renderLocation(loc))} </Portal>
</Portal> );
);
}
} }
export default MapEvents; export default MapEvents;

View File

@@ -1,76 +1,67 @@
import React from 'react' import React from 'react'
import { Portal } from 'react-portal' import { Portal } from 'react-portal'
class MapNarratives extends React.Component { function MapNarratives ({ styles, onSelectNarrative, svg, narrative, narratives, projectPoint }) {
function getNarrativeStyle(narrativeId) {
projectPoint(location) { const styleName = (narrativeId && narrativeId in styles)
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)
? narrativeId ? narrativeId
: 'default' : 'default'
return this.props.narrativeProps[styleName] return styles[styleName]
} }
getStepStyle(name) { function getStepStyle(name) {
if (name === 'None') return null 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 === '') return (step.latitude === '' || step.longitude === '')
} }
renderNarrativeStep(idx, n) { function renderNarrativeStep(idx, n) {
const step = n.steps[idx] const step = n.steps[idx]
const step2 = n.steps[idx + 1] const step2 = n.steps[idx + 1]
// don't draw if one of the steps has no location // 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 return null
// 0 if not in narrative mode, 1 if active narrative, 0.1 if inactive // 0 if not in narrative mode, 1 if active narrative, 0.1 if inactive
let styles = { let styles = {
strokeOpacity: (n === null) ? 0 strokeOpacity: (n === null) ? 0
: (step && (n.id === this.props.narrative.id)) ? 1 : 0.1, : (step && (n.id === narrative.id)) ? 1 : 0.1,
strokeWidth: 0, strokeWidth: 0,
strokeDasharray: 'none', strokeDasharray: 'none',
stroke: 'none' stroke: 'none'
} }
const p1 = this.projectPoint([step.latitude, step.longitude]) const p1 = projectPoint([step.latitude, step.longitude])
const p2 = this.projectPoint([step2.latitude, step2.longitude]) const p2 = projectPoint([step2.latitude, step2.longitude])
if (step) { if (step) {
if (process.env.features.NARRATIVE_STEP_STYLES) { if (process.env.features.NARRATIVE_STEP_STYLES) {
const _idx = step.narratives.indexOf(n.id) const _idx = step.narratives.indexOf(n.id)
const stepStyle = step.narrative___stepStyles[_idx] const stepStyle = step.narrative___stepStyles[_idx]
return this._renderNarrativeStep( return _renderNarrativeStep(
p1, p1,
p2, p2,
{ ...styles, ...this.getStepStyle(stepStyle) } { ...styles, ...getStepStyle(stepStyle) }
) )
// otherwise steps are styled per narrative // otherwise steps are styled per narrative
} else { } else {
styles = { styles = {
...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 const { stroke, strokeWidth, strokeDasharray, strokeOpacity } = styles
return ( return (
<line <line
@@ -80,7 +71,7 @@ class MapNarratives extends React.Component {
y1={p1.y} y1={p1.y}
y2={p2.y} y2={p2.y}
markerStart="none" markerStart="none"
onClick={() => this.props.onSelectNarrative(n)} onClick={() => onSelectNarrative(n)}
style={{ style={{
strokeWidth, strokeWidth,
strokeDasharray, 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) const steps = n.steps.slice(0, n.steps.length - 1)
return ( return (
<g id={`narrative-${n.id.replace(/ /g,"_")}`} className="narrative"> <g id={`narrative-${n.id.replace(/ /g,"_")}`} className="narrative">
{steps.map((s, idx) => this.renderNarrativeStep(idx, n))} {steps.map((s, idx) => renderNarrativeStep(idx, n))}
</g> </g>
) )
} }
render() { if (narrative === null) return (<div />)
if (this.props.narrative === null) return (<div />)
return ( return (
<Portal node={this.props.svg}> <Portal node={svg}>
{this.props.narratives.map(n => this.renderNarrative(n))} {narratives.map(n => renderNarrative(n))}
</Portal> </Portal>
) )
}
} }
export default MapNarratives export default MapNarratives

View File

@@ -2,17 +2,8 @@ import React from 'react';
import { Portal } from 'react-portal'; import { Portal } from 'react-portal';
class MapSelectedEvents extends React.Component { 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) { renderMarker (event) {
const { x, y } = this.projectPoint([event.latitude, event.longitude]); const { x, y } = this.props.projectPoint([event.latitude, event.longitude]);
return ( return (
<g <g
className="location-marker" className="location-marker"
@@ -42,4 +33,4 @@ class MapSelectedEvents extends React.Component {
) )
} }
} }
export default MapSelectedEvents; export default MapSelectedEvents;

View File

@@ -0,0 +1,51 @@
import React from 'react'
import { Portal } from 'react-portal'
function MapShapes({ svg, shapes, projectPoint, styles }) {
function renderShape(shape) {
const lineCoords = []
const points = shape.points
.map(projectPoint)
points.forEach((p1, idx) => {
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 (
<line
id={`${shape.name}_style`}
markerStart="none"
{...coords}
style={shapeStyles}
>
</line>
)
})
}
if (!shapes || !shapes.length) return null
return (
<Portal node={svg}>
<g id={`shapes-layer`} className="narrative">
{shapes.map(renderShape)}
</g>
</Portal>
)
}
export default MapShapes

View File

@@ -0,0 +1,25 @@
import React from 'react';
function MapSites({ sites, projectPoint }) {
function renderSite(site) {
const { x, y } = projectPoint([site.latitude, site.longitude]);
return (<div
className="leaflet-tooltip site-label leaflet-zoom-animated leaflet-tooltip-top"
style={{ opacity: 1, transform: `translate3d(calc(${x}px - 50%), ${y - 25}px, 0px)`}}>
{site.site}
</div>
);
}
if (!sites || !sites.length) return null;
return (
<div className="sites-layer">
{sites.map(renderSite)}
</div>
)
}
export default MapSites;

View File

@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { selectActiveNarrative } from '../../selectors' import { selectActiveNarrative } from '../../../selectors'
function NarrativeCard ({ narrative }) { function NarrativeCard ({ narrative }) {
// no display if no narrative // no display if no narrative

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import NarrativeCard from './NarrativeCard' import Card from './Card'
import NarrativeAdjust from './NarrativeAdjust' import Adjust from './Adjust'
import NarrativeClose from './NarrativeClose' import Close from './Close'
export default ({ narrative, methods }) => { export default ({ narrative, methods }) => {
if (!narrative) return null if (!narrative) return null
@@ -12,18 +12,18 @@ export default ({ narrative, methods }) => {
return ( return (
<React.Fragment> <React.Fragment>
<NarrativeCard narrative={narrative} /> <Card narrative={narrative} />
<NarrativeAdjust <Adjust
isDisabled={!prevExists} isDisabled={!prevExists}
direction='left' direction='left'
onClickHandler={methods.onPrev} onClickHandler={methods.onPrev}
/> />
<NarrativeAdjust <Adjust
isDisabled={!nextExists} isDisabled={!nextExists}
direction='right' direction='right'
onClickHandler={methods.onNext} onClickHandler={methods.onNext}
/> />
<NarrativeClose <Close
onClickHandler={() => methods.onSelectNarrative(null)} onClickHandler={() => methods.onSelectNarrative(null)}
closeMsg='-- exit from narrative --' closeMsg='-- exit from narrative --'
/> />

View File

@@ -3,8 +3,8 @@ import React from 'react';
const TimelineClip = ({ dims }) => ( const TimelineClip = ({ dims }) => (
<clipPath id="clip"> <clipPath id="clip">
<rect <rect
x="120" x={dims.margin_left}
y="0" y="0"
width={dims.width - dims.margin_left - dims.width_controls} width={dims.width - dims.margin_left - dims.width_controls}
height={dims.height - 25} height={dims.height - 25}
> >

View File

@@ -5,14 +5,14 @@ const TimelineHandles = ({ dims, onMoveTime }) => {
return ( return (
<g className="time-controls-inline"> <g className="time-controls-inline">
<g <g
transform={`translate(${dims.margin_left + 20}, 62)`} transform={`translate(${dims.margin_left - 20}, 120)`}
onClick={() => onMoveTime('backwards')} onClick={() => onMoveTime('backwards')}
> >
<circle r="15"></circle> <circle r="15"></circle>
<path d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z" transform="rotate(270)"></path> <path d="M0,-7.847549217020565L6.796176979388489,3.9237746085102825L-6.796176979388489,3.9237746085102825Z" transform="rotate(270)"></path>
</g> </g>
<g <g
transform={`translate(${dims.width - dims.width_controls - 20}, 62)`} transform={`translate(${dims.width - dims.width_controls + 20}, 120)`}
onClick={() => onMoveTime('forward')} onClick={() => onMoveTime('forward')}
> >
<circle r="15"></circle> <circle r="15"></circle>
@@ -23,4 +23,4 @@ const TimelineHandles = ({ dims, onMoveTime }) => {
} }
export default TimelineHandles; export default TimelineHandles;

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { formatterWithYear } from '../../../js/utilities.js';
const TimelineLabels = ({ dims, timelabels }) => {
return (
<g>
<line
class="axisBoundaries"
x1={dims.margin_left}
x2={dims.margin_left}
y1="10"
y2="20"
>
</line>
<line
class="axisBoundaries"
x1={dims.width - dims.width_controls}
x2={dims.width - dims.width_controls}
y1="10"
y2="20"
>
</line>
<text
class="timeLabel0 timeLabel"
x="5"
y="15"
>
{formatterWithYear(timelabels[0])}
</text>
<text
class="timelabelF timeLabel"
x={dims.width - dims.width_controls - 5}
y="15"
style={{ textAnchor: 'end' }}
>
{formatterWithYear(timelabels[1])}
</text>
</g>
)
}
export default TimelineLabels;

View File

@@ -1,44 +0,0 @@
import React from 'react';
import { formatterWithYear } from '../../js/utilities.js';
const TimelineLabels = ({ dims, timelabels }) => {
return (
<g>
<line
class="axisBoundaries"
x1={dims.margin_left}
x2={dims.margin_left}
y1="10"
y2="20"
>
</line>
<line
class="axisBoundaries"
x1={dims.width - dims.width_controls}
x2={dims.width - dims.width_controls}
y1="10"
y2="20"
>
</line>
{/* <text */}
{/* class="timeLabel0 timeLabel" */}
{/* x="5" */}
{/* y="15" */}
{/* > */}
{/* {formatterWithYear(timelabels[0])} */}
{/* </text> */}
{/* <text */}
{/* class="timelabelF timeLabel" */}
{/* x={dims.width - dims.width_controls - 5} */}
{/* y="15" */}
{/* style={{ textAnchor: 'end' }} */}
{/* > */}
{/* {formatterWithYear(timelabels[1])} */}
{/* </text> */}
</g>
)
}
export default TimelineLabels;

View File

@@ -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]
@@ -156,11 +156,13 @@ function updateCategoryFilters(appState, action) {
} }
function updateTimeRange(appState, action) { // XXX function updateTimeRange(appState, action) { // XXX
return Object.assign({}, appState, { return {
filters: Object.assign({}, appState.filters, { ...appState,
timerange: action.timerange timeline: {
}), ...appState.timeline,
}) range: action.timerange
},
}
} }
function resetAllFilters(appState) { // XXX function resetAllFilters(appState) { // XXX

View File

@@ -0,0 +1,8 @@
import Joi from 'joi'
const shapeSchema = Joi.object().keys({
name: Joi.string().required(),
items: Joi.array().required()
})
export default shapeSchema

View File

@@ -1,12 +1,13 @@
import Joi from 'joi'; import Joi from 'joi'
import eventSchema from '../schema/eventSchema'; import eventSchema from '../schema/eventSchema'
import categorySchema from '../schema/categorySchema'; import categorySchema from '../schema/categorySchema'
import siteSchema from '../schema/siteSchema'; import siteSchema from '../schema/siteSchema'
import narrativeSchema from '../schema/narrativeSchema'; import narrativeSchema from '../schema/narrativeSchema'
import sourceSchema from '../schema/sourceSchema' 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 * Create an error notification object
@@ -21,8 +22,8 @@ function makeError(type, id, message) {
} }
const isLeaf = node => (Object.keys(node.children).length === 0); const isLeaf = node => (Object.keys(node.children).length === 0)
const isDuplicate = (node, set) => { return (set.has(node.key)); }; const isDuplicate = (node, set) => { return (set.has(node.key)) }
/* /*
@@ -61,8 +62,9 @@ export function validateDomain (domain) {
sites: [], sites: [],
narratives: [], narratives: [],
sources: {}, sources: {},
tags: {},
shapes: [],
notifications: domain.notifications, notifications: domain.notifications,
tags: {}
} }
const discardedDomain = { const discardedDomain = {
@@ -71,18 +73,19 @@ export function validateDomain (domain) {
sites: [], sites: [],
narratives: [], narratives: [],
sources: [], sources: [],
shapes: []
} }
function validateArrayItem(item, domainKey, schema) { function validateArrayItem(item, domainKey, schema) {
const result = Joi.validate(item, schema); const result = Joi.validate(item, schema)
if (result.error !== null) { if (result.error !== null) {
const id = item.id || '-'; const id = item.id || '-'
const domainStr = capitalize(domainKey); const domainStr = capitalize(domainKey)
const error = makeError(domainStr, id, result.error.message); const error = makeError(domainStr, id, result.error.message)
discardedDomain[domainKey].push(Object.assign(item, { error })); discardedDomain[domainKey].push(Object.assign(item, { error }))
} else { } else {
sanitizedDomain[domainKey].push(item); sanitizedDomain[domainKey].push(item)
} }
} }
@@ -109,29 +112,38 @@ export function validateDomain (domain) {
}) })
} }
validateArray(domain.events, 'events', eventSchema); validateArray(domain.events, 'events', eventSchema)
validateArray(domain.categories, 'categories', categorySchema); validateArray(domain.categories, 'categories', categorySchema)
validateArray(domain.sites, 'sites', siteSchema); validateArray(domain.sites, 'sites', siteSchema)
validateArray(domain.narratives, 'narratives', narrativeSchema); validateArray(domain.narratives, 'narratives', narrativeSchema)
validateObject(domain.sources, 'sources', sourceSchema); 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 // Message the number of failed items in domain
Object.keys(discardedDomain).forEach(disc => { Object.keys(discardedDomain).forEach(disc => {
const len = discardedDomain[disc].length; const len = discardedDomain[disc].length
if (len) { if (len) {
sanitizedDomain.notifications.push({ sanitizedDomain.notifications.push({
message: `${len} invalid ${disc} not displayed.`, message: `${len} invalid ${disc} not displayed.`,
items: discardedDomain[disc], items: discardedDomain[disc],
type: 'error' type: 'error'
}); })
} }
}); })
// Validate uniqueness of tags // Validate uniqueness of tags
const tagSet = new Set([]); const tagSet = new Set([])
const duplicateTags = []; const duplicateTags = []
validateTree(domain.tags, {}, tagSet, duplicateTags); validateTree(domain.tags, {}, tagSet, duplicateTags)
// Duplicated tags // Duplicated tags
if (duplicateTags.length > 0) { if (duplicateTags.length > 0) {
@@ -139,9 +151,9 @@ export function validateDomain (domain) {
message: `Tags are required to be unique. Ignoring duplicates for now.`, message: `Tags are required to be unique. Ignoring duplicates for now.`,
items: duplicateTags, items: duplicateTags,
type: 'error' type: 'error'
}); })
} }
sanitizedDomain.tags = domain.tags; sanitizedDomain.tags = domain.tags
return sanitizedDomain; return sanitizedDomain
} }

View File

@@ -16,11 +16,11 @@
.leaflet-container { .leaflet-container {
height: 100%; height: 100%;
img.leaflet-tile { // img.leaflet-tile {
-webkit-filter: contrast(120%) brightness(115%) grayscale(95%); /* Webkit */ // -webkit-filter: contrast(120%) brightness(115%) grayscale(95%); /* Webkit */
filter: gray; /* IE6-9 */ // filter: gray; /* IE6-9 */
filter: contrast(120%) brightness(115%) grayscale(95%); /* W3C */ // filter: contrast(120%) brightness(115%) grayscale(95%); /* W3C */
} // }
} }
&.hidden { &.hidden {
@@ -65,14 +65,14 @@
} }
} }
.sites-layer { .sites-layer, .shapes-layer {
position: fixed; position: fixed;
top: 0px; top: 0px;
left: 110px; left: 110px;
} }
&.narrative-mode { &.narrative-mode {
.sites-layer { .sites-layer, .shapes-layer {
position: fixed; position: fixed;
top: 0px; top: 0px;
left: 0px; left: 0px;

View File

@@ -15,13 +15,17 @@ export const getSites = (state) => {
} }
export const getSources = state => { export const getSources = state => {
if (process.env.features.USE_SOURCES) return state.domain.sources 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 [] return []
} }
export const getNotifications = state => state.domain.notifications 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
/** /**

View File

@@ -39,32 +39,48 @@ 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: {
zoomLevels: [ anchor: [31.356397, 34.784818],
{ label: '3 years', duration: 1576800 }, startZoom: 11,
{ label: '3 months', duration: 129600 }, minZoom: 7,
{ label: '3 days', duration: 4320 }, maxZoom: 18,
{ label: '12 hours', duration: 720 }, bounds: null,
{ label: '2 hours', duration: 120 }, maxBounds: [[180, -180], [-180, 180]]
{ label: '30 min', duration: 30 }, },
{ label: '10 min', duration: 10 } 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: { flags: {
isFetchingDomain: false, isFetchingDomain: false,
isFetchingSources: false, isFetchingSources: false,
@@ -81,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',
@@ -91,6 +108,13 @@ const initial = {
stroke: 'red', stroke: 'red',
strokeWidth: 3 strokeWidth: 3
} }
},
shapes: {
default: {
stroke: 'blue',
strokeWidth: 3,
opacity: 0.9
}
} }
}, },
dom: { dom: {
@@ -109,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