Files
ukraine-timemap/src/selectors/index.js
Lachlan Kermode 08bbe9c85e fix projects!
2020-06-18 10:30:25 +02:00

260 lines
7.9 KiB
JavaScript

import { createSelector } from 'reselect'
import { insetSourceFrom, dateMin, dateMax } from '../common/utilities'
import { isTimeRangedIn } from './helpers'
import { sizes } from '../common/global'
// Input selectors
export const getEvents = state => state.domain.events
export const getCategories = state => state.domain.categories
export const getNarratives = state => state.domain.narratives
export const getActiveNarrative = state => state.app.narrative
export const getActiveStep = state => state.app.narrativeState.current
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 getNotifications = state => state.domain.notifications
export const getTagTree = state => state.domain.tags
export const getActiveTags = state => state.app.filters.tags
export const getActiveCategories = state => state.app.filters.categories
export const getTimeRange = state => state.app.timeline.range
export const getTimelineDimensions = state => state.app.timeline.dimensions
export const selectNarrative = state => state.app.narrative
export const getFeatures = state => state.features
export const selectSites = createSelector([getSites, getFeatures], (sites, features) => {
if (features.USE_SITES) {
return sites.filter(s => !!(+s.enabled))
}
return []
})
export const selectSources = createSelector([getSources, getFeatures], (sources, features) => {
if (features.USE_SOURCES) return sources
return {}
})
export const selectShapes = createSelector([getShapes, getFeatures], (shapes, features) => {
if (features.USE_SHAPES) return shapes
return []
})
/**
* Of all available events, selects those that
* 1. fall in time range
* 2. exist in an active tag
* 3. exist in an active category
*/
export const selectEvents = createSelector(
[getEvents, getActiveTags, getActiveCategories, getTimeRange, getFeatures],
(events, activeTags, activeCategories, timeRange, features) => {
return events.reduce((acc, event) => {
const isMatchingTag = (event.tags && event.tags.map(tag => activeTags.includes(tag)).some(s => s)) || activeTags.length === 0
const isActiveTag = isMatchingTag || activeTags.length === 0
const isActiveCategory = activeCategories.includes(event.category) || activeCategories.length === 0
let isActiveTime = isTimeRangedIn(event, timeRange)
isActiveTime = features.GRAPH_NONLOCATED ? ((!event.latitude && !event.longitude) || isActiveTime) : isActiveTime
if (isActiveTime && isActiveTag && isActiveCategory) {
acc[event.id] = { ...event }
}
return acc
}, [])
})
/**
* Of all available events, selects those that fall within the time range,
* and if TAGS are being used, select them if their tags are enabled
*/
export const selectNarratives = createSelector(
[getEvents, getNarratives, getSources, getFeatures],
(events, narrativesMeta, sources, features) => {
if (!features.USE_NARRATIVES) {
return []
}
const narratives = {}
const narrativeSkeleton = id => ({ id, steps: [] })
/* populate narratives dict with events */
events.forEach(evt => {
const isInNarrative = evt.narratives.length > 0
evt.narratives.forEach(narrative => {
// initialise
if (!narratives[narrative]) { narratives[narrative] = narrativeSkeleton(narrative) }
// add evt to steps
if (isInNarrative) {
// NB: insetSourceFrom is a 'curried' function to allow with maps
narratives[narrative].steps.push(insetSourceFrom(sources)(evt))
}
})
})
/* sort steps by time */
Object.keys(narratives).forEach(key => {
const steps = narratives[key].steps
steps.sort((a, b) => a.datetime - b.datetime)
if (narrativesMeta.find(n => n.id === key)) {
narratives[key] = {
...narrativesMeta.find(n => n.id === key),
...narratives[key]
}
}
})
// Return narratives in original order
// + filter those that are undefined
return narrativesMeta.map(n => narratives[n.id]).filter(d => d)
})
/** Aggregate information about the narrative and the current step into
* a single object. If narrative is null, the whole object is null.
*/
export const selectActiveNarrative = createSelector(
[getActiveNarrative, getActiveStep],
(narrative, current) => narrative
? { ...narrative, current }
: null
)
/**
* Group events by location. Each location is an object:
{
events: [...],
label: 'Location name',
latitude: '47.7',
longitude: '32.2'
}
*/
export const selectLocations = createSelector(
[selectEvents],
(events) => {
const activeLocations = {}
events.forEach(event => {
const location = event.location
if (activeLocations[location]) {
activeLocations[location].events.push(event)
} else {
activeLocations[location] = {
label: location,
events: [event],
latitude: event.latitude,
longitude: event.longitude
}
}
})
return Object.values(activeLocations)
}
)
export const selectEventsWithProjects = createSelector(
[selectEvents, getFeatures],
(events, features) => {
if (!features.GRAPH_NONLOCATED) {
return [events, []]
}
const projSize = 2 * sizes.eventDotR
const projectIdx = features.GRAPH_NONLOCATED.projectIdx || 0
const getProject = ev => ev.tags[projectIdx]
const projects = {}
// get all projects
events = events.reduce((acc, event) => {
const project = event.tags.length >= 1 && !event.latitude && !event.longitude ? getProject(event) : null
// add project if it doesn't exist
if (project !== null) {
if (projects.hasOwnProperty(project)) {
projects[project].start = dateMin(projects[project].start, event.datetime)
projects[project].end = dateMax(projects[project].end, event.datetime)
} else {
projects[project] = {
start: event.datetime,
end: event.datetime,
key: project,
category: event.category
}
}
}
acc.push({ ...event, project })
return acc
}, [])
let projObjs = Object.values(projects)
projObjs.sort((a, b) => a.start - b.start)
// active projects is a data structure with projObjs.length empty slots
let activeProjs = Object.keys(projects).map((_, idx) => null)
const projectsWithOffset = projObjs.reduce((acc, proj, theIdx) => {
// remove any project that have ended from slots
activeProjs.forEach((theProj, theProjIdx) => {
if (theProj !== null) {
const projInSlot = projects[theProj]
if (projInSlot.end < proj.start) {
activeProjs[theProjIdx] = null
}
}
})
let i = 0
// find the first empty slot
while (activeProjs[i]) i++
// put proj in slot
activeProjs[i] = proj.key
proj.offset = i * projSize
acc[proj.key] = proj
return acc
}, {})
return [events, projectsWithOffset]
}
)
export const selectStackedEvents = createSelector(
[selectEventsWithProjects],
eventsWithProjects => {
return eventsWithProjects[0]
}
)
export const selectProjects = createSelector(
[selectEventsWithProjects, getFeatures],
(eventsWithProjects, features) => {
if (!features.GRAPH_NONLOCATED) {
return []
}
return eventsWithProjects[1]
}
)
/**
* Of all the sources, select those that are relevant to the selected events.
*/
export const selectSelected = createSelector(
[getSelected, getSources],
(selected, sources) => {
if (selected.length === 0) {
return []
}
return selected.map(insetSourceFrom(sources))
}
)
export const selectDimensions = createSelector(
[getTimelineDimensions],
(dimensions) => {
return {
...dimensions,
trackHeight: dimensions.contentHeight - 50 // height of time labels
}
}
)