From 52ce273761ee3d24413598dce17b60013822d9f3 Mon Sep 17 00:00:00 2001 From: esikkala <esko.ikkala@aalto.fi> Date: Tue, 21 Dec 2021 14:04:28 +0200 Subject: [PATCH] Adapt to WarMemoirSampo configs --- .../facet_results/ResultClassRoute.js | 38 ++ .../components/main_layout/InstancePage.js | 1 + src/client/components/main_layout/MuiIcon.js | 6 +- .../components/main_layout/VideoPage.js | 150 +++++++ .../main_layout/VideoTableOfContents.js | 400 ++++++++++++++++++ .../components/main_layout/WordCloud.js | 67 +++ src/client/containers/SemanticPortal.js | 7 + src/client/reducers/general/videoPlayer.js | 14 + src/client/reducers/index.js | 2 + src/server/sparql/Mappers.js | 23 + src/server/sparql/Utils.js | 28 +- 11 files changed, 729 insertions(+), 7 deletions(-) create mode 100644 src/client/components/main_layout/VideoPage.js create mode 100644 src/client/components/main_layout/VideoTableOfContents.js create mode 100644 src/client/components/main_layout/WordCloud.js create mode 100644 src/client/reducers/general/videoPlayer.js diff --git a/src/client/components/facet_results/ResultClassRoute.js b/src/client/components/facet_results/ResultClassRoute.js index 391b2110..31b2f013 100644 --- a/src/client/components/facet_results/ResultClassRoute.js +++ b/src/client/components/facet_results/ResultClassRoute.js @@ -9,6 +9,8 @@ const LeafletMap = lazy(() => import('./LeafletMap')) const Deck = lazy(() => import('./Deck')) const ApexCharts = lazy(() => import('./ApexCharts')) const Network = lazy(() => import('./Network')) +const VideoPage = lazy(() => import('../main_layout/VideoPage')) +const WordCloud = lazy(() => import('../main_layout/WordCloud')) // const BarChartRace = lazy(() => import('../../facet_results/BarChartRace')) const ExportCSV = lazy(() => import('./ExportCSV')) const Export = lazy(() => import('./Export')) @@ -350,6 +352,42 @@ const ResultClassRoute = props => { ) break } + case 'VideoPage': { + const videoPageProps = { + portalConfig, + perspectiveConfig: perspective, + layoutConfig, + screenSize, + resultClass, + perspectiveState, + properties: getVisibleRows(perspectiveState), + localID: props.localID, + routeProps: props.routeProps, + videoPlayerState: props.videoPlayerState, + updateVideoPlayerTime: props.updateVideoPlayerTime + } + routeComponent = ( + <Route + path={path} + render={() => + <VideoPage {...videoPageProps} />} + /> + ) + break + } + case 'WordCloud': { + const wordCloudProps = { + data: perspectiveState.instanceTableData[resultClassConfig.wordCloudProperty] + } + routeComponent = ( + <Route + path={path} + render={() => + <WordCloud {...wordCloudProps} />} + /> + ) + break + } case 'Export': { const { pageType = 'facetResults' } = resultClassConfig const exportResultClass = resultClassConfig.resultClass diff --git a/src/client/components/main_layout/InstancePage.js b/src/client/components/main_layout/InstancePage.js index 9ab80038..0dc9b2a0 100644 --- a/src/client/components/main_layout/InstancePage.js +++ b/src/client/components/main_layout/InstancePage.js @@ -159,6 +159,7 @@ class InstancePage extends React.Component { defaultResultClass={resultClass} resultClass={instancePageResultClass} resultClassConfig={resultClassConfig} + localID={this.state.localID} {...this.props} /> ) diff --git a/src/client/components/main_layout/MuiIcon.js b/src/client/components/main_layout/MuiIcon.js index a24e4dc5..53c3bb86 100644 --- a/src/client/components/main_layout/MuiIcon.js +++ b/src/client/components/main_layout/MuiIcon.js @@ -11,7 +11,8 @@ import { BubbleChart, ShowChart, FormatAlignJustify, - ClearAll + ClearAll, + OndemandVideo } from '@material-ui/icons' import has from 'lodash' @@ -28,7 +29,8 @@ const MuiIcon = props => { BubbleChart: BubbleChart, ShowChart: ShowChart, FormatAlignJustify: FormatAlignJustify, - ClearAll: ClearAll + ClearAll: ClearAll, + OndemandVideo: OndemandVideo } if (has(MuiIcons, props.iconName)) { const MuiIconComponent = MuiIcons[props.iconName] diff --git a/src/client/components/main_layout/VideoPage.js b/src/client/components/main_layout/VideoPage.js new file mode 100644 index 00000000..1c357cb2 --- /dev/null +++ b/src/client/components/main_layout/VideoPage.js @@ -0,0 +1,150 @@ +import React from 'react' +import { makeStyles } from '@material-ui/core/styles' +import Paper from '@material-ui/core/Paper' +import Grid from '@material-ui/core/Grid' +import { Typography } from '@material-ui/core' +import InstancePageTable from './InstancePageTable' +import Player from './Player' +import VideoTableOfContents from './VideoTableOfContents' +import { has } from 'lodash' + +const useStyles = makeStyles(theme => ({ + root: { + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + fontFamily: 'Roboto, Helvetica, Arial, sans-serif', + backgroundColor: '#bdbdbd' + }, + mainContainer: props => ({ + margin: 0, + maxWidth: 1100, + // minHeight: 1100, + marginTop: theme.spacing(1), + // flexWrap: 'wrap-reverse', + [theme.breakpoints.up(props.layoutConfig.hundredPercentHeightBreakPoint)]: { + height: `calc(100% - ${theme.spacing(2.5)}px)` + } + }), + gridItem: props => ({ + [theme.breakpoints.up(props.layoutConfig.hundredPercentHeightBreakPoint)]: { + height: '100%' + }, + paddingTop: '0px !important', + paddingBottom: '0px !important' + }), + tableOfContents: props => ({ + padding: theme.spacing(2), + overflow: 'auto', + top: theme.spacing(0.5), + [theme.breakpoints.up(props.layoutConfig.hundredPercentHeightBreakPoint)]: { + height: 'calc(100% - 32px)' + } + }), + videoPlayerContainer: props => ({ + [theme.breakpoints.up(props.layoutConfig.hundredPercentHeightBreakPoint)]: { + height: '60%' + }, + [theme.breakpoints.down('sm')]: { + maxWidth: 500 + }, + overflow: 'auto', + marginBottom: theme.spacing(1), + display: 'flex', + alignItems: 'center' + }), + tableContainer: props => ({ + marginBottom: theme.spacing(1), + [theme.breakpoints.up(props.layoutConfig.hundredPercentHeightBreakPoint)]: { + height: `calc(40% - ${theme.spacing(1)}px)`, + overflow: 'auto' + } + }), + wordCloud: props => ({ + marginTop: theme.spacing(1), + padding: theme.spacing(2), + overflow: 'auto', + height: 200, + display: 'none', + [theme.breakpoints.up(props.layoutConfig.hundredPercentHeightBreakPoint)]: { + height: '40%', + display: 'block' + } + }), + wordCloudContainer: { + width: '100%' + }, + tooltip: { + maxWidth: 500 + }, + tooltipContent: { + padding: theme.spacing(1) + }, + tooltipList: { + listStylePosition: 'inside', + paddingLeft: 0 + } +})) + +const VideoPage = props => { + const classes = useStyles(props) + const { instanceTableData } = props.perspectiveState + const { portalConfig, perspectiveConfig, localID, resultClass, screenSize, layoutConfig } = props + let { properties } = props + + const readyToRenderVideoPlayer = () => { + return `http://ldf.fi/warmemoirsampo/${localID}` === instanceTableData.id && + has(instanceTableData, 'youTubeID') + } + + if (!has(instanceTableData, 'warsaPage')) { + properties = properties.filter(prop => prop.id !== 'warsaPage') + } + + return ( + <div className={classes.root}> + <Grid className={classes.mainContainer} container spacing={1}> + <Grid className={classes.gridItem} item xs={12} sm={12} md={7}> + <Paper className={classes.videoPlayerContainer}> + {readyToRenderVideoPlayer() && + <Player + resultClass={props.resultClass} + data={instanceTableData} + routeProps={props.routeProps} + videoPlayerState={props.videoPlayerState} + updateVideoPlayerTime={props.updateVideoPlayerTime} + />} + </Paper> + <Paper className={classes.tableContainer}> + <InstancePageTable + portalConfig={portalConfig} + perspectiveConfig={perspectiveConfig} + resultClass={resultClass} + data={instanceTableData} + properties={properties} + screenSize={screenSize} + layoutConfig={layoutConfig} + /> + </Paper> + </Grid> + <Grid className={classes.gridItem} item xs={12} sm={12} md={5}> + <Paper className={classes.tableOfContents}> + <Typography variant='h6' component='h2'>Sisällysluettelo</Typography> + {has(instanceTableData, 'timeSlice') && + <VideoTableOfContents + instanceTableData={instanceTableData} + toc={instanceTableData.timeSlice} + textFormat='plain-text-from-text-slice' + // textFormat='annotated-html-from-text-slice' + // textFormat='annotated-html-from-time-slice' + videoPlayerState={props.videoPlayerState} + />} + </Paper> + </Grid> + </Grid> + </div> + ) +} + +export default VideoPage diff --git a/src/client/components/main_layout/VideoTableOfContents.js b/src/client/components/main_layout/VideoTableOfContents.js new file mode 100644 index 00000000..6e7d2dc7 --- /dev/null +++ b/src/client/components/main_layout/VideoTableOfContents.js @@ -0,0 +1,400 @@ +import React from 'react' +import { withStyles } from '@material-ui/core/styles' +import Accordion from '@material-ui/core/Accordion' +import AccordionDetails from '@material-ui/core/AccordionDetails' +import AccordionSummary from '@material-ui/core/AccordionSummary' +import Typography from '@material-ui/core/Typography' +import ExpandMoreIcon from '@material-ui/icons/ExpandMore' +import Divider from '@material-ui/core/Divider' +import Tooltip from '@material-ui/core/Tooltip' +import { Link } from 'react-router-dom' +import { has } from 'lodash' +import parse from 'html-react-parser' +import { arrayToObject } from '../../helpers/helpers' + +const styles = theme => ({ + root: { + width: '100%', + marginTop: theme.spacing(1) + }, + heading: { + fontSize: theme.typography.pxToRem(15), + flexBasis: '33.33%', + flexShrink: 0 + }, + secondaryHeadingContainer: { + display: 'flex', + alignItems: 'center' + }, + secondaryHeading: { + fontSize: theme.typography.pxToRem(12), + color: theme.palette.text.secondary + }, + timeLink: { + marginRight: theme.spacing(1) + }, + activeAccordion: { + border: '2px solid red' + }, + accordionDetailsRoot: { + flexDirection: 'column' + }, + tocSubHeading: { + marginTop: theme.spacing(1) + }, + tooltip: { + maxWidth: 500 + }, + tooltipContent: { + padding: theme.spacing(1) + } +}) + +class VideoTableOfContents extends React.Component { + constructor (props) { + super(props) + const { + mentionedPlace, + mentionedPerson, + mentionedOrganization, + mentionedUnit, + mentionedEvent, + mentionedProduct + } = props.instanceTableData + let namedEntities = [ + ...(Array.isArray(mentionedPlace) + ? mentionedPlace + : [mentionedPlace]), + ...(Array.isArray(mentionedPerson) + ? mentionedPerson + : [mentionedPerson]), + ...(Array.isArray(mentionedOrganization) + ? mentionedOrganization + : [mentionedOrganization]), + ...(Array.isArray(mentionedUnit) + ? mentionedUnit + : [mentionedUnit]), + ...(Array.isArray(mentionedEvent) + ? mentionedEvent + : [mentionedEvent]), + ...(Array.isArray(mentionedProduct) + ? mentionedProduct + : [mentionedProduct]) + ] + if (namedEntities !== null) { + namedEntities = arrayToObject({ + array: namedEntities, + keyField: 'id' + }) + } + this.state = { + expandedSet: new Set([]), + currentPart: null, + namedEntities + } + } + + componentDidUpdate = (prevProps, prevState) => { + const currentPart = this.getCurrentPart() + if (this.props.videoPlayerState.videoPlayerTime !== prevProps.videoPlayerState.videoPlayerTime) { + this.setState({ currentPart }) + } + } + + getCurrentPart = () => { + const { videoPlayerTime } = this.props.videoPlayerState + let currentPart = null + let toc_ = this.props.toc + if (!Array.isArray(toc_)) { + toc_ = [this.props.toc] + } + for (const part of toc_) { + if (part.beginTimeInSeconds <= videoPlayerTime && part.endTimeInSeconds > videoPlayerTime) { + currentPart = part + break // there are errors in timecodes, choose only the first part that fits the condition + } + } + return currentPart + } + + renderTooltip = (domNode, namedEntityID) => { + let tooltipContent = namedEntityID + if (has(this.state.namedEntities, namedEntityID)) { + const entity = this.state.namedEntities[namedEntityID] + const tooltipHeading = has(entity, 'wikipediaLink') + ? ( + <p> + <a href={entity.wikipediaLink} target='_blank' rel='noopener noreferrer'> + {entity.prefLabel} (Wikipedia) + </a> + </p> + ) + : (<p>{entity.prefLabel} (Wikipedia)</p>) + tooltipContent = ( + <div className={this.props.classes.tooltipContent}> + {tooltipHeading} + <p> + {entity.description} + </p> + </div> + ) + } + return ( + <Tooltip + title={tooltipContent} + interactive + placement='top' + arrow + classes={{ + tooltip: this.props.classes.tooltip + }} + > + <span + style={{ + textDecoration: 'underline', + cursor: 'pointer' + }} + > + {domNode.children[0].data} + </span> + </Tooltip> + ) + } + + renderLink = (domNode, namedEntityID) => { + if (has(this.state.namedEntities, namedEntityID)) { + const entity = this.state.namedEntities[namedEntityID] + return ( + <Link to={entity.dataProviderUrl}> + {domNode.children[0].data} + </Link> + ) + } else { + return ( + <> + {domNode.children[0].data} + </> + ) + } + } + + parseHTMLTextSlice = slice => { + const html = parse(slice.annotatedTextContent, { + replace: domNode => { + if (domNode.type === 'tag' && domNode.name === 'span' && + has(domNode.attribs, 'data-link')) { + const namedEntityID = domNode.attribs['data-link'] + return this.renderTooltip(domNode, namedEntityID) + } + } + }) + return ( + <li key={slice.order}>{html}</li> + ) + } + + parseHTMLTimeSlice = annotatedTextContent => { + const html = parse(annotatedTextContent, { + replace: domNode => { + if (domNode.type === 'tag' && domNode.name === 'span' && + has(domNode.attribs, 'data-uri')) { + const namedEntityID = domNode.attribs['data-uri'] + return this.renderLink(domNode, namedEntityID) + } + } + }) + return ( + <p> + {html} + </p> + ) + } + + handleAccordionOnChange = rowID => () => { + const { expandedSet } = this.state + if (expandedSet.has(rowID)) { + expandedSet.delete(rowID) + } else { + expandedSet.add(rowID) + } + this.setState({ expandedSet }) + } + + render () { + const { classes, toc, textFormat } = this.props + const { expandedSet } = this.state + let toc_ = toc + if (!Array.isArray(toc)) { + toc_ = [toc] + } + return ( + <div className={classes.root}> + {toc_.map(row => { + const rowID = row.order + let isCurrent = false + if (this.state.currentPart && rowID === this.state.currentPart.order) { + isCurrent = true + } + const expanded = expandedSet.has(rowID) || isCurrent + const hasPlaceLinks = has(row, 'mentionedPlace') + const hasPersonLinks = has(row, 'mentionedPerson') + const hasUnitLinks = has(row, 'mentionedUnit') + const hasOrganizationLinks = has(row, 'mentionedOrganization') + const hasEventLinks = has(row, 'mentionedEvent') + const hasProductLinks = has(row, 'mentionedProduct') + const hasNamedEntityLinks = + hasPlaceLinks || + hasPersonLinks || + hasUnitLinks || + hasOrganizationLinks || + hasEventLinks || + hasProductLinks + const hasTextSlices = has(row, 'textSlice') + if (hasPlaceLinks) { + if (Array.isArray(row.mentionedPlace)) { + row.mentionedPlace.forEach(place => { + if (Array.isArray(place.prefLabel)) { + place.prefLabel = place.prefLabel[0] + } + }) + row.mentionedPlace.sort((a, b) => a.prefLabel.localeCompare(b.prefLabel)) + } + } + const timeSliceHasAnnotatedTextContent = has(row, 'annotatedTextContent') + return ( + <Accordion + className={isCurrent ? classes.activeAccordion : null} + key={rowID} + expanded={expanded} + onChange={this.handleAccordionOnChange(rowID)} + > + <AccordionSummary + style={{ + root: { + '&$expanded': { minHeight: 15 } + }, + content: { + '&$expanded': { marginBottom: 0 } + } + }} + expandIcon={<ExpandMoreIcon />} + IconButtonProps={{ + disabled: isCurrent + }} + aria-label='Expand' + aria-controls={`${rowID}-content`} + id={`${rowID}-header`} + > + <Link + className={classes.timeLink} + to={{ hash: row.beginTimeInSeconds }} + replace + onClick={event => { + if (expanded) { + event.stopPropagation() + } + }} + onFocus={event => event.stopPropagation()} + > + <Typography className={classes.heading}> + {row.beginTimeLabel} + </Typography> + </Link> + {!expanded && + <div className={classes.secondaryHeadingContainer}> + <Typography className={classes.secondaryHeading}>{row.prefLabel}</Typography> + </div>} + + </AccordionSummary> + <AccordionDetails + classes={{ + root: classes.accordionDetailsRoot + }} + > + <Typography>Haastattelijan muistiinpanot</Typography> + {textFormat === 'plain-text-from-text-slice' && hasTextSlices && + <ul> + {Array.isArray(row.textSlice) + ? row.textSlice.map(slice => <li key={slice.order}>{slice.textContent}</li>) + : <li key={row.textSlice.order}>{row.textSlice.textContent}</li>} + </ul>} + {textFormat === 'annotated-html-from-text-slice' && hasTextSlices && + <ul> + {Array.isArray(row.textSlice) + ? row.textSlice.map(slice => this.parseHTMLTextSlice(slice)) + : this.parseHTMLTextSlice(row.textSlice)} + </ul>} + {textFormat === 'annotated-html-from-time-slice' && timeSliceHasAnnotatedTextContent && + this.parseHTMLTimeSlice(row.annotatedTextContent)} + {hasNamedEntityLinks && + <> + <Divider /> + <Typography className={classes.tocSubHeading}>Automaattisesti tunnistetut</Typography> + <ul> + {hasPlaceLinks && + <li>paikat + <ul> + {Array.isArray(row.mentionedPlace) + ? row.mentionedPlace.map(place => + <li key={place.id}><Link to={place.dataProviderUrl}>{place.prefLabel}</Link></li>) + : <li key={row.mentionedPlace.id}><Link to={row.mentionedPlace.dataProviderUrl}>{row.mentionedPlace.prefLabel}</Link></li>} + </ul> + </li>} + {hasPersonLinks && + <li>henkilöt + <ul> + {Array.isArray(row.mentionedPerson) + ? row.mentionedPerson.map(person => + <li key={person.id}><Link to={person.dataProviderUrl}>{person.prefLabel}</Link></li>) + : <li key={row.mentionedPerson.id}><Link to={row.mentionedPerson.dataProviderUrl}>{row.mentionedPerson.prefLabel}</Link></li>} + </ul> + </li>} + {hasUnitLinks && + <li>joukko-osastot + <ul> + {Array.isArray(row.mentionedUnit) + ? row.mentionedUnit.map(unit => + <li key={unit.id}><Link to={unit.dataProviderUrl}>{unit.prefLabel}</Link></li>) + : <li key={row.mentionedUnit.id}><Link to={row.mentionedUnit.dataProviderUrl}>{row.mentionedUnit.prefLabel}</Link></li>} + </ul> + </li>} + {hasOrganizationLinks && + <li>organisaatiot + <ul> + {Array.isArray(row.mentionedOrganization) + ? row.mentionedOrganization.map(organization => + <li key={organization.id}><Link to={organization.dataProviderUrl}>{organization.prefLabel}</Link></li>) + : <li key={row.mentionedOrganization.id}><Link to={row.mentionedOrganization.dataProviderUrl}>{row.mentionedOrganization.prefLabel}</Link></li>} + </ul> + </li>} + {hasEventLinks && + <li>tapahtumat + <ul> + {Array.isArray(row.mentionedEvent) + ? row.mentionedEvent.map(event => + <li key={event.id}><Link to={event.dataProviderUrl}>{event.prefLabel}</Link></li>) + : <li key={row.mentionedEvent.id}><Link to={row.mentionedEvent.dataProviderUrl}>{row.mentionedEvent.prefLabel}</Link></li>} + </ul> + </li>} + {hasProductLinks && + <li>nimikkeet + <ul> + {Array.isArray(row.mentionedProduct) + ? row.mentionedProduct.map(product => + <li key={product.id}><Link to={product.dataProviderUrl}>{product.prefLabel}</Link></li>) + : <li key={row.mentionedProduct.id}><Link to={row.mentionedProduct.dataProviderUrl}>{row.mentionedProduct.prefLabel}</Link></li>} + </ul> + </li>} + </ul> + </>} + </AccordionDetails> + </Accordion> + ) + } + )} + </div> + ) + } +} + +export default withStyles(styles)(VideoTableOfContents) diff --git a/src/client/components/main_layout/WordCloud.js b/src/client/components/main_layout/WordCloud.js new file mode 100644 index 00000000..284335c8 --- /dev/null +++ b/src/client/components/main_layout/WordCloud.js @@ -0,0 +1,67 @@ +import React from 'react' +import ReactWordcloud from 'react-wordcloud' +import { makeStyles } from '@material-ui/core/styles' +import Paper from '@material-ui/core/Paper' +import 'tippy.js/dist/tippy.css' +import 'tippy.js/animations/scale.css' + +const options = { + rotations: 0, + fontSizes: [14, 60], + deterministic: true +} + +const useStyles = makeStyles(theme => ({ + wordCloudOuterContainer: props => ({ + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#bdbdbd' + }), + wordCloudInnerContainer: props => ({ + width: '100%', + height: '100%', + [theme.breakpoints.down('md')]: { + minHeight: 400, + overflow: 'auto' + }, + [theme.breakpoints.up('lg')]: { + width: '50%', + height: '50%' + } + }) +})) + +const Wordcloud = props => { + const { data, maxWords } = props + const classes = useStyles(props) + + if (data == null) { + return (<></>) + } + + data.forEach(el => { + el.weight = +el.weight + }) + + // sort without mutating the original array + let words = [...data].sort((a, b) => b.weight - a.weight) + if (words.length > maxWords) { + words.splice(maxWords) + } + words = words.map(item => ({ text: item.prefLabel, value: item.weight })) + + return ( + <div className={classes.wordCloudOuterContainer}> + <Paper className={classes.wordCloudInnerContainer}> + <ReactWordcloud + options={options} + words={words} + /> + </Paper> + </div> + ) +} + +export default Wordcloud diff --git a/src/client/containers/SemanticPortal.js b/src/client/containers/SemanticPortal.js index 92a37798..2f1095ba 100644 --- a/src/client/containers/SemanticPortal.js +++ b/src/client/containers/SemanticPortal.js @@ -38,6 +38,7 @@ import { updatePerspectiveHeaderExpanded, loadLocales, animateMap, + updateVideoPlayerTime, clientFSToggleDataset, clientFSFetchResults, clientFSSortResults, @@ -472,6 +473,8 @@ const SemanticPortal = props => { perspective={perspective} animationValue={props.animationValue} animateMap={props.animateMap} + videoPlayerState={props.videoPlayer} + updateVideoPlayerTime={props.updateVideoPlayerTime} screenSize={screenSize} rootUrl={rootUrlWithLang} apexChartsConfig={apexChartsConfig} @@ -542,6 +545,8 @@ const SemanticPortal = props => { perspective={perspective} animationValue={props.animationValue} animateMap={props.animateMap} + videoPlayerState={props.videoPlayer} + updateVideoPlayerTime={props.updateVideoPlayerTime} screenSize={screenSize} rootUrl={rootUrlWithLang} apexChartsConfig={apexChartsConfig} @@ -677,6 +682,7 @@ const mapStateToProps = state => { stateToProps.leafletMap = state.leafletMap stateToProps.fullTextSearch = state.fullTextSearch stateToProps.animationValue = state.animation.value + stateToProps.videoPlayer = state.videoPlayer stateToProps.options = state.options stateToProps.error = state.error return stateToProps @@ -707,6 +713,7 @@ const mapDispatchToProps = ({ updatePerspectiveHeaderExpanded, loadLocales, animateMap, + updateVideoPlayerTime, clientFSToggleDataset, clientFSFetchResults, clientFSClearResults, diff --git a/src/client/reducers/general/videoPlayer.js b/src/client/reducers/general/videoPlayer.js new file mode 100644 index 00000000..9233d9e7 --- /dev/null +++ b/src/client/reducers/general/videoPlayer.js @@ -0,0 +1,14 @@ +import { UPDATE_VIDEO_PLAYER_TIME } from '../../actions' + +export const INITIAL_STATE = { + videoPlayerTime: null +} + +const videoPlayer = (state = INITIAL_STATE, action) => { + if (action.type === UPDATE_VIDEO_PLAYER_TIME) { + state = { ...state, videoPlayerTime: action.value } + } + return state +} + +export default videoPlayer diff --git a/src/client/reducers/index.js b/src/client/reducers/index.js index 061b837b..0cf0a4de 100644 --- a/src/client/reducers/index.js +++ b/src/client/reducers/index.js @@ -10,6 +10,7 @@ import { createFullTextSearchReducer } from './general/fullTextSearch' import error from './general/error' import options from './general/options' import animation from './general/animation' +import videoPlayer from './general/videoPlayer' import leafletMap from './general/leafletMap' import { resultsInitialState, @@ -21,6 +22,7 @@ import { const reducers = { leafletMap, animation, + videoPlayer, options, error, toastr: toastrReducer diff --git a/src/server/sparql/Mappers.js b/src/server/sparql/Mappers.js index a927f3eb..04dc9b6e 100644 --- a/src/server/sparql/Mappers.js +++ b/src/server/sparql/Mappers.js @@ -358,6 +358,29 @@ const getChoroplethMapColor = ({ value, clusters }) => { return heatmapColor } +export const createPaddedTimeCodes = ({ data, config }) => { + data.forEach(item => { + let target = item[config.target] + if (!Array.isArray(target)) { + target = [target] + } + target.forEach(targetItem => { + const { hours, minutes, seconds } = targetItem + if (hours == null || minutes == null || seconds == null) { + // console.log(targetItem) + } else { + const paddedTimecode = createPaddedTimeCode({ hours, minutes, seconds }) + targetItem[config.timeCodeProperty] = paddedTimecode + } + }) + }) + return data +} + +const createPaddedTimeCode = ({ hours, minutes, seconds }) => { + return `${hours}:${minutes.padStart(2, '0')}:${seconds.padStart(2, '0')}` +} + // const NS_PER_SEC = 1e9 // const MS_PER_NS = 1e-6 diff --git a/src/server/sparql/Utils.js b/src/server/sparql/Utils.js index 0163b4c2..7d92bc72 100644 --- a/src/server/sparql/Utils.js +++ b/src/server/sparql/Utils.js @@ -1,12 +1,14 @@ import { readFile } from 'fs/promises' import { has } from 'lodash' -// import { backendSearchConfig as oldBackendSearchConfig } from './findsampo/BackendSearchConfig' -// import { findsPerspectiveConfig } from './findsampo/perspective_configs/FindsPerspectiveConfig' + +// import { backendSearchConfig as oldBackendSearchConfig } from './veterans/BackendSearchConfig' + +// import { videosConfig } from './veterans/perspective_configs/VideosConfig' // import { typesPerspectiveConfig } from './perspective_configs/TypesPerspectiveConfig' // import { periodsPerspectiveConfig } from './perspective_configs/PeriodsPerspectiveConfig' // import { coinsPerspectiveConfig } from './perspective_configs/CoinsPerspectiveConfig' -// import { INITIAL_STATE } from '../../client/reducers/findsampo/findsFacets' +// import { INITIAL_STATE } from '../../client/reducers/veterans/videosFacets' // import { INITIAL_STATE } from '../../client/reducers/findsampo/finds' export const createBackendSearchConfig = async () => { @@ -36,6 +38,9 @@ export const createBackendSearchConfig = async () => { const instancePagePropertiesQueryBlock = sparqlQueries[instancePagePropertiesQueryBlockID] paginatedResultsConfig.propertiesQueryBlock = paginatedResultsPropertiesQueryBlock instanceConfig.propertiesQueryBlock = instancePagePropertiesQueryBlock + if (instanceConfig.postprocess) { + instanceConfig.postprocess.func = resultMappers[instanceConfig.postprocess.func] + } if (has(instanceConfig, 'instancePageResultClasses')) { for (const instancePageResultClass in instanceConfig.instancePageResultClasses) { const instancePageResultClassConfig = instanceConfig.instancePageResultClasses[instancePageResultClass] @@ -78,6 +83,10 @@ export const createBackendSearchConfig = async () => { const instancePagePropertiesQueryBlockID = instanceConfig.propertiesQueryBlock const instancePagePropertiesQueryBlock = sparqlQueries[instancePagePropertiesQueryBlockID] instanceConfig.propertiesQueryBlock = instancePagePropertiesQueryBlock + console.log(instanceConfig) + if (instanceConfig.postprocess) { + instanceConfig.postprocess.func = resultMappers[instanceConfig.postprocess.func] + } let hasInstancePageResultClasses = false if (has(instanceConfig, 'instancePageResultClasses')) { for (const instancePageResultClass in instanceConfig.instancePageResultClasses) { @@ -196,6 +205,9 @@ export const mergeFacetConfigs = (clientFacets, serverFacets) => { if (serverFacet.facetValueFilter && serverFacet.facetValueFilter !== '') { mergedFacet.facetValueFilter = serverFacet.facetValueFilter } + if (serverFacet.facetLabelFilter && serverFacet.facetLabelFilter !== '') { + mergedFacet.facetLabelFilter = serverFacet.facetLabelFilter + } if (has(serverFacet, 'literal')) { mergedFacet.literal = serverFacet.literal } @@ -209,6 +221,9 @@ export const mergeFacetConfigs = (clientFacets, serverFacets) => { if (serverFacet.facetValueFilter && serverFacet.facetValueFilter !== '') { mergedFacet.facetValueFilter = serverFacet.facetValueFilter } + if (serverFacet.facetLabelFilter && serverFacet.facetLabelFilter !== '') { + mergedFacet.facetLabelFilter = serverFacet.facetLabelFilter + } mergedFacet.facetType = 'hierarchical' mergedFacet.predicate = serverFacet.predicate mergedFacet.parentProperty = serverFacet.parentProperty @@ -249,7 +264,7 @@ export const mergeFacetConfigs = (clientFacets, serverFacets) => { mergedFacets[facetID] = orderedFacet } // console.log(mergedFacets) - // console.log(JSON.stringify(mergedFacets)) + console.log(JSON.stringify(mergedFacets)) } export const createExtraResultClassesForJSONConfig = async oldBackendSearchConfig => { @@ -312,8 +327,11 @@ export const createExtraResultClassesForJSONConfig = async oldBackendSearchConfi } // createExtraResultClassesForJSONConfig(oldBackendSearchConfig) -// mergeFacetConfigs(INITIAL_STATE.facets, findsPerspectiveConfig.facets) + +// mergeFacetConfigs(INITIAL_STATE.facets, videosConfig.facets) + // console.log(JSON.stringify(INITIAL_STATE.properties)) + // "tabID": 0, // "tabPath": "", // "tabIcon": "", -- GitLab