mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 13:28:36 +03:00
Merging in develop branch
This commit is contained in:
@@ -4,7 +4,7 @@ import { urlFromEnv } from '../common/utilities'
|
||||
// TODO: relegate these URLs entirely to environment variables
|
||||
// const CONFIG_URL = urlFromEnv('CONFIG_EXT')
|
||||
const EVENT_DATA_URL = urlFromEnv('EVENTS_EXT')
|
||||
const CATEGORY_URL = urlFromEnv('CATEGORIES_EXT')
|
||||
// const CATEGORY_URL = urlFromEnv('CATEGORIES_EXT')
|
||||
const ASSOCIATIONS_URL = urlFromEnv('ASSOCIATIONS_EXT')
|
||||
const SOURCES_URL = urlFromEnv('SOURCES_EXT')
|
||||
const SITES_URL = urlFromEnv('SITES_EXT')
|
||||
@@ -42,13 +42,6 @@ export function fetchDomain () {
|
||||
)
|
||||
).then(results => results.flatMap(t => t))
|
||||
|
||||
let catPromise = Promise.resolve([])
|
||||
if (features.USE_CATEGORIES) {
|
||||
catPromise = fetch(CATEGORY_URL)
|
||||
.then(response => response.json())
|
||||
.catch(() => handleError(domainMsg('categories')))
|
||||
}
|
||||
|
||||
let associationsPromise = Promise.resolve([])
|
||||
if (features.USE_ASSOCIATIONS) {
|
||||
if (!ASSOCIATIONS_URL) {
|
||||
@@ -87,7 +80,6 @@ export function fetchDomain () {
|
||||
|
||||
return Promise.all([
|
||||
eventPromise,
|
||||
catPromise,
|
||||
associationsPromise,
|
||||
sourcesPromise,
|
||||
sitesPromise,
|
||||
@@ -96,17 +88,17 @@ export function fetchDomain () {
|
||||
.then(response => {
|
||||
const result = {
|
||||
events: response[0],
|
||||
categories: response[1],
|
||||
associations: response[2],
|
||||
sources: response[3],
|
||||
sites: response[4],
|
||||
shapes: response[5],
|
||||
associations: response[1],
|
||||
sources: response[2],
|
||||
sites: response[3],
|
||||
shapes: response[4],
|
||||
notifications
|
||||
}
|
||||
if (Object.values(result).some(resp => resp.hasOwnProperty('error'))) {
|
||||
throw new Error('Some URLs returned negative. If you are in development, check the server is running')
|
||||
}
|
||||
dispatch(toggleFetchingDomain())
|
||||
dispatch(setInitialCategories(result.associations))
|
||||
return result
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -212,6 +204,14 @@ export function setNotLoading () {
|
||||
}
|
||||
}
|
||||
|
||||
export const SET_INITIAL_CATEGORIES = 'SET_INITIAL_CATEGORIES'
|
||||
export function setInitialCategories (values) {
|
||||
return {
|
||||
type: SET_INITIAL_CATEGORIES,
|
||||
values
|
||||
}
|
||||
}
|
||||
|
||||
export const UPDATE_TIMERANGE = 'UPDATE_TIMERANGE'
|
||||
export function updateTimeRange (timerange) {
|
||||
return {
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export const FILTER_MODE = 'FILTER'
|
||||
export const NARRATIVE_MODE = 'NARRATIVE'
|
||||
export const ASSOCIATION_MODES = {
|
||||
CATEGORY: 'CATEGORY',
|
||||
NARRATIVE: 'NARRATIVE',
|
||||
FILTER: 'FILTER'
|
||||
}
|
||||
|
||||
@@ -63,6 +63,18 @@ export function trimAndEllipse (string, stringNum) {
|
||||
return string
|
||||
}
|
||||
|
||||
export function getEventCategories (event, categories) {
|
||||
const matchedCategories = []
|
||||
if (event.associations && event.associations.length > 0) {
|
||||
event.associations.reduce((acc, val) => {
|
||||
const foundCategory = categories.find(cat => cat.id === val)
|
||||
if (foundCategory) acc.push(foundCategory)
|
||||
return acc
|
||||
}, matchedCategories)
|
||||
}
|
||||
return matchedCategories
|
||||
}
|
||||
|
||||
/**
|
||||
* Inset the full source represenation from 'allSources' into an event. The
|
||||
* function is 'curried' to allow easy use with maps. To use for a single
|
||||
|
||||
@@ -78,7 +78,7 @@ class Timeline extends React.Component {
|
||||
makeScaleY (categories, trackHeight, marginTop) {
|
||||
const { features } = this.props
|
||||
if (features.GRAPH_NONLOCATED && features.GRAPH_NONLOCATED.categories) {
|
||||
categories = categories.filter(cat => !features.GRAPH_NONLOCATED.categories.includes(cat.category))
|
||||
categories = categories.filter(cat => !features.GRAPH_NONLOCATED.categories.includes(cat.id))
|
||||
}
|
||||
const catHeight = trackHeight / (categories.length)
|
||||
const shiftUp = trackHeight / (categories.length) / 2
|
||||
@@ -87,7 +87,8 @@ class Timeline extends React.Component {
|
||||
const catsYpos = categories.map((g, i) => {
|
||||
return ((i + 1) * catHeight) - shiftUp + marginShift + manualAdjustment
|
||||
})
|
||||
const catMap = categories.map(c => c.category)
|
||||
const catMap = categories.map(c => c.id)
|
||||
|
||||
return (cat) => {
|
||||
const idx = catMap.indexOf(cat)
|
||||
return catsYpos[idx]
|
||||
@@ -268,11 +269,16 @@ class Timeline extends React.Component {
|
||||
|
||||
getY (event) {
|
||||
const { features, domain } = this.props
|
||||
const { USE_CATEGORIES, GRAPH_NONLOCATED } = features
|
||||
const { categories } = domain
|
||||
|
||||
if (!USE_CATEGORIES) { return this.state.dims.trackHeight / 2 }
|
||||
const categoriesExist = categories && categories.length > 0
|
||||
|
||||
const { GRAPH_NONLOCATED } = features
|
||||
|
||||
if (!categoriesExist) { return this.state.dims.trackHeight / 2 }
|
||||
|
||||
const { category, project } = event
|
||||
|
||||
if (GRAPH_NONLOCATED && GRAPH_NONLOCATED.categories.includes(category)) {
|
||||
return this.state.dims.marginTop + domain.projects[project].offset + this.props.ui.eventRadius
|
||||
}
|
||||
@@ -335,7 +341,7 @@ class Timeline extends React.Component {
|
||||
onDragStart={() => { this.onDragStart() }}
|
||||
onDrag={() => { this.onDrag() }}
|
||||
onDragEnd={() => { this.onDragEnd() }}
|
||||
categories={this.props.domain.categories}
|
||||
categories={this.props.app.activeCategories}
|
||||
features={this.props.features}
|
||||
/>
|
||||
<Handles
|
||||
@@ -361,6 +367,7 @@ class Timeline extends React.Component {
|
||||
<Events
|
||||
events={this.props.domain.events}
|
||||
projects={this.props.domain.projects}
|
||||
categories={this.props.domain.categories}
|
||||
styleDatetime={this.styleDatetime}
|
||||
narrative={this.props.app.narrative}
|
||||
getDatetimeX={this.getDatetimeX}
|
||||
@@ -399,6 +406,7 @@ function mapStateToProps (state) {
|
||||
narratives: state.domain.narratives
|
||||
},
|
||||
app: {
|
||||
activeCategories: selectors.getActiveCategories(state),
|
||||
selected: state.app.selected,
|
||||
language: state.app.language,
|
||||
timeline: state.app.timeline,
|
||||
|
||||
@@ -26,11 +26,10 @@ class TimelineCategories extends React.Component {
|
||||
|
||||
renderCategory (cat, idx) {
|
||||
const { features, dims } = this.props
|
||||
const { category } = cat
|
||||
const strokeWidth = 1 // dims.trackHeight / (this.props.categories.length + 1)
|
||||
if (features.GRAPH_NONLOCATED &&
|
||||
features.GRAPH_NONLOCATED.categories &&
|
||||
features.GRAPH_NONLOCATED.categories.includes(category)) {
|
||||
features.GRAPH_NONLOCATED.categories.includes(cat)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -40,26 +39,27 @@ class TimelineCategories extends React.Component {
|
||||
class='tick'
|
||||
style={{ strokeWidth }}
|
||||
opacity='0.5'
|
||||
transform={`translate(0,${this.props.getCategoryY(category)})`}
|
||||
transform={`translate(0,${this.props.getCategoryY(cat)})`}
|
||||
>
|
||||
<line x1={dims.marginLeft} x2={dims.width - dims.width_controls} />
|
||||
</g>
|
||||
<g class='tick' opacity='1' transform={`translate(0,${this.props.getCategoryY(category)})`}>
|
||||
<text x={dims.marginLeft - 5} dy='0.32em'>{category}</text>
|
||||
<g class='tick' opacity='1' transform={`translate(0,${this.props.getCategoryY(cat)})`}>
|
||||
<text x={dims.marginLeft - 5} dy='0.32em'>{cat}</text>
|
||||
</g>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { dims } = this.props
|
||||
const categories = this.props.features.USE_CATEGORIES
|
||||
const { dims, categories } = this.props
|
||||
const categoriesExist = categories && categories.length > 0
|
||||
const renderedCategories = categoriesExist
|
||||
? this.props.categories.map((cat, idx) => this.renderCategory(cat, idx))
|
||||
: this.renderCategory('Events', 0)
|
||||
|
||||
return (
|
||||
<g class='yAxis'>
|
||||
{categories}
|
||||
{renderedCategories}
|
||||
<rect
|
||||
ref={this.grabRef}
|
||||
class='drag-grabber'
|
||||
|
||||
@@ -13,14 +13,14 @@ export default ({
|
||||
<div>
|
||||
{categories.map(cat => {
|
||||
return (<li
|
||||
key={cat.category.replace(/ /g, '_')}
|
||||
key={cat.id.replace(/ /g, '_')}
|
||||
className={'filter-filter active'}
|
||||
style={{ marginLeft: '20px' }}
|
||||
>
|
||||
<Checkbox
|
||||
label={cat.category}
|
||||
isActive={activeCategories.includes(cat.category)}
|
||||
onClickCheckbox={() => onCategoryFilter(cat.category)}
|
||||
label={cat.id}
|
||||
isActive={activeCategories.includes(cat.id)}
|
||||
onClickCheckbox={() => onCategoryFilter(cat.id)}
|
||||
/>
|
||||
</li>)
|
||||
})}
|
||||
|
||||
@@ -4,7 +4,7 @@ import DatetimeBar from './DatetimeBar'
|
||||
import DatetimeSquare from './DatetimeSquare'
|
||||
import DatetimeStar from './DatetimeStar'
|
||||
import Project from './Project'
|
||||
import { calcOpacity } from '../../../common/utilities'
|
||||
import { calcOpacity, getEventCategories } from '../../../common/utilities'
|
||||
|
||||
function renderDot (event, styles, props) {
|
||||
return <DatetimeDot
|
||||
@@ -60,6 +60,7 @@ function renderStar (event, styles, props) {
|
||||
const TimelineEvents = ({
|
||||
events,
|
||||
projects,
|
||||
categories,
|
||||
narrative,
|
||||
getDatetimeX,
|
||||
getY,
|
||||
@@ -75,14 +76,14 @@ const TimelineEvents = ({
|
||||
}) => {
|
||||
const narIds = narrative ? narrative.steps.map(s => s.id) : []
|
||||
|
||||
function renderEvent (event) {
|
||||
function renderEvent (aggregated, event) {
|
||||
if (narrative) {
|
||||
if (!(narIds.includes(event.id))) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const isDot = (!!event.location && !!event.longitude) || (features.GRAPH_NONLOCATED && event.projectOffset !== -1)
|
||||
|
||||
let renderShape = isDot ? renderDot : renderBar
|
||||
if (event.shape) {
|
||||
if (event.shape === 'bar') {
|
||||
@@ -96,23 +97,33 @@ const TimelineEvents = ({
|
||||
}
|
||||
}
|
||||
|
||||
const eventY = getY(event)
|
||||
let colour = event.colour ? event.colour : getCategoryColor(event.category)
|
||||
const styles = {
|
||||
fill: colour,
|
||||
fillOpacity: eventY > 0 ? calcOpacity(1) : 0,
|
||||
transition: `transform ${transitionDuration / 1000}s ease`
|
||||
}
|
||||
const relatedCategories = getEventCategories(event, categories)
|
||||
|
||||
return renderShape(event, styles, {
|
||||
x: getDatetimeX(event.datetime),
|
||||
y: eventY,
|
||||
eventRadius,
|
||||
onSelect: () => onSelect(event),
|
||||
dims,
|
||||
highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.associations[features.HIGHLIGHT_GROUPS.filterIndexIndicatingGroup]) : [],
|
||||
features
|
||||
})
|
||||
if (relatedCategories && relatedCategories.length > 0) {
|
||||
relatedCategories.forEach(cat => {
|
||||
const eventY = getY({ ...event, category: cat.id })
|
||||
|
||||
let colour = event.colour ? event.colour : getCategoryColor(cat.id)
|
||||
const styles = {
|
||||
fill: colour,
|
||||
fillOpacity: eventY > 0 ? calcOpacity(1) : 0,
|
||||
transition: `transform ${transitionDuration / 1000}s ease`
|
||||
}
|
||||
|
||||
aggregated.push(
|
||||
renderShape(event, styles, {
|
||||
x: getDatetimeX(event.datetime),
|
||||
y: eventY,
|
||||
eventRadius,
|
||||
onSelect: () => onSelect(event),
|
||||
dims,
|
||||
highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.filters[features.HIGHLIGHT_GROUPS.filterIndexIndicatingGroup]) : [],
|
||||
features
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
return aggregated
|
||||
}
|
||||
|
||||
let renderProjects = () => null
|
||||
@@ -136,7 +147,7 @@ const TimelineEvents = ({
|
||||
clipPath={'url(#clip)'}
|
||||
>
|
||||
{renderProjects()}
|
||||
{events.map(event => renderEvent(event))}
|
||||
{events.reduce(renderEvent, [])}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import initial from '../store/initial.js'
|
||||
import { ASSOCIATION_MODES } from '../common/constants'
|
||||
import { toggleFlagAC } from '../common/utilities'
|
||||
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
FETCH_SOURCE_ERROR,
|
||||
SET_LOADING,
|
||||
SET_NOT_LOADING,
|
||||
SET_INITIAL_CATEGORIES,
|
||||
UPDATE_SEARCH_QUERY
|
||||
} from '../actions'
|
||||
|
||||
@@ -113,13 +115,14 @@ function toggleFilter (appState, action) {
|
||||
if (!(action.value instanceof Array)) {
|
||||
action.value = [action.value]
|
||||
}
|
||||
const { filter: associationType } = action
|
||||
|
||||
let newFilters = appState.associations.filters.slice(0)
|
||||
let newAssociations = appState.associations[associationType].slice(0)
|
||||
action.value.forEach(vl => {
|
||||
if (newFilters.includes(vl)) {
|
||||
newFilters = newFilters.filter(s => s !== vl)
|
||||
if (newAssociations.includes(vl)) {
|
||||
newAssociations = newAssociations.filter(s => s !== vl)
|
||||
} else {
|
||||
newFilters.push(vl)
|
||||
newAssociations.push(vl)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -127,7 +130,7 @@ function toggleFilter (appState, action) {
|
||||
...appState,
|
||||
associations: {
|
||||
...appState.associations,
|
||||
filters: newFilters
|
||||
[associationType]: newAssociations
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,6 +221,21 @@ function setNotLoading (appState) {
|
||||
}
|
||||
}
|
||||
|
||||
function setInitialCategories (appState, action) {
|
||||
const categories = action.values.reduce((acc, val) => {
|
||||
if (val.mode === ASSOCIATION_MODES.CATEGORY) acc.push(val.id)
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
return {
|
||||
...appState,
|
||||
associations: {
|
||||
...appState.associations,
|
||||
categories: categories
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateSearchQuery (appState, action) {
|
||||
return {
|
||||
...appState,
|
||||
@@ -269,6 +287,8 @@ function app (appState = initial.app, action) {
|
||||
return setLoading(appState)
|
||||
case SET_NOT_LOADING:
|
||||
return setNotLoading(appState)
|
||||
case SET_INITIAL_CATEGORIES:
|
||||
return setInitialCategories(appState, action)
|
||||
case UPDATE_SEARCH_QUERY:
|
||||
return updateSearchQuery(appState, action)
|
||||
default:
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import Joi from 'joi'
|
||||
|
||||
const categorySchema = Joi.object().keys({
|
||||
category: Joi.string().required(),
|
||||
description: Joi.string(),
|
||||
group: Joi.string()
|
||||
})
|
||||
|
||||
export default categorySchema
|
||||
@@ -23,7 +23,7 @@ function createEventSchema (custom) {
|
||||
type: Joi.string().allow(''),
|
||||
category: Joi.string().allow(''),
|
||||
category_full: Joi.string().allow(''),
|
||||
associations: Joi.array(),
|
||||
associations: Joi.array().required().default([]),
|
||||
sources: Joi.array(),
|
||||
comments: Joi.string().allow(''),
|
||||
time_display: Joi.string().allow(''),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
|
||||
import createEventSchema from './eventSchema'
|
||||
import categorySchema from './categorySchema'
|
||||
import siteSchema from './siteSchema'
|
||||
import associationsSchema from './associationsSchema'
|
||||
import sourceSchema from './sourceSchema'
|
||||
@@ -47,7 +46,6 @@ function findDuplicateAssociations (associations) {
|
||||
export function validateDomain (domain, features) {
|
||||
const sanitizedDomain = {
|
||||
events: [],
|
||||
categories: [],
|
||||
sites: [],
|
||||
associations: [],
|
||||
sources: {},
|
||||
@@ -61,7 +59,6 @@ export function validateDomain (domain, features) {
|
||||
|
||||
const discardedDomain = {
|
||||
events: [],
|
||||
categories: [],
|
||||
sites: [],
|
||||
associations: [],
|
||||
sources: [],
|
||||
@@ -110,7 +107,6 @@ export function validateDomain (domain, features) {
|
||||
|
||||
const eventSchema = createEventSchema(features.CUSTOM_EVENT_FIELDS)
|
||||
validateArray(domain.events, 'events', eventSchema)
|
||||
validateArray(domain.categories, 'categories', categorySchema)
|
||||
validateArray(domain.sites, 'sites', siteSchema)
|
||||
validateArray(domain.associations, 'associations', associationsSchema)
|
||||
validateObject(domain.sources, 'sources', sourceSchema)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { createSelector } from 'reselect'
|
||||
import { insetSourceFrom, dateMin, dateMax, isLatitude, isLongitude } from '../common/utilities'
|
||||
import { isTimeRangedIn } from './helpers'
|
||||
import { FILTER_MODE, NARRATIVE_MODE } from '../common/constants'
|
||||
import { ASSOCIATION_MODES } from '../common/constants'
|
||||
|
||||
// Input selectors
|
||||
export const getEvents = state => state.domain.events
|
||||
export const getCategories = state => state.domain.categories
|
||||
export const getNarratives = state => state.domain.associations.filter(item => item.mode === NARRATIVE_MODE)
|
||||
export const getCategories = state => state.domain.associations.filter(item => item.mode === ASSOCIATION_MODES.CATEGORY)
|
||||
export const getNarratives = state => state.domain.associations.filter(item => item.mode === ASSOCIATION_MODES.NARRATIVE)
|
||||
export const getActiveNarrative = state => state.app.associations.narrative
|
||||
export const getSelected = state => state.app.selected
|
||||
export const getSites = state => state.domain.sites
|
||||
export const getSources = state => state.domain.sources
|
||||
export const getShapes = state => state.domain.shapes
|
||||
export const getFilters = state => state.domain.associations.filter(item => item.mode === FILTER_MODE)
|
||||
export const getFilters = state => state.domain.associations.filter(item => item.mode === ASSOCIATION_MODES.FILTER)
|
||||
export const getNotifications = state => state.domain.notifications
|
||||
export const getActiveFilters = state => state.app.associations.filters
|
||||
export const getActiveCategories = state => state.app.associations.categories
|
||||
@@ -55,7 +55,11 @@ export const selectEvents = createSelector(
|
||||
.some(s => s)
|
||||
) || activeFilters.length === 0
|
||||
const isActiveFilter = isMatchingFilter || activeFilters.length === 0
|
||||
const isActiveCategory = activeCategories.includes(event.category) || activeCategories.length === 0
|
||||
const isActiveCategory = (event.associations &&
|
||||
event.associations.map(association =>
|
||||
activeCategories.includes(association))
|
||||
.some(s => s)
|
||||
) || activeCategories.length === 0
|
||||
let isActiveTime = isTimeRangedIn(event, timeRange)
|
||||
isActiveTime = features.GRAPH_NONLOCATED
|
||||
? ((!event.latitude && !event.longitude) || isActiveTime)
|
||||
|
||||
Reference in New Issue
Block a user