Commit 07effaf8 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Merge branch 'encyclopedia-gui-integration' into 'v1.0.0'

Structure.js component

See merge request !152
parents 2ebffd29 ea38601f
Pipeline #80859 passed with stages
in 22 minutes and 32 seconds
......@@ -4,6 +4,7 @@
"commit": "e98694e",
"private": true,
"dependencies": {
"@lauri-codes/materia": "0.0.5",
"@material-ui/core": "^4.0.0",
"@material-ui/icons": "^4.0.0",
"@material-ui/lab": "^4.0.0-alpha.49",
......@@ -23,6 +24,7 @@
"lodash": "^4.17.15",
"material-ui-chip-input": "^1.0.0-beta.14",
"material-ui-flat-pagination": "^4.0.0",
"mathjs": "^7.1.0",
"object-hash": "^2.0.3",
"pace": "^0.0.4",
"pace-js": "^1.0.2",
......@@ -51,7 +53,6 @@
"remark": "^12.0.1",
"remark-math": "^2.0.1",
"swagger-client": "^3.8.22",
"three.js": "^0.77.1",
"url-parse": "^1.4.3",
"use-query-params": "^0.6.0"
},
......
......@@ -9,7 +9,10 @@ import Browser, { Item, Content, Compartment, List, Adaptor } from './Browser'
import { resolveRef, rootSections } from './metainfo'
import { Title, metainfoAdaptorFactory, DefinitionLabel } from './MetainfoBrowser'
import { Matrix, Number } from './visualizations'
import Structure from '../visualization/Structure'
import { StructureViewer } from '@lauri-codes/materia'
import Markdown from '../Markdown'
import { convert } from '../../utils'
export const configState = atom({
key: 'config',
......@@ -20,12 +23,21 @@ export const configState = atom({
}
})
// Shared instance of the StructureViewer
const viewer = new StructureViewer()
// Contains details about the currently visualized system. Used to detect if a
// reload is needed for the StructureViewer.
const visualizedSystem = {}
export default function ArchiveBrowser({data}) {
const searchOptions = useMemo(() => archiveSearchOptions(data), [data])
return <Browser
adaptor={archiveAdaptorFactory(data)}
form={<ArchiveConfigForm searchOptions={searchOptions} />}
/>
return (
<Browser
adaptor={archiveAdaptorFactory(data)}
form={<ArchiveConfigForm searchOptions={searchOptions} />}
/>
)
}
ArchiveBrowser.propTypes = ({
data: PropTypes.object.isRequired
......@@ -288,11 +300,59 @@ QuantityValue.propTypes = ({
def: PropTypes.object.isRequired
})
/**
* An optional overview for a section displayed directly underneath the section
* title.
*/
function Overview({section, def}) {
// Structure visualization for section_system
if (def.name === 'section_system') {
let url = window.location.href
let name = 'section_system'
let rootIndex = url.indexOf(name) + name.length
let sectionPath = url.substring(0, rootIndex)
let tmp = url.substring(rootIndex)
let tmpIndex = tmp.indexOf('/')
let index = tmpIndex === -1 ? tmp : tmp.slice(0, tmpIndex)
let system
let positionsOnly = false
// Loading exact same system, no need to reload visualizer
if (sectionPath === visualizedSystem.sectionPath && index === visualizedSystem.index) {
// Loading same system with different positions
} else if (sectionPath === visualizedSystem.sectionPath) {
positionsOnly = true
system = {
positions: convert(section.atom_positions, 'm', 'angstrom')
}
// Completely new system
} else {
system = {
'species': section.atom_species,
'cell': convert(section.lattice_vectors, 'm', 'angstrom'),
'positions': convert(section.atom_positions, 'm', 'angstrom'),
'pbc': section.configuration_periodic_dimensions
}
}
visualizedSystem.sectionPath = sectionPath
visualizedSystem.index = index
return <Structure viewer={viewer} system={system} positionsOnly={positionsOnly}></Structure>
}
return null
}
Overview.propTypes = ({
def: PropTypes.object,
section: PropTypes.object
})
function Section({section, def}) {
const config = useRecoilValue(configState)
const filter = config.showCodeSpecific ? def => true : def => !def.name.startsWith('x_')
return <Content>
<Title def={def} data={section} kindLabel="section" />
<Overview def={def} section={section}></Overview>
<Compartment title="sub sections">
{def.sub_sections
.filter(subSectionDef => section[subSectionDef.name] || config.showAllDefined)
......
......@@ -100,7 +100,7 @@ class ArchiveEntryView extends React.Component {
if (doesNotExist) {
return (
<Typography className={classes.error}>
No archive does exist for this entry. Either the archive was not generated due
No archive exists for this entry. Either the archive was not generated due
to parsing or other processing errors (check the log tab), or the entry it
self does not exist.
</Typography>
......
......@@ -4,6 +4,7 @@ import { withStyles, Divider, Card, CardContent, Grid, CardHeader, Typography, L
import { withApi } from '../api'
import { compose } from 'recompose'
import ApiDialogButton from '../ApiDialogButton'
// import Structure from '../visualization/Structure'
import Quantity from '../Quantity'
import { Link as RouterLink } from 'react-router-dom'
import { DOI } from '../search/DatasetList'
......@@ -23,6 +24,10 @@ class RepoEntryView extends React.Component {
},
entryCards: {
marginTop: theme.spacing(2)
},
structureViewer: {
height: '25rem',
padding: '0'
}
})
......
import React, { useState, useEffect, useRef, useCallback } from 'react'
import PropTypes from 'prop-types'
import { makeStyles } from '@material-ui/core/styles'
import {
Box,
Checkbox,
Menu,
MenuItem,
Button,
IconButton,
Tooltip,
Typography,
Dialog,
DialogContent,
DialogActions,
FormControlLabel
} from '@material-ui/core'
import {
MoreVert,
Fullscreen,
FullscreenExit,
CameraAlt,
Replay
} from '@material-ui/icons'
import { StructureViewer } from '@lauri-codes/materia'
export default function Structure(props) {
// States
const [anchorEl, setAnchorEl] = React.useState(null)
const [fullscreen, setFullscreen] = useState(false)
const [showBonds, setShowBonds] = useState(true)
const [showLatticeConstants, setShowLatticeConstants] = useState(true)
const [showCell, setShowCell] = useState(true)
const [error, setError] = useState(null)
// Variables
const open = Boolean(anchorEl)
const viewer = useRef(null)
const refCanvas = useRef(null)
// Styles
const useStyles = makeStyles((theme) => {
return {
root: {
width: '100%',
height: 0,
paddingBottom: `${(1 / props.aspectRatio) * 100}%`, // CSS hack for fixed aspect ratio
position: 'relative'
},
rootInner: {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0
},
container: {
display: 'flex',
width: '100%',
height: '100%',
flexDirection: 'column',
backgroundColor: 'white'
},
header: {
display: 'flex',
flexDirection: 'row',
zIndex: 1
},
spacer: {
flex: 1
},
viewerCanvas: {
flex: 1,
zIndex: 0,
minHeight: 0, // added min-height: 0 to allow the item to shrink to fit inside the container.
marginBottom: theme.spacing(2),
display: error === null ? 'block' : 'none'
},
errorContainer: {
flex: 1,
zIndex: 0,
minHeight: 0, // added min-height: 0 to allow the item to shrink to fit inside the container.
marginBottom: theme.spacing(2),
alignItems: 'center',
justifyContent: 'center',
display: error === null ? 'none' : 'flex'
},
errorMessage: {
flex: '0 0 70%',
color: '#aaa',
textAlign: 'center'
},
iconButton: {
backgroundColor: 'white'
}
}
})
const classes = useStyles(props)
// In order to properly detect changes in a reference, a reference callback is
// used. This is the recommended way to monitor reference changes as a simple
// useRef is not guaranteed to update:
// https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node
const measuredRef = useCallback(node => {
refCanvas.current = node
if (node === null) {
return
}
if (viewer.current === null) {
return
}
viewer.current.changeHostElement(node, true, true)
}, [])
// Run only on first render to initialize the viewer. See the viewer
// documentation for details on the meaning of different options:
// https://lauri-codes.github.io/materia/viewers/structureviewer
useEffect(() => {
let options
if (props.options === undefined) {
options = {
view: {
autoResize: true,
autoFit: true,
fitMargin: 0.5
},
bonds: {
enabled: true
},
latticeConstants: {
size: 0.7,
font: 'Titillium Web,sans-serif',
a: {color: '#f44336'},
b: {color: '#4caf50'},
c: {color: '#5c6bc0'}
},
controls: {
enableZoom: true,
enablePan: true,
enableRotate: true
},
renderer: {
backgroundColor: ['#ffffff', 1],
shadows: {
enabled: false
}
}
}
} else {
options = props.options
}
if (props.viewer === undefined) {
viewer.current = new StructureViewer(undefined, options)
} else {
viewer.current = props.viewer
viewer.current.setOptions(options, false, false)
}
if (refCanvas.current !== null) {
viewer.current.changeHostElement(refCanvas.current, false, false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Called only on first render to load the given structure.
useEffect(() => {
if (props.system === undefined) {
return
}
if (props.positionsOnly) {
viewer.current.setPositions(props.system.positions)
return
}
let nAtoms = props.system.species.length
if (nAtoms >= props.sizeLimit) {
setError('Visualization is disabled due to large system size.')
return
}
// Systems with cell are centered on the cell center and orientation is defined
// by the cell vectors.
let cell = props.system.cell
if (cell !== undefined) {
viewer.current.setOptions({layout: {
viewCenter: 'COC',
viewRotation: {
align: {
top: 'c',
right: 'b'
},
rotations: [
[0, 1, 0, 60],
[1, 0, 0, 30]
]
}
}})
// Systems without cell are centered on the center of positions
} else {
viewer.current.setOptions({layout: {
viewCenter: 'COP',
viewRotation: {
rotations: [
[0, 1, 0, 60],
[1, 0, 0, 30]
]
}
}})
}
viewer.current.load(props.system)
viewer.current.fitToCanvas()
viewer.current.saveReset()
viewer.current.reset()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Viewer settings
useEffect(() => {
viewer.current.setOptions({bonds: {enabled: showBonds}})
}, [showBonds])
useEffect(() => {
viewer.current.setOptions({latticeConstants: {enabled: showLatticeConstants}})
}, [showLatticeConstants])
useEffect(() => {
viewer.current.setOptions({cell: {enabled: showCell}})
}, [showCell])
// Memoized callbacks
const openMenu = useCallback((event) => {
setAnchorEl(event.currentTarget)
}, [])
const closeMenu = useCallback(() => {
setAnchorEl(null)
}, [])
const toggleFullscreen = useCallback(() => {
setFullscreen(!fullscreen)
}, [fullscreen])
const takeScreencapture = useCallback(() => {
viewer.current.takeScreenShot(props.captureName)
}, [props.captureName])
const handleReset = useCallback(() => {
viewer.current.reset()
viewer.current.fitToCanvas()
viewer.current.render()
}, [])
const content = <Box className={classes.container}>
<div className={classes.header}>
{fullscreen && <Typography variant="h6">Structure</Typography>}
<div className={classes.spacer}></div>
<Tooltip title="Reset view">
<IconButton className={classes.iconButton} onClick={handleReset} disabled={error}>
<Replay />
</IconButton>
</Tooltip>
<Tooltip
title="Toggle fullscreen">
<IconButton className={classes.iconButton} onClick={toggleFullscreen} disabled={error}>
{fullscreen ? <FullscreenExit /> : <Fullscreen />}
</IconButton>
</Tooltip>
<Tooltip title="Capture image">
<IconButton className={classes.iconButton} onClick={takeScreencapture} disabled={error}>
<CameraAlt />
</IconButton>
</Tooltip>
<Tooltip title="Options">
<IconButton className={classes.iconButton} onClick={openMenu} disabled={error}>
<MoreVert />
</IconButton>
</Tooltip>
<Menu
id='settings-menu'
anchorEl={anchorEl}
getContentAnchorEl={null}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
keepMounted
open={open}
onClose={closeMenu}
>
<MenuItem key='show-bonds'>
<FormControlLabel
control={
<Checkbox
checked={showBonds}
onChange={(event) => { setShowBonds(!showBonds) }}
color='primary'
/>
}
label='Show bonds'
/>
</MenuItem>
<MenuItem key='show-axis'>
<FormControlLabel
control={
<Checkbox
checked={showLatticeConstants}
onChange={(event) => { setShowLatticeConstants(!showLatticeConstants) }}
color='primary'
/>
}
label='Show lattice constants'
/>
</MenuItem>
<MenuItem key='show-cell'>
<FormControlLabel
control={
<Checkbox
checked={showCell}
onChange={(event) => { setShowCell(!showCell) }}
color='primary'
/>
}
label='Show simulation cell'
/>
</MenuItem>
</Menu>
</div>
<div className={classes.viewerCanvas} ref={measuredRef}></div>
<div className={classes.errorContainer}><div className={classes.errorMessage}>{error}</div></div>
</Box>
return (
<Box className={classes.root}>
<Box className={classes.rootInner}>
{fullscreen ? '' : content}
</Box>
<Dialog maxWidth="lg" fullWidth open={fullscreen}>
<DialogContent>
<Box className={classes.root}>
<Box className={classes.rootInner}>
{fullscreen ? content : ''}
</Box>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setFullscreen(false)}>
Close
</Button>
</DialogActions>
</Dialog>
</Box>
)
}
Structure.propTypes = {
viewer: PropTypes.object, // Optional shared viewer instance.
system: PropTypes.object, // The system to display as section_system
options: PropTypes.object, // Viewer options
captureName: PropTypes.string, // Name of the file that the user can download
aspectRatio: PropTypes.number, // Fixed aspect ratio for the viewer canvas
sizeLimit: PropTypes.number, // Maximum number of atoms to attempt to display
positionsOnly: PropTypes.bool // Whether to update only positions. This is much faster than loading the entire structure.
}
Structure.defaultProps = {
aspectRatio: 4 / 3,
captureName: 'structure',
sizeLimit: 300
}
import { unit } from 'mathjs'
export const isEquivalent = (a, b) => {
// Create arrays of property names
var aProps = Object.getOwnPropertyNames(a)
......@@ -31,6 +33,48 @@ export const capitalize = (s) => {
return s.charAt(0).toUpperCase() + s.slice(1)
}
/**
* Used to convert numeric values from one unit to another. Works on
* n-dimensional arrays and implemented as a relatively simple for loop for
* performance. If conversion times become an issue, it might be worthwhile to
* look at vectorization with SIMD.
*
* @param {*} value The values to convert
* @param {*} from Original unit.
* @param {*} to Target unit.
*
* @return {*} A copy of the original data with units converted.
*/
export function convert(value, from, to) {
// Determine the scaling factor
let factor = unit(1, from).toNumber(to)
// Convert arrays
function scaleRecursive(list, newList) {
let isScalarArray = !Array.isArray(list[0])
if (isScalarArray) {
for (let i = 0, size = list.length; i < size; ++i) {
newList.push(list[i] * factor)
}
} else {
for (let i = 0, size = list.length; i < size; ++i) {
let iList = []
newList.push(iList)
scaleRecursive(list[i], iList)
}
}
}
let isArray = Array.isArray(value)
let newValue
if (!isArray) {
newValue = value * factor
} else {
newValue = []
scaleRecursive(value, newValue)
}
return newValue
}
export function arraysEqual(_arr1, _arr2) {
if (!Array.isArray(_arr1) || !Array.isArray(_arr2) || _arr1.length !== _arr2.length) {
return false
......
......@@ -1287,6 +1287,13 @@
resolved "https://registry.yarnpkg.com/@kyleshockey/object-assign-deep/-/object-assign-deep-0.4.2.tgz#84900f0eefc372798f4751b5262830b8208922ec"
integrity sha1-hJAPDu/DcnmPR1G1JigwuCCJIuw=
 
"@lauri-codes/materia@0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@lauri-codes/materia/-/materia-0.0.5.tgz#7ea8d0e8f74f723c7ec64630854b3c121aaccf20"
integrity sha512-6kLzBU4hgaMuJGl5FkU8GCa84SeIZzY0YgVRplc0Uic2+amh6MiAi2V4orq4PeHcMAInLC+VsK62Azp8cDsT/Q==
dependencies:
three "^0.119.1"
"@material-ui/codemod@^4.5.0":
version "4.5.0"
resolved "https://registry.yarnpkg.com/@material-ui/codemod/-/codemod-4.5.0.tgz#e258a4865a7d68506579e046a6170fd742ffdf4f"
......@@ -3367,6 +3374,11 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
 
complex.js@^2.0.11:
version "2.0.11"
resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.11.tgz#09a873fbf15ffd8c18c9c2201ccef425c32b8bf1"
integrity sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw==
component-emitter@^1.2.1: