import { createSelector } from 'reselect' import { insetSourceFrom, dateMin, dateMax } from '../common/utilities' import { isTimeRangedIn } from './helpers' import { FILTER_MODE, NARRATIVE_MODE } 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 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 getNotifications = state => state.domain.notifications export const getActiveFilters = state => state.app.associations.filters export const getActiveCategories = state => state.app.associations.categories export const getTimeRange = state => state.app.timeline.range export const getTimelineDimensions = state => state.app.timeline.dimensions export const selectNarrative = state => state.app.associations.narrative export const getFeatures = state => state.features export const getEventRadius = state => state.ui.eventRadius 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 filter * 3. exist in an active category */ export const selectEvents = createSelector( [getEvents, getActiveFilters, getActiveCategories, getTimeRange, getFeatures], (events, activeFilters, activeCategories, timeRange, features) => { return events.reduce((acc, event) => { const isMatchingFilter = (event.associations && event.associations.map(association => activeFilters.includes(association)) .some(s => s) ) || activeFilters.length === 0 const isActiveFilter = isMatchingFilter || activeFilters.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 && isActiveFilter && isActiveCategory) { acc[event.id] = { ...event } } return acc }, []) }) /** * Of all available events, selects those that fall within the time range, * and if filters are being used, select them if their filters are enabled */ export const selectNarratives = createSelector( [getEvents, getNarratives, getSources, getFeatures], (events, narrativesMeta, sources, features) => { if (Array.isArray(narrativesMeta) && narrativesMeta.length === 0) { return [] } const narratives = {} const narrativeSkeleton = id => ({ id, steps: [] }) /* populate narratives dict with events */ events.forEach(evt => { evt.associations.forEach(association => { const foundNarrative = narrativesMeta.find(narr => narr.id === association) if (foundNarrative) { const { id: narrId } = foundNarrative // initialise if (!narratives[narrId]) { narratives[narrId] = narrativeSkeleton(narrId) } // add evt to steps // NB: insetSourceFrom is a 'curried' function to allow with maps narratives[narrId].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) const existingAssociatedNarrative = narrativesMeta.find(n => n.id === key) if (existingAssociatedNarrative) { narratives[key] = { ...existingAssociatedNarrative, ...narratives[key] } } }) // Return narratives in original order // + filter those that are undefined return narrativesMeta.map(n => narratives[n.id]).filter(d => d) }) /** We iterate through narrative.steps and check the idx there against the selected array and we return the idx */ export const selectNarrativeIdx = createSelector( [getSelected, getActiveNarrative], (selected, narrative) => { // Only one event selected in narrative mode if (narrative === null) return -1 const selectedEvent = selected[0] let selectedIdx narrative.steps.forEach((step, idx) => { if (selectedEvent.id === step.id) { selectedIdx = idx } }) return selectedIdx } ) /** 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, selectNarrativeIdx], (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}$_${event.latitude}_${event.longitude}` 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, getEventRadius], (events, features, eventRadius) => { if (!features.GRAPH_NONLOCATED) { return [events, []] } const projSize = 2 * eventRadius const projectIdx = features.GRAPH_NONLOCATED.projectIdx || 0 const getProject = ev => ev.filters[projectIdx] const projects = {} // get all projects events = events.reduce((acc, event) => { const project = event.filters.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 } } )