mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-13 13:58:35 +03:00
Merge pull request #89 from forensic-architecture/topic/shapes-support
Topic/shapes support
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,3 +2,5 @@ build/
|
|||||||
node_modules/
|
node_modules/
|
||||||
config.js
|
config.js
|
||||||
dev.config.js
|
dev.config.js
|
||||||
|
|
||||||
|
src/\.DS_Store
|
||||||
|
|||||||
@@ -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: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
BIN
src/components/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,22 +35,23 @@ class Map extends React.Component {
|
|||||||
|
|
||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
if (this.map === null) {
|
if (this.map === null) {
|
||||||
this.initializeMap();
|
this.initializeMap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
// Set appropriate zoom for narrative
|
// Set appropriate zoom for narrative
|
||||||
if (hash(nextProps.app.mapBounds) !== hash(this.props.app.mapBounds)
|
const { bounds } = nextProps.app.map
|
||||||
&& nextProps.app.mapBounds !== null) {
|
if (hash(bounds) !== hash(this.props.app.map.bounds)
|
||||||
this.map.fitBounds(nextProps.app.mapBounds);
|
&& bounds !== null) {
|
||||||
|
this.map.fitBounds(bounds)
|
||||||
} else {
|
} else {
|
||||||
if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) {
|
if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) {
|
||||||
// Fly to first of events selected
|
// Fly to first of events selected
|
||||||
const eventPoint = (nextProps.app.selected.length > 0) ? nextProps.app.selected[0] : null;
|
const eventPoint = (nextProps.app.selected.length > 0) ? nextProps.app.selected[0] : null
|
||||||
|
|
||||||
if (eventPoint !== null && eventPoint.latitude && eventPoint.longitude) {
|
if (eventPoint !== null && eventPoint.latitude && eventPoint.longitude) {
|
||||||
this.map.setView([eventPoint.latitude, eventPoint.longitude]);
|
this.map.setView([eventPoint.latitude, eventPoint.longitude])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,51 +61,50 @@ class Map extends React.Component {
|
|||||||
/**
|
/**
|
||||||
* Creates a Leaflet map and a tilelayer for the map background
|
* Creates a Leaflet map and a tilelayer for the map background
|
||||||
*/
|
*/
|
||||||
|
const { map: mapConf } = this.props.app
|
||||||
const map =
|
const map =
|
||||||
L.map(this.props.ui.dom.map)
|
L.map(this.props.ui.dom.map)
|
||||||
.setView(this.props.app.mapAnchor, 14)
|
.setView(mapConf.anchor, mapConf.startZoom)
|
||||||
.setMinZoom(7)
|
.setMinZoom(mapConf.minZoom)
|
||||||
.setMaxZoom(18)
|
.setMaxZoom(mapConf.maxZoom)
|
||||||
.setMaxBounds([[180, -180], [-180, 180]])
|
.setMaxBounds(mapConf.maxBounds)
|
||||||
|
|
||||||
let s;
|
let s
|
||||||
if (process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== 'your_token') {
|
|
||||||
|
if ((supportedMapboxMap.indexOf(this.props.ui.tiles) !== -1) && process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== defaultToken) {
|
||||||
s = L.tileLayer(
|
s = L.tileLayer(
|
||||||
`http://a.tiles.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.png?access_token=${process.env.MAPBOX_TOKEN}`
|
`http://a.tiles.mapbox.com/v4/mapbox.${this.props.ui.tiles}/{z}/{x}/{y}@2x.png?access_token=${process.env.MAPBOX_TOKEN}`
|
||||||
);
|
)
|
||||||
|
} else if (process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== defaultToken) {
|
||||||
|
s = L.tileLayer(
|
||||||
|
`http://a.tiles.mapbox.com/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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
BIN
src/components/presentational/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -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">
|
||||||
@@ -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 }) => {
|
||||||
|
|
||||||
@@ -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">
|
||||||
@@ -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) {
|
||||||
@@ -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 }) => {
|
||||||
|
|
||||||
@@ -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;
|
||||||
@@ -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 }) => {
|
||||||
|
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
51
src/components/presentational/Map/Shapes.jsx
Normal file
51
src/components/presentational/Map/Shapes.jsx
Normal 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
|
||||||
25
src/components/presentational/Map/Sites.jsx
Normal file
25
src/components/presentational/Map/Sites.jsx
Normal 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;
|
||||||
@@ -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
|
||||||
@@ -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 --'
|
||||||
/>
|
/>
|
||||||
@@ -3,7 +3,7 @@ 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}
|
||||||
@@ -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>
|
||||||
44
src/components/presentational/Timeline/Labels.js
Normal file
44
src/components/presentational/Timeline/Labels.js
Normal 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;
|
||||||
@@ -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;
|
|
||||||
@@ -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
|
||||||
|
|||||||
8
src/reducers/schema/shapeSchema.js
Normal file
8
src/reducers/schema/shapeSchema.js
Normal 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
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user