diff --git a/LICENSE b/LICENSE index 85ec0235b4efad3ca03f3c51a63ec5458ad7e268..824a4fbde8468411db007a66412dd348e156bd3a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Semantic Computing Research Group (SeCo) +Copyright (c) 2021 Semantic Computing Research Group (SeCo) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/client/actions/index.js b/src/client/actions/index.js index d496cc5df43bf277681185f0603b2796a3d3a187..fb16620bcff578e88d2fa459a6cd940f3b55a557 100644 --- a/src/client/actions/index.js +++ b/src/client/actions/index.js @@ -387,8 +387,9 @@ export const clientFSSortResults = options => ({ type: CLIENT_FS_SORT_RESULTS, options }) -export const fetchKnowledgeGraphMetadata = ({ resultClass }) => ({ +export const fetchKnowledgeGraphMetadata = ({ perspectiveID, resultClass }) => ({ type: FETCH_KNOWLEDGE_GRAPH_METADATA, + perspectiveID, resultClass }) export const fetchKnowledgeGraphMetadataFailded = (resultClass, error, message) => ({ diff --git a/src/client/components/App.js b/src/client/components/App.js index 074e05d723f0e2fd9c316af7e64d97a300af4113..15d5caeae448e4859362c3564910734ef59a897e 100644 --- a/src/client/components/App.js +++ b/src/client/components/App.js @@ -1,14 +1,16 @@ import React from 'react' import { MuiThemeProvider, createTheme } from '@material-ui/core/styles' import SemanticPortal from '../containers/SemanticPortal' +import portalConfig from '../../configs/portalConfig.json' +const { colorPalette } = portalConfig.layoutConfig const theme = createTheme({ palette: { primary: { - main: '#212121' + main: colorPalette.primary.main }, secondary: { - main: '#EB1806' + main: colorPalette.secondary.main } }, overrides: { diff --git a/src/client/components/facet_results/LeafletMap.js b/src/client/components/facet_results/LeafletMap.js index f257b9b0df464f629c406b67c1f0669a134f4a1a..55e09216ae765352af916597e14c1a1b3ff0eb63 100644 --- a/src/client/components/facet_results/LeafletMap.js +++ b/src/client/components/facet_results/LeafletMap.js @@ -117,6 +117,7 @@ class LeafletMap extends React.Component { if (this.props.mapMode && (this.props.pageType === 'facetResults' || this.props.pageType === 'instancePage')) { this.props.fetchResults({ + perspectiveID: this.props.perspectiveConfig.id, resultClass: this.props.resultClass, facetClass: this.props.facetClass, sortBy: null, @@ -166,6 +167,7 @@ class LeafletMap extends React.Component { // check if filters have changed if (has(prevProps, 'facetUpdateID') && prevProps.facetUpdateID !== this.props.facetUpdateID) { this.props.fetchResults({ + perspectiveID: this.props.perspectiveConfig.id, resultClass: this.props.resultClass, facetClass: this.props.facetClass, sortBy: null, @@ -181,6 +183,7 @@ class LeafletMap extends React.Component { // check if map mode has changed if (prevState.mapMode !== this.state.mapMode) { this.props.fetchResults({ + perspectiveID: this.props.perspectiveConfig.id, resultClass: this.props.resultClass, facetClass: this.props.facetClass, sortBy: null diff --git a/src/client/components/facet_results/ReactVirtualizedList.js b/src/client/components/facet_results/ReactVirtualizedList.js new file mode 100644 index 0000000000000000000000000000000000000000..8dd0cf597c18c712d29be51bc7f63a78c246ef90 --- /dev/null +++ b/src/client/components/facet_results/ReactVirtualizedList.js @@ -0,0 +1,194 @@ +import React, { useEffect } from 'react' +import { List, AutoSizer } from 'react-virtualized' +import { makeStyles } from '@material-ui/core/styles' +import Card from '@material-ui/core/Card' +import CardActionArea from '@material-ui/core/CardActionArea' +import CardContent from '@material-ui/core/CardContent' +import CardMedia from '@material-ui/core/CardMedia' +import Typography from '@material-ui/core/Typography' +import intl from 'react-intl-universal' +import { Link } from 'react-router-dom' +import CircularProgress from '@material-ui/core/CircularProgress' +import purple from '@material-ui/core/colors/purple' + +const useStyles = makeStyles(theme => ({ + root: props => { + return { + marginTop: theme.spacing(1), + maxWidth: 350, + height: window.innerHeight - props.layoutConfig.topBar.reducedHeight - props.layoutConfig.tabHeight - 139, + [theme.breakpoints.up(600)]: { + height: window.innerHeight - props.layoutConfig.topBar.reducedHeight - props.layoutConfig.tabHeight - 256 + }, + [theme.breakpoints.up(props.layoutConfig.hundredPercentHeightBreakPoint)]: { + height: window.innerHeight - props.layoutConfig.topBar.reducedHeight - props.layoutConfig.tabHeight - 178 + }, + [theme.breakpoints.up(1100)]: { + height: window.innerHeight - props.layoutConfig.topBar.reducedHeight - props.layoutConfig.tabHeight - 196 + }, + [theme.breakpoints.up(props.layoutConfig.reducedHeightBreakpoint)]: { + height: window.innerHeight - props.layoutConfig.topBar.reducedHeight - props.layoutConfig.tabHeight - 265 + }, + fontFamily: 'Roboto' + } + }, + list: { + [theme.breakpoints.up('md')]: { + paddingRight: 4 + } + }, + link: { + textDecoration: 'none' + }, + progressContainer: { + width: '100%', + height: 600, + [theme.breakpoints.up('md')]: { + height: 'calc(100% - 80px)' + }, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } +})) + +const ReactVirtualizedList = props => { + const classes = useStyles(props) + const { results } = props.perspectiveState + + useEffect(() => { + props.fetchResults({ + resultClass: props.resultClass, + facetClass: props.facetClass + }) + }, []) + + useEffect(() => { + const { facetUpdateID } = props + if (facetUpdateID > 0) { + props.fetchResults({ + resultClass: props.resultClass, + facetClass: props.facetClass + }) + } + }, [props.facetUpdateID]) + + const rowRenderer = ({ + key, // Unique key within array of rows + index, // Index of row within collection + isScrolling, // The List is currently being scrolled + isVisible, // This row is visible within the List (eg it is not an overscanned row) + style // Style object to be applied to row (to position it) + }) => { + const data = props.perspectiveState.results[index] + let image = null + if (data.imageURL) { + const { imageURL } = data + image = imageURL.includes(', ') ? imageURL.split(', ')[0] : imageURL + } + return ( + <div className={classes.rowRoot} key={key} style={style}> + <Link className={classes.link} to={data.dataProviderUrl}> + <Card> + <CardActionArea> + {image && + <CardMedia + component='img' + alt='Kuva löydöstä' + height='140' + image={image} + title='Kuva löydöstä' + />} + <CardContent> + <Typography gutterBottom variant='h5' component='h2'> + {data.findName} + </Typography> + <Typography variant='body2' color='textSecondary' component='p'> + <strong>{intl.get('perspectives.finds.properties.objectType.label')}: </strong> + {data.objectType} + </Typography> + <Typography variant='body2' color='textSecondary' component='p'> + <strong>{intl.get('perspectives.finds.properties.material.label')}: </strong> + {data.material} + </Typography> + <Typography variant='body2' color='textSecondary' component='p'> + <strong>{intl.get('perspectives.finds.properties.period.label')}: </strong> + {data.period} + </Typography> + <Typography variant='body2' color='textSecondary' component='p'> + <strong>{intl.get('perspectives.finds.properties.municipality.label')}: </strong> + {data.municipality} + </Typography> + </CardContent> + </CardActionArea> + </Card> + </Link> + + </div> + ) + } + + const getRowHeight = ({ index }) => { + const data = props.perspectiveState.results[index] + let height = 300 + if (!data.imageURL) { + height -= 140 + } + if (data.findName.length > 26) { + height += 32 + } + if (data.findName.length > 40) { + height += 54 + } + if (data.period) { + const limit = window.innerWidth < 328 ? 25 : 34 + if (data.period.length > limit) { + height += 20 + } + } + return height + } + + const validResults = () => { + const { results, resultClass } = props.perspectiveState + if (resultClass !== props.resultClass) { return false } + if (results == null) { return false } + if (results.length < 1) { return false } + return true + } + + // if (props.perspectiveState.results) { + // props.perspectiveState.results.map(r => { + // if (r.period && r.period.length > 33) { + // console.log(r) + // } + // }) + // } + + return ( + <div className={classes.root}> + {(!validResults() || props.perspectiveState.results.fetching) + ? ( + <div className={classes.progressContainer}> + <CircularProgress style={{ color: purple[500] }} thickness={5} /> + </div> + ) + : ( + <AutoSizer> + {({ height, width }) => ( + <List + className={classes.list} + height={height} + width={width} + rowCount={results.length} + rowHeight={getRowHeight} + rowRenderer={rowRenderer} + /> + )} + </AutoSizer> + )} + </div> + ) +} + +export default ReactVirtualizedList diff --git a/src/client/components/facet_results/ResultClassRoute.js b/src/client/components/facet_results/ResultClassRoute.js index cb944843df64dd36f2b85f1c45dae7926ffbb7d8..31b2f0137d240f1b385e20df4e8e6da388e1babf 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')) @@ -86,19 +88,36 @@ const ResultClassRoute = props => { ) break case 'InstancePageTable': { + const properties = resultClassConfig.properties + ? resultClassConfig.properties + : getVisibleRows(perspectiveState) + let instanceTableProps = { + portalConfig, + perspectiveConfig: perspective, + layoutConfig, + resultClass, + fetchResults: props.fetchResults, + properties, + screenSize + } + if (resultClassConfig.fetchResultsWhenMounted) { + instanceTableProps = { + ...instanceTableProps, + fetchResultsWhenMounted: true, + data: perspectiveState.results ? perspectiveState.results[0] : null, + uri: perspectiveState.instanceTableData.id, + resultUpdateID: perspectiveState.resultUpdateID + } + } else { + instanceTableProps = { + ...instanceTableProps, + data: perspectiveState.instanceTableData + } + } routeComponent = ( <Route path={path} - render={routeProps => - <InstancePageTable - portalConfig={portalConfig} - perspectiveConfig={perspective} - resultClass={props.defaultResultClass} - data={perspectiveState.instanceTableData} - properties={getVisibleRows(perspectiveState)} - screenSize={screenSize} - layoutConfig={layoutConfig} - />} + render={routeProps => <InstancePageTable {...instanceTableProps} />} /> ) break @@ -157,6 +176,9 @@ const ResultClassRoute = props => { updateFacetOption: props.updateFacetOption } } + if (pageType === 'instancePage') { + leafletProps.uri = perspectiveState.instanceTableData.id + } routeComponent = ( <Route path={path} @@ -233,7 +255,8 @@ const ResultClassRoute = props => { fill, createChartData, doNotRenderOnMount = false, - dropdownForResultClasses = false + dropdownForResultClasses = false, + dropdownForChartTypes = false } = resultClassConfig const apexProps = { portalConfig, @@ -264,6 +287,17 @@ const ResultClassRoute = props => { apexProps.resultClasses = resultClassConfig.resultClasses apexProps.dropdownForResultClasses = true } + if (dropdownForChartTypes && has(resultClassConfig, 'chartTypes')) { + const { chartTypes } = resultClassConfig + const newChartTypes = chartTypes.map(chartType => { + return { + id: chartType.id, + createChartData: props.apexChartsConfig[chartType.createChartData] + } + }) + apexProps.chartTypes = newChartTypes + apexProps.dropdownForChartTypes = true + } routeComponent = ( <Route path={path} @@ -318,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 9ab80038559d4ad4f359450193909223b8f46a1b..0dc9b2a0e4b87ac30e9d87141871527cb8a1f89a 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/InstancePageTable.js b/src/client/components/main_layout/InstancePageTable.js index 7c7525631098a1403c330d1624b3bc2ca0aebbca..dd7c73ee59d43ce5b31a7a6008160f84dac5c3bf 100644 --- a/src/client/components/main_layout/InstancePageTable.js +++ b/src/client/components/main_layout/InstancePageTable.js @@ -87,7 +87,8 @@ class InstancePageTable extends React.Component { componentDidMount = () => { if (this.props.fetchResultsWhenMounted) { this.props.fetchResults({ - resultClass: this.props.resultClassVariant, + perspectiveID: this.props.perspectiveConfig.id, + resultClass: this.props.resultClass, facetClass: this.props.facetClass, uri: this.props.uri }) @@ -123,15 +124,16 @@ class InstancePageTable extends React.Component { } render = () => { - const { classes, data, resultClass, properties, screenSize } = this.props + const { classes, data, properties, screenSize, perspectiveConfig } = this.props + const perspectiveID = perspectiveConfig.id return ( <> {data && <Table className={classes.instanceTable} size='small'> <TableBody> {properties.map(row => { - const label = intl.get(`perspectives.${resultClass}.properties.${row.id}.label`) - const description = intl.get(`perspectives.${resultClass}.properties.${row.id}.description`) + const label = intl.get(`perspectives.${perspectiveID}.properties.${row.id}.label`) + const description = intl.get(`perspectives.${perspectiveID}.properties.${row.id}.description`) const { id, valueType, makeLink, externalLink, sortValues, sortBy, numberedList, minWidth, linkAsButton, collapsedMaxWords, showSource, sourceExternalLink, renderAsHTML, HTMLParserTask diff --git a/src/client/components/main_layout/KnowledgeGraphMetadataTable.js b/src/client/components/main_layout/KnowledgeGraphMetadataTable.js index eb34d761446158b386f8a2f2039280c8b238dbe3..58822256e0b362db83119c1a6924a2cdb496c6e6 100644 --- a/src/client/components/main_layout/KnowledgeGraphMetadataTable.js +++ b/src/client/components/main_layout/KnowledgeGraphMetadataTable.js @@ -24,8 +24,11 @@ const KnowledgeGraphMetadataTable = props => { useEffect(() => { if (props.fetchKnowledgeGraphMetadata) { - const { resultClass } = props - props.fetchKnowledgeGraphMetadata({ resultClass }) + const { perspectiveID, resultClass } = props + props.fetchKnowledgeGraphMetadata({ + perspectiveID, + resultClass + }) } }, []) diff --git a/src/client/components/main_layout/MuiIcon.js b/src/client/components/main_layout/MuiIcon.js index ea108b95244d03c6e09a6e162648deb7b911afbc..53c3bb867b3c0ac62f98bcf18da7146269f8bfd9 100644 --- a/src/client/components/main_layout/MuiIcon.js +++ b/src/client/components/main_layout/MuiIcon.js @@ -10,7 +10,9 @@ import { CloudDownload, BubbleChart, ShowChart, - FormatAlignJustify + FormatAlignJustify, + ClearAll, + OndemandVideo } from '@material-ui/icons' import has from 'lodash' @@ -26,7 +28,9 @@ const MuiIcon = props => { CloudDownload: CloudDownload, BubbleChart: BubbleChart, ShowChart: ShowChart, - FormatAlignJustify: FormatAlignJustify + FormatAlignJustify: FormatAlignJustify, + ClearAll: ClearAll, + OndemandVideo: OndemandVideo } if (has(MuiIcons, props.iconName)) { const MuiIconComponent = MuiIcons[props.iconName] diff --git a/src/client/components/main_layout/PerspectiveTabs.js b/src/client/components/main_layout/PerspectiveTabs.js index be0357046560b8bdba9cfb3c3da095789aa3a38f..0773cd6409e7efe959ff721ca70c5880b269acee 100644 --- a/src/client/components/main_layout/PerspectiveTabs.js +++ b/src/client/components/main_layout/PerspectiveTabs.js @@ -1,6 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' import { withStyles } from '@material-ui/core/styles' +import Grow from '@material-ui/core/Grow' import Tabs from '@material-ui/core/Tabs' import Tab from '@material-ui/core/Tab' import { Link } from 'react-router-dom' @@ -38,6 +39,12 @@ class PerspectiveTabs extends React.Component { if (newPath !== oldPath) { this.setState({ value: this.pathnameToValue(newPath) }) } + + // Fix tabs indicator not showing on first load + // https://stackoverflow.com/a/61205108 + const evt = document.createEvent('UIEvents') + evt.initUIEvent('resize', true, false, window, 0) + window.dispatchEvent(evt) } pathnameToValue = pathname => { @@ -62,30 +69,34 @@ class PerspectiveTabs extends React.Component { const scrollButtons = tabs.length > 3 ? 'auto' : 'on' return ( <Paper className={classes.root}> - <Tabs - value={this.state.value} - onChange={this.handleChange} - indicatorColor='secondary' - textColor='secondary' - variant={variant} - scrollButtons={scrollButtons} - > - {tabs.map(tab => - <Tab - classes={{ - root: classes.tabRoot, - labelIcon: classes.tabLabelIcon, - wrapper: classes.tabWrapper - }} - key={tab.id} - icon={tab.icon} - label={intl.get(`tabs.${tab.id}`)} - component={Link} - to={tab.id} - wrapped - /> - )} - </Tabs> + <Grow in> + <Tabs + value={this.state.value} + onChange={this.handleChange} + indicatorColor='secondary' + textColor='secondary' + variant={variant} + scrollButtons={scrollButtons} + > + {tabs.map(tab => + <Tab + classes={{ + root: classes.tabRoot, + labelIcon: classes.tabLabelIcon, + wrapper: classes.tabWrapper + }} + key={tab.id} + icon={tab.icon} + label={intl.get(`tabs.${tab.id}`)} + component={Link} + to={tab.id} + wrapped + /> + )} + </Tabs> + + </Grow> + </Paper> ) } diff --git a/src/client/components/main_layout/TopBarInfoButton.js b/src/client/components/main_layout/TopBarInfoButton.js index c7f21692272d816ce433d73a8703e092a740a260..dd34179ccdef26664fdd58b0db76dda03965446d 100644 --- a/src/client/components/main_layout/TopBarInfoButton.js +++ b/src/client/components/main_layout/TopBarInfoButton.js @@ -33,8 +33,41 @@ class TopBarInfoButton extends React.Component { this.setState({ anchorEl: null }) }; - render () { + renderInfoItem = item => { const { classes } = this.props + let jsx + if (item.externalLink) { + jsx = ( + <a + className={classes.link} + key={item.id} + href={intl.get(`topBar.info.${item.translatedUrl}`)} + target='_blank' + rel='noopener noreferrer' + > + <MenuItem onClick={this.handleInfoMenuClose}> + {intl.get(`topBar.info.${item.translatedText}`)} + </MenuItem> + </a> + ) + } else { + jsx = ( + <MenuItem + key={item.id} + component={this.AdapterLink} + to={`${this.props.rootUrl}${item.internalLink}`} + onClick={this.handleInfoMenuClose} + > + {intl.get(`topBar.info.${item.translatedText}`)} + </MenuItem> + ) + } + return jsx + } + + render () { + const { classes, layoutConfig } = this.props + const { infoDropdown } = layoutConfig.topBar return ( <> <Button @@ -60,25 +93,7 @@ class TopBarInfoButton extends React.Component { open={Boolean(this.state.anchorEl)} onClose={this.handleInfoMenuClose} > - <MenuItem - key={0} - component={this.AdapterLink} - to={`${this.props.rootUrl}/about`} - onClick={this.handleInfoMenuClose} - > - {intl.get('topBar.info.aboutThePortal')} - </MenuItem> - <a - className={classes.link} - key={1} - href={intl.get('topBar.info.blogUrl')} - target='_blank' - rel='noopener noreferrer' - > - <MenuItem onClick={this.handleInfoMenuClose}> - {intl.get('topBar.info.blog')} - </MenuItem> - </a> + {infoDropdown.map(item => this.renderInfoItem(item))} </Menu> </> ) diff --git a/src/client/components/main_layout/VideoPage.js b/src/client/components/main_layout/VideoPage.js new file mode 100644 index 0000000000000000000000000000000000000000..1c357cb2985e54a2ffc38dd768cca0c19195b461 --- /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 0000000000000000000000000000000000000000..6e7d2dc7935970dced6ce29c2cb74fe06a083d74 --- /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 0000000000000000000000000000000000000000..284335c82a8146060b2861f440d3ca89b893c36c --- /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 388278969de0452ae278bed7cf7b0f42506f7d71..2f1095bae23125278f5b0242454b8ef75ff83935 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, @@ -86,13 +87,13 @@ const FullTextSearch = lazy(() => import('../components/main_layout/FullTextSear const FacetBar = lazy(() => import('../components/facet_bar/FacetBar')) const FacetResults = lazy(() => import('../components/facet_results/FacetResults')) const FederatedResults = lazy(() => import('../components/facet_results/FederatedResults')) +const KnowledgeGraphMetadataTable = lazy(() => import('../components/main_layout/KnowledgeGraphMetadataTable')) // ** General components end ** // ** Import portal specific components ** const Main = lazy(() => import(`../components/perspectives/${portalID}/Main`)) const MainClientFS = lazy(() => import(`../components/perspectives/${portalID}/MainClientFS`)) const Footer = lazy(() => import(`../components/perspectives/${portalID}/Footer`)) -const KnowledgeGraphMetadataTable = lazy(() => import(`../components/perspectives/${portalID}/KnowledgeGraphMetadataTable`)) // ** Portal specific components end ** const useStyles = makeStyles(theme => ({ @@ -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} @@ -631,6 +636,7 @@ const SemanticPortal = props => { <KnowledgeGraphMetadataTable portalConfig={portalConfig} layoutConfig={layoutConfig} + perspectiveID='perspective1' resultClass='perspective1KnowledgeGraphMetadata' fetchKnowledgeGraphMetadata={props.fetchKnowledgeGraphMetadata} knowledgeGraphMetadata={props.perspective1.knowledgeGraphMetadata} @@ -676,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 @@ -706,6 +713,7 @@ const mapDispatchToProps = ({ updatePerspectiveHeaderExpanded, loadLocales, animateMap, + updateVideoPlayerTime, clientFSToggleDataset, clientFSFetchResults, clientFSClearResults, diff --git a/src/client/epics/index.js b/src/client/epics/index.js index c260227e00d19dce2084c8d48a58d751f0b961d1..d45b897a7c90e643f2fdb22608d5e5001ecc13c0 100644 --- a/src/client/epics/index.js +++ b/src/client/epics/index.js @@ -531,7 +531,7 @@ const fetchKnowledgeGraphMetadataEpic = (action$, state$) => action$.pipe( ofType(FETCH_KNOWLEDGE_GRAPH_METADATA), withLatestFrom(state$), mergeMap(([action]) => { - const requestUrl = `${apiUrl}/void/${action.resultClass}` + const requestUrl = `${apiUrl}/void/${action.perspectiveID}/${action.resultClass}` return ajax({ url: requestUrl, method: 'GET' diff --git a/src/client/helpers/helpers.js b/src/client/helpers/helpers.js index 338f29b26745ade2030dde1ee57035cdf6395891..1e14944d6da2f68c05c479f8b83bce0a28e0183c 100644 --- a/src/client/helpers/helpers.js +++ b/src/client/helpers/helpers.js @@ -19,7 +19,9 @@ export const stateToUrl = ({ limit = null, optimize = null, fromID = null, - toID = null + toID = null, + period = null, + province = null }) => { const params = {} if (perspectiveID !== null) { params.perspectiveID = perspectiveID } @@ -36,6 +38,8 @@ export const stateToUrl = ({ if (optimize !== null) { params.optimize = optimize } if (fromID !== null) { params.fromID = fromID } if (toID !== null) { params.toID = toID } + if (period !== null) { params.period = period } + if (province !== null) { params.province = province } if (facets !== null) { const constraints = [] for (const [key, value] of Object.entries(facets)) { @@ -195,6 +199,10 @@ export const processPortalConfig = async portalConfig => { mapboxConfig.mapboxAccessToken = mapboxAccessToken } layoutConfig.mainPage.bannerBackround = bannerBackround.replace('<BANNER_IMAGE_URL', bannerImageURL) + if (layoutConfig.topBar.logoImage) { + const { default: image } = await import(/* webpackMode: "eager" */ `../img/${layoutConfig.topBar.logoImage}`) + layoutConfig.topBar.logoImage = image + } } export const createPerspectiveConfig = async ({ portalID, searchPerspectives }) => { diff --git a/src/client/reducers/general/videoPlayer.js b/src/client/reducers/general/videoPlayer.js new file mode 100644 index 0000000000000000000000000000000000000000..9233d9e7131e2643d59d862653ec19ad42b33ce1 --- /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/translations/sampo/localeEN.json b/src/client/translations/sampo/localeEN.json index f06473d21aee468f97618bccb77a5b4e1d9b6407..bc33f3cfac31ed3997f710297280b0460fbd3aac 100644 --- a/src/client/translations/sampo/localeEN.json +++ b/src/client/translations/sampo/localeEN.json @@ -23,7 +23,7 @@ "info": { "info": "Info", "blog": "Project blog", - "blogUrl": "https://seco.cs.aalto.fi", + "blogUrl": "https://seco.cs.aalto.fi/tools/sampo-ui/", "aboutThePortal": "About the Portal" }, "searchBarPlaceHolder": "Search all content", diff --git a/src/configs/portalConfig.json b/src/configs/portalConfig.json index b65a32f2cd412d39f652f3d1dded5f2b150dad17..30b6dc74227a9365457e68ec4def59531dcf6a43 100644 --- a/src/configs/portalConfig.json +++ b/src/configs/portalConfig.json @@ -44,6 +44,14 @@ "sitemapInstancePageQuery": "sitemapInstancePageQuery" }, "layoutConfig": { + "colorPalette": { + "primary": { + "main": "#212121" + }, + "secondary": { + "main": "#EB1806" + } + }, "hundredPercentHeightBreakPoint": "md", "reducedHeightBreakpoint": "xl", "tabHeight": 58, @@ -52,9 +60,22 @@ "topBar": { "showLanguageButton": true, "feedbackLink": "https://link.webropolsurveys.com/...", - "reducedHeight": 48, + "reducedHeight": 44, "defaultHeight": 64, - "mobileMenuBreakpoint": 1360 + "mobileMenuBreakpoint": 1360, + "infoDropdown": [ + { + "id": "about", + "translatedText": "aboutThePortal", + "internalLink": "/about" + }, + { + "id": "blog", + "externalLink": true, + "translatedUrl": "blogUrl", + "translatedText": "blog" + } + ] }, "mainPage": { "bannerImage": "main_page/mmm-banner.jpg", diff --git a/src/configs/sampo/search_perspectives/perspective1.json b/src/configs/sampo/search_perspectives/perspective1.json index 3b01432782450fee1c62b9cc0daad7d2fa9b5b2c..53152059e441fcc1e7234ea2633f7c523e29dc15 100644 --- a/src/configs/sampo/search_perspectives/perspective1.json +++ b/src/configs/sampo/search_perspectives/perspective1.json @@ -52,8 +52,12 @@ "component": "Export", "tabPath": "export", "tabIcon": "CloudDownload", - "resultClass": "perspective2", - "facetClass": "perspective2" + "resultClass": "perspective1", + "facetClass": "perspective1" + }, + "perspective1KnowledgeGraphMetadata": { + "sparqlQuery": "knowledgeGraphMetadataQuery", + "resultMapper": "makeObjectList" } }, "properties": [ diff --git a/src/configs/sampo/search_perspectives/perspective2.json b/src/configs/sampo/search_perspectives/perspective2.json index 5a94f67aa9881e2d993f8188b19c5a3e5e543c65..72c1ac89f9ff9d2f570ce8bf0bf9c73b4a25c1d0 100644 --- a/src/configs/sampo/search_perspectives/perspective2.json +++ b/src/configs/sampo/search_perspectives/perspective2.json @@ -68,7 +68,7 @@ "tabPath": "production_places", "tabIcon": "AddLocation", "sparqlQuery": "productionPlacesQuery", - "facetClass": "perspective1", + "facetClass": "perspective2", "filterTarget": "manuscripts", "resultMapper": "mapPlaces", "facetID": "productionPlace", @@ -85,7 +85,7 @@ "tabPath": "production_places_heatmap", "tabIcon": "AddLocation", "sparqlQuery": "productionPlacesQuery", - "facetClass": "perspective1", + "facetClass": "perspective2", "filterTarget": "manuscripts", "resultMapper": "mapPlaces", "layerType": "heatmapLayer" @@ -96,7 +96,7 @@ "tabPath": "last_known_locations", "tabIcon": "LocationOn", "sparqlQuery": "lastKnownLocationsQuery", - "facetClass": "perspective1", + "facetClass": "perspective2", "filterTarget": "manuscripts", "resultMapper": "mapPlaces", "facetID": "lastKnownLocation", @@ -111,7 +111,7 @@ "tabPath": "migrations", "tabIcon": "Redo", "sparqlQuery": "migrationsQuery", - "facetClass": "perspective1", + "facetClass": "perspective2", "filterTarget": "manuscript", "resultMapper": "makeObjectList", "layerType": "arcLayer", @@ -138,7 +138,7 @@ "tabPath": "production_dates", "tabIcon": "ShowChart", "sparqlQuery": "productionsByDecadeQuery", - "facetClass": "perspective1", + "facetClass": "perspective2", "filterTarget": "instance", "resultMapper": "mapLineChart", "resultMapperConfig": { @@ -157,7 +157,7 @@ }, "productionsByDecadeAndCountry": { "sparqlQuery": "productionsByDecadeAndCountryQuery", - "facetClass": "perspective1", + "facetClass": "perspective2", "filterTarget": "manuscript", "resultMapper": "makeObjectList", "postprocess": { @@ -173,7 +173,7 @@ "tabPath": "event_dates", "tabIcon": "AddLocation", "sparqlQuery": "eventsByDecadeQuery", - "facetClass": "perspective1", + "facetClass": "perspective2", "filterTarget": "manuscript", "resultMapper": "mapMultipleLineChart", "resultMapperConfig": { @@ -212,7 +212,7 @@ "tabPath": "network", "tabIcon": "BubbleChart", "sparqlQuery": "manuscriptFacetResultsNetworkLinksQuery", - "facetClass": "perspective1", + "facetClass": "perspective2", "sparqlQueryNodes": "manuscriptNetworkNodesQuery", "filterTarget": "manuscript", "useNetworkAPI": true, @@ -227,12 +227,8 @@ "component": "Export", "tabPath": "export", "tabIcon": "CloudDownload", - "resultClass": "perspective1", - "facetClass": "perspective1" - }, - "perspective1KnowledgeGraphMetadata": { - "sparqlQuery": "knowledgeGraphMetadataQuery", - "resultMapper": "makeObjectList" + "resultClass": "perspective2", + "facetClass": "perspective2" }, "jenaText": { "propertiesQueryBlock": "fullTextSearchProperties" diff --git a/src/server/index.js b/src/server/index.js index 1f0f980064e3fd1c5e5e6dc548290863ba5859a4..7d52b9bab025f7947a831517cd692f8faf52733f 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -366,12 +366,13 @@ app.get(`${apiPath}/wfs`, async (req, res, next) => { } }) -app.get(`${apiPath}/void/:resultClass`, async (req, res, next) => { +app.get(`${apiPath}/void/:perspectiveID/:resultClass`, async (req, res, next) => { const { params } = req try { const backendSearchConfig = await createBackendSearchConfig() const data = await getAllResults({ backendSearchConfig, + perspectiveID: params.perspectiveID, resultClass: params.resultClass, resultFormat: 'json' }) diff --git a/src/server/openapi.yaml b/src/server/openapi.yaml index 01f8cf31e5e06cf7d7f5eda8c36ebd1e0b430265..9579a7b8e8346e58a86ab296a0c667934dd2ece0 100644 --- a/src/server/openapi.yaml +++ b/src/server/openapi.yaml @@ -556,10 +556,16 @@ paths: application/json: schema: type: object - /void/{resultClass}: + /void/{perspectiveID}/{resultClass}: get: summary: Retrieve a VoID description parameters: + - in: path + name: perspectiveID + schema: + type: string + example: perspective1 + required: true - in: path name: resultClass schema: diff --git a/src/server/sparql/Filters.js b/src/server/sparql/Filters.js index b0adc34e536efe9c7d6ce1eefac728d649290529..56e2cf661c19a601530066468d646c3a42c82c7b 100644 --- a/src/server/sparql/Filters.js +++ b/src/server/sparql/Filters.js @@ -95,11 +95,17 @@ const generateTextFilter = ({ }) => { const facetConfig = backendSearchConfig[facetClass].facets[facetID] let filterStr = '' + let queryObject + if (facetConfig.textQueryProperty) { + queryObject = `(${facetConfig.textQueryProperty} '${queryString}')` + } else { + queryObject = `'${queryString}'` + } if (!has(facetConfig, 'textQueryPredicate')) { - filterStr = `?${filterTarget} text:query (${facetConfig.textQueryProperty} '${queryString}') .` + filterStr = `?${filterTarget} text:query ${queryObject} .` } else { filterStr = ` - ?textQueryTarget text:query (${facetConfig.textQueryProperty} '${queryString}') . + ?textQueryTarget text:query ${queryObject} . ?${filterTarget} ${facetConfig.textQueryPredicate} ?textQueryTarget . ` diff --git a/src/server/sparql/Mappers.js b/src/server/sparql/Mappers.js index a927f3ebd923c943a20fa3205378190ff19e5765..04dc9b6ea851f2843b2ddb166c2686443d735ad0 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/sampo/sparql_queries/SparqlQueriesPerspective1.js b/src/server/sparql/sampo/sparql_queries/SparqlQueriesPerspective1.js index ead7630663faee7b8ab1bde5159ae96c5cb495f3..19e11062a735c743c558769e84d51f28f8857236 100644 --- a/src/server/sparql/sampo/sparql_queries/SparqlQueriesPerspective1.js +++ b/src/server/sparql/sampo/sparql_queries/SparqlQueriesPerspective1.js @@ -1,4 +1,4 @@ -const perspectiveID = 'perspective2' +const perspectiveID = 'perspective1' export const workProperties = ` { @@ -59,3 +59,18 @@ export const workProperties = ` OPTIONAL { ?productionTimespan__id crm:P82b_end_of_the_end ?productionTimespan__end } } ` + +export const knowledgeGraphMetadataQuery = ` + SELECT * + WHERE { + ?id a sd:Dataset ; + dct:title ?title ; + dct:publisher ?publisher ; + dct:rightsHolder ?rightsHolder ; + dct:modified ?modified ; + dct:source ?databaseDump__id . + ?databaseDump__id skos:prefLabel ?databaseDump__prefLabel ; + mmm-schema:data_provider_url ?databaseDump__dataProviderUrl ; + dct:modified ?databaseDump__modified . + } +` diff --git a/src/server/sparql/sampo/sparql_queries/SparqlQueriesPerspective2.js b/src/server/sparql/sampo/sparql_queries/SparqlQueriesPerspective2.js index 97dd7838f30c2d01dca9f17eae1997239f94629c..56ed0c10cc9c188663509f315e54344682e662ba 100644 --- a/src/server/sparql/sampo/sparql_queries/SparqlQueriesPerspective2.js +++ b/src/server/sparql/sampo/sparql_queries/SparqlQueriesPerspective2.js @@ -1,4 +1,4 @@ -const perspectiveID = 'perspective1' +const perspectiveID = 'perspective2' export const manuscriptPropertiesInstancePage = ` { @@ -582,21 +582,6 @@ export const eventsByDecadeQuery = ` ORDER BY ?category ` -export const knowledgeGraphMetadataQuery = ` - SELECT * - WHERE { - ?id a sd:Dataset ; - dct:title ?title ; - dct:publisher ?publisher ; - dct:rightsHolder ?rightsHolder ; - dct:modified ?modified ; - dct:source ?databaseDump__id . - ?databaseDump__id skos:prefLabel ?databaseDump__prefLabel ; - mmm-schema:data_provider_url ?databaseDump__dataProviderUrl ; - dct:modified ?databaseDump__modified . - } -` - export const choroplethQuery = ` SELECT ?id ?prefLabel ?polygon (count(?death) as ?instanceCount) WHERE {