mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-11 12:58:35 +03:00
293 lines
7.7 KiB
JavaScript
293 lines
7.7 KiB
JavaScript
import { createSelector} from 'reselect'
|
|
import { parseTimestamp, compareTimestamp, insetSourceFrom } from '../js/utilities'
|
|
|
|
// Input selectors
|
|
export const getEvents = state => state.domain.events
|
|
export const getLocations = state => state.domain.locations
|
|
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) => {
|
|
if (process.env.features.USE_SITES) return state.domain.sites
|
|
return []
|
|
}
|
|
export const getSources = state => {
|
|
if (process.env.features.USE_SOURCES) return state.domain.sources
|
|
return []
|
|
}
|
|
export const getNotifications = state => state.domain.notifications
|
|
export const getTagTree = state => state.domain.tags
|
|
export const getTagsFilter = state => state.app.filters.tags
|
|
export const getCategoriesFilter = state => state.app.filters.categories
|
|
export const getTimeRange = state => state.app.filters.timerange
|
|
|
|
|
|
/**
|
|
* Some handy helpers
|
|
*/
|
|
|
|
/**
|
|
* Given an event and all tags,
|
|
* returns true/false if event has any tag that is active
|
|
*/
|
|
function isTaggedIn(event, tagFilters) {
|
|
if (event.tags) {
|
|
const isTagged = event.tags.some((tag) => {
|
|
return tagFilters.find(tF => (tF.key === tag && tF.active))
|
|
})
|
|
return isTagged
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given an event and all categories,
|
|
* returns true/false if event has a category that is active
|
|
*/
|
|
function isTaggedInWithCategory(event, categories) {
|
|
if (event.category) {
|
|
if (categories.find(c => (c.category === event.category && c.active))) return true
|
|
return false;
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Returns true if no tags are selected
|
|
*/
|
|
function isNoTags(tagFilters) {
|
|
return (
|
|
tagFilters.length === 0
|
|
|| !process.env.features.USE_TAGS
|
|
|| tagFilters.every(t => !t.active)
|
|
)
|
|
}
|
|
|
|
/*
|
|
* Returns true if no categories are selected
|
|
*/
|
|
function isNoCategories(categories) {
|
|
return (
|
|
categories.length === 0
|
|
|| !process.env.features.CATEGORIES_AS_TAGS
|
|
|| categories.every(c => !c.active)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Given an event and a time range,
|
|
* returns true/false if the event falls within timeRange
|
|
*/
|
|
function isTimeRangedIn(event, timeRange) {
|
|
return (
|
|
timeRange[0] < parseTimestamp(event.timestamp)
|
|
&& parseTimestamp(event.timestamp) < timeRange[1]
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 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 selectEvents = createSelector(
|
|
[getEvents, getTagsFilter, getCategoriesFilter, getTimeRange],
|
|
(events, tagFilters, categories, timeRange) => {
|
|
|
|
return events.reduce((acc, event) => {
|
|
const isTagged = isTaggedIn(event, tagFilters) || isNoTags(tagFilters)
|
|
const isTaggedWithCategory = isTaggedInWithCategory(event, categories) || isNoCategories(categories)
|
|
const isTimeRanged = isTimeRangedIn(event, timeRange)
|
|
|
|
if (isTimeRanged && isTagged && isTaggedWithCategory) {
|
|
const eventClone = Object.assign({}, event)
|
|
acc[event.id] = eventClone
|
|
}
|
|
|
|
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],
|
|
(events, narrativesMeta, sources) => {
|
|
|
|
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(compareTimestamp)
|
|
|
|
// steps.forEach((step, i) => {
|
|
// narratives[key].byId[step.id].next = (i < steps.length - 2) ? steps[i + 1] : null
|
|
// narratives[key].byId[step.id].prev = (i > 0) ? steps[i - 1] : null
|
|
// })
|
|
|
|
if (narrativesMeta.find(n => n.id === key)) {
|
|
narratives[key] = {
|
|
...narrativesMeta.find(n => n.id === key),
|
|
...narratives[key]
|
|
}
|
|
}
|
|
})
|
|
|
|
return Object.values(narratives)
|
|
})
|
|
|
|
/** 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 selectedLocations = {}
|
|
events.forEach(event => {
|
|
const location = event.location
|
|
|
|
if (selectedLocations[location]) {
|
|
selectedLocations[location].events.push(event)
|
|
} else {
|
|
selectedLocations[location] = {
|
|
label: location,
|
|
events: [event],
|
|
latitude: event.latitude,
|
|
longitude: event.longitude
|
|
}
|
|
}
|
|
})
|
|
return Object.values(selectedLocations)
|
|
}
|
|
)
|
|
|
|
/**
|
|
* Group events by 'datetime'. Each datetime is an object:
|
|
{
|
|
timestamp: '',
|
|
date: '8/23/2016',
|
|
time: '12:00',
|
|
events: [...]
|
|
}
|
|
*/
|
|
export const selectDatetimes = createSelector(
|
|
[selectEvents],
|
|
events => {
|
|
const datetimes = {}
|
|
events.forEach(event => {
|
|
const { timestamp } = event
|
|
if (datetimes.hasOwnProperty(timestamp)) {
|
|
datetimes[timestamp].events.push(event)
|
|
} else {
|
|
datetimes[timestamp] = {
|
|
timestamp: event.timestamp,
|
|
date: event.date,
|
|
time: event.time,
|
|
events: [event]
|
|
}
|
|
}
|
|
})
|
|
return Object.values(datetimes)
|
|
}
|
|
)
|
|
|
|
|
|
/**
|
|
* 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))
|
|
}
|
|
)
|
|
|
|
/*
|
|
* Select categories, return them as a list
|
|
*/
|
|
export const selectCategories = createSelector(
|
|
[getCategories],
|
|
(categories) => {
|
|
categories.map(cat => {
|
|
cat.active = (!cat.hasOwnProperty('active')) ? false : cat.active
|
|
});
|
|
console.log(categories)
|
|
return categories;
|
|
}
|
|
)
|
|
|
|
|
|
/**
|
|
* Given a tree of tags, return those tags as a list
|
|
* Each node has been aware of its depth, and given an 'active' flag
|
|
*/
|
|
export const selectTagList = createSelector(
|
|
[getTagTree],
|
|
(tags) => {
|
|
const tagList = []
|
|
let depth = 0
|
|
function traverseNode(node, depth) {
|
|
node.active = (!node.hasOwnProperty('active')) ? false : node.active
|
|
node.depth = depth
|
|
|
|
if (node.active) tagList.push(node)
|
|
|
|
if (Object.keys(node.children).length > 0) {
|
|
Object.values(node.children).forEach((childNode) => {
|
|
traverseNode(childNode, depth + 1)
|
|
})
|
|
}
|
|
}
|
|
if (tags && tags !== undefined) {
|
|
if (tags.key && tags.children) traverseNode(tags, depth)
|
|
}
|
|
return tagList
|
|
}
|
|
)
|