import { useMemo } from 'react'
import useSWR from 'swr'
import { findCounty, findIn } from '../utils'
import bbox from '@turf/bbox'
import { randomPosition } from '@turf/random'
import booleanPointInPolygon from '@turf/boolean-point-in-polygon'
import { useSelector } from 'react-redux'
import { useGeoda } from '../contexts/Geoda'
import useGetGeojson from './useGetGeojson'
import { paramsSelectors } from '../stores/paramsStore'
const { selectDatasets } = paramsSelectors
/**
* Helper function to filter stories
*
* @category Utils/Stories
* @param {StoryMeta} story Story meta to check
* @param {StoryFilter} filter Filter spec to apply
* @returns {boolean} If the story passes the filter
*/
const doFilter = (story, filter) => {
return filter.every(({ property, value, operation }) => {
const storyValue = story[property]
if (operation === 'match') {
return storyValue === value
} else if (operation === 'contains') {
return storyValue.includes(value)
} else if (operation === 'not') {
return storyValue !== value
} else {
return false
}
})
}
/**
* Summarizes coutns fo theme, state, urbanicitiy, type, and tags on story
* metadata.
*
* @category Utils/Stories
* @param {StoryMeta[]} stories List of stories
* @returns {Object<string, number>} Dictionary of counts of each property
*/
const getCounts = (stories) => {
let counts = {
theme: {},
tags: {},
state: {},
urbanicity: {},
type: {},
county: {},
}
if (!stories) return counts
for (let i = 0; i < stories.length; i++) {
const story = stories[i]
const { state, county, theme, tags, urbanicity, type } = story
theme && (counts.theme[theme] = (counts.theme[theme] || 0) + 1)
state && (counts.state[state] = (counts.state[state] || 0) + 1)
urbanicity &&
(counts.urbanicity[urbanicity] =
(counts.urbanicity[urbanicity] || 0) + 1)
type && (counts.type[type] = (counts.type[type] || 0) + 1)
county && (counts.county[county] = (counts.county[county] || 0) + 1)
if (tags) {
for (let j = 0; j < story.tags.length; j++) {
const tag = story.tags[j]
counts.tags[tag] = (counts.tags[tag] || 0) + 1
}
}
}
return counts
}
/**
* Returns a utiliy function to generate a random point within a polygon
*
* @category Hooks
* @returns {Object<{ ready: boolean, getRandomPoint: Function }>} ready state and getRandomPoint function (geoid: number) => [lng, lat]
*/
function useCentroidRandomizer() {
const datasets = useSelector(selectDatasets)
const { geoda, geodaReady } = useGeoda()
const usafactsDataset = findIn(datasets, 'file', 'county_usfacts.geojson')
const [geo] = useGetGeojson({
geoda,
geodaReady,
currDataset: usafactsDataset,
})
const getPoint = (bounds, geog) => {
const xy = randomPosition(bounds)
const point = {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: xy,
},
}
if (booleanPointInPolygon(point, geog)) {
return xy
} else {
return getPoint(bounds, geog)
}
}
const getRandomPoint = (geoid) => {
if (geo?.data) {
const geog = geo.data.features.find(
(f) => f.properties.GEOID === geoid
)
const bounds = bbox(geog.geometry)
const point = getPoint(bounds, geog)
return point
}
}
return {
ready: !!geo?.data,
getRandomPoint,
}
}
/**
* @typedef {Object} StoryFilter
* @property {string} property - Name of property to filter against
* @property {string} value - Value of property to filter
* @property {string} operation - Filter operation - 'match' | 'contains' |
* 'not'
*/
/**
* @typedef {Object} UseStoriesReturn
* @property {StoryMeta[]} stories List of all stories meta information
* @property {Object<string, number>} counts Counts of various properties for
* filtered stories
* @property {StoryMeta[]} relatedStories List of related stories meta
* information
* @property {StoryMeta} activeStory Selected stories meta information
*/
/**
* Hook to get stories from the API
*
* @category Hooks
* @param {Object} props
* @param props.selectedStory = {},
* @param {StoryFilter[]} props.filters Array of filters to apply to stories
* @param {string} props.singleStoryId - ID of a single story to fetch
* @returns {UseStoriesReturn}
*/
function useStories({ selectedStory = {}, filters = [], singleStoryId = '' }) {
const { ready: centroidReady, getRandomPoint } = useCentroidRandomizer()
const fetcher = centroidReady
? (url) =>
fetch(url)
.then((r) => r.json())
.then((rows) =>
rows.map((row) => ({
...row,
...findCounty(row.fips),
centroid: getRandomPoint(row.fips),
}))
)
.catch((err) => console.log(err))
: () => []
const fetchName = centroidReady
? `${process.env.REACT_APP_STORIES_PUBLIC_URL}/index.json`
: 'null-data'
const { data: allStories, error } = useSWR(fetchName, fetcher)
const { counts, stories } = useMemo(() => {
if (error) {
return {
counts: {},
stories: [],
}
}
if (!allStories) {
return {
counts: {},
stories: [],
}
}
if (!filters.length) {
return {
counts: getCounts(allStories),
stories: allStories,
}
}
const stories = allStories.filter((story) => doFilter(story, filters))
const counts = getCounts(stories)
return {
counts,
stories,
}
}, [JSON.stringify({allStories,filters})])
const activeStory = useMemo(() => {
if (!allStories?.length) {
return {}
}
if (singleStoryId) {
return allStories.find((story) => story.id === singleStoryId)
}
return selectedStory
}, [allStories?.length, JSON.stringify({ singleStoryId, selectedStory })])
const relatedStories = useMemo(() => {
if (error) {
return []
}
if (!allStories) {
return []
}
if (!activeStory?.id) {
return []
}
const tags = activeStory.tags
const county = activeStory.county
const theme = activeStory.theme
const state = activeStory.county.split(',').slice(-1)[0]
return allStories
.map((story) => {
if (story.id === activeStory.id) {
return false
}
let matchCriteria = 0
if (story.theme === theme) matchCriteria++
if (story.county === county) matchCriteria++
if (story.county.includes(state)) matchCriteria++
story.tags.forEach(
(tag) => tags.includes(tag) && matchCriteria++
)
return (
matchCriteria && {
...story,
matchCriteria,
}
)
})
.filter((story) => story)
.sort((a, b) => b.matchCriteria - a.matchCriteria)
}, [JSON.stringify(activeStory), allStories?.length])
return {
stories,
counts,
relatedStories,
activeStory,
}
}
export { useStories }
Source