import React, { Component } from 'react'
import { Route, Link } from 'react-router-dom'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { history } from '../../../store'
import { session } from '../../../session'
import actions from '../../../actions'
import * as graphActions from '../graph.actions'
import { Loader } from '../../../common'
import { clamp, dist, mod, angle } from '../../../util'
const defaultState = {
dragging: false,
bounds: null,
mouseX: 0,
mouseY: 0,
box: {
x: 0, y: 0,
w: 0, h: 0,
},
page: null,
measurements: {},
}
class GraphEditor extends Component {
state = {
...defaultState,
}
constructor() {
super()
// bind these events in the constructor, so we can remove event listeners later
this.handleMouseDown = this.handleMouseDown.bind(this)
this.handleMouseMove = this.handleMouseMove.bind(this)
this.handleMouseUp = this.handleMouseUp.bind(this)
this.handleWindowResize = this.handleWindowResize.bind(this)
this.graphRef = React.createRef()
this.measurements = {}
}
getBoundingClientRect() {
if (!this.graphRef.current) return null
const rect = this.graphRef.current.getBoundingClientRect()
const scrollTop = document.body.scrollTop || document.body.parentNode.scrollTop
const scrollLeft = document.body.scrollLeft || document.body.parentNode.scrollLeft
const bounds = {
top: rect.top + scrollTop,
left: rect.left + scrollLeft,
width: rect.width,
height: rect.height,
}
// console.log(bounds)
return bounds
}
componentDidMount() {
document.body.addEventListener('mousemove', this.handleMouseMove)
document.body.addEventListener('mouseup', this.handleMouseUp)
window.addEventListener('resize', this.handleWindowResize)
this.setState({ bounds: this.getBoundingClientRect() })
}
componentDidUpdate(prevProps) {
if (!this.state.bounds) {
this.setState({ bounds: this.getBoundingClientRect() })
}
}
addMeasurement({ id, width, height }) {
this.measurements[id] = { width, height }
this.setState({
measurements: { ...this.measurements },
})
}
handleWindowResize() {
this.setState({ bounds: this.getBoundingClientRect() })
}
handleMouseDown(e, page) {
const bounds = this.getBoundingClientRect()
const mouseX = e.pageX
const mouseY = e.pageY
let w = 128 / bounds.width
let h = 16 / bounds.height
let { x, y } = page.settings
x = clamp(x, 0, 1)
y = clamp(y, 0, 1)
this.setState({
page,
dragging: true,
bounds,
mouseX,
mouseY,
box: {
x, y,
w, h,
},
initialBox: {
x, y,
w, h,
}
})
}
handleMouseMove(e) {
const {
dragging,
bounds, mouseX, mouseY, initialBox, box
} = this.state
if (dragging) {
e.preventDefault()
let { x, y, w, h } = initialBox
let dx = (e.pageX - mouseX) / bounds.width
let dy = (e.pageY - mouseY) / bounds.height
this.setState({
box: {
x: clamp(x + dx, 0, 1.0 - w),
y: clamp(y + dy, 0, 1.0 - h),
w, h,
}
})
}
}
handleMouseUp(e) {
// const { actions } = this.props
const { dragging, bounds, initialBox, box, page } = this.state
if (!dragging) return
e.preventDefault()
const { width, height } = bounds
const { x, y, w, h } = box
let url = window.location.pathname
this.setState({
page: null,
box: null,
initialBox: null,
dragging: false,
})
if (dist(width * x, height * y, width * initialBox.x, height * initialBox.y) < 3) return
const updatedPage = {
...page,
settings: {
...page.settings,
x, y,
}
}
this.props.graphActions.updateGraphPage(updatedPage)
actions.page.update(updatedPage)
}
render(){
// console.log(this.props.graph.show.res)
const { page: currentPage, box, measurements } = this.state
const { res: graph } = this.props.graph.show
// console.log(res.pages)
return (
{this.state.bounds && graph.pages.map(page => (
this.handleMouseDown(e, page)}
onMeasure={measurement => this.addMeasurement(measurement)}
/>
))}
)
}
}
const DEFAULT_MEASUREMENT = { width: 16, height: 16 }
class GraphCanvas extends Component {
constructor(props) {
super(props)
this.canvasRef = React.createRef()
}
componentDidMount() {
if (this.props.bounds) {
this.draw({})
}
}
componentDidUpdate(prevProps) {
this.draw(prevProps)
}
draw(prevProps) {
const { current: canvas } = this.canvasRef
const { bounds, pages, currentPage, box, measurements } = this.props
const { width, height } = bounds
if (prevProps.bounds !== bounds) {
canvas.width = width
canvas.height = height
}
console.log(measurements)
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, width, height)
ctx.lineWidth = 2
const coordsLookup = pages.reduce((a,b) => {
if (currentPage && box && b.id === currentPage.id) {
a[b.id] = {
x: box.x,
y: box.y,
backlinks: new Set([]),
}
} else {
a[b.id] = {
x: b.settings.x,
y: b.settings.y,
backlinks: new Set([]),
}
}
return a
}, {})
pages.map(page => {
const sourceCoord = coordsLookup[page.id]
page.backlinks.map(tile => {
if (tile.target_page_id <= 0) return
const targetCoord = coordsLookup[tile.page_id]
let tile_measurement = measurements[tile.page_id] || DEFAULT_MEASUREMENT
let target_measurement = measurements[tile.target_page_id] || DEFAULT_MEASUREMENT
let x1_offset = tile_measurement.width / 2
let y1_offset = tile_measurement.height / 2
let x2_offset = target_measurement.width / 2
let y2_offset = target_measurement.height / 2
let theta = angle(targetCoord.x, targetCoord.y, sourceCoord.x, sourceCoord.y)
// skip duplicate links
if (sourceCoord.backlinks.has(tile.page_id)) {
return
}
// if this is the first time encountering this link...
if (!targetCoord.backlinks.has(tile.target_page_id)) {
sourceCoord.backlinks.add(tile.page_id)
ctx.strokeStyle = "#ff88ff"
} else { // otherwise this is a two-way link
x1_offset += 10 * Math.cos(theta + Math.PI /2)
y1_offset += 10 * Math.sin(theta + Math.PI /2)
x2_offset += 10 * Math.cos(theta + Math.PI /2)
y2_offset += 10 * Math.sin(theta + Math.PI /2)
ctx.strokeStyle = "#88ffff"
}
ctx.beginPath()
const x1 = targetCoord.x * width + x1_offset
const y1 = targetCoord.y * height + y1_offset
const x2 = sourceCoord.x * width + x2_offset
const y2 = sourceCoord.y * height + y2_offset
this.arrow(ctx, x1, y1, x2, y2)
ctx.stroke()
})
})
}
arrow(ctx, x1, y1, x2, y2) {
const headlen = 10 // length of head in pixels
const farOffset = 20
const endOffset = 1
const theta = angle(x1, y1, x2, y2)
x1 += Math.cos(theta) * 0
x2 -= Math.cos(theta) * farOffset
y1 += Math.sin(theta) * 0
y2 -= Math.sin(theta) * farOffset
const xEnd = x2 - Math.cos(theta) * endOffset
const yEnd = y2 - Math.sin(theta) * endOffset
const leftAngle = mod(theta - Math.PI / 6, Math.PI * 2)
const rightAngle = mod(theta + Math.PI / 6, Math.PI * 2)
ctx.moveTo(x1, y1)
ctx.lineTo(xEnd, yEnd)
ctx.moveTo(x2, y2)
ctx.lineTo(x2 - headlen * Math.cos(leftAngle), y2 - headlen * Math.sin(leftAngle))
ctx.moveTo(x2, y2)
ctx.lineTo(x2 - headlen * Math.cos(rightAngle), y2 - headlen * Math.sin(rightAngle))
}
render() {
return (
)
}
}
class PageHandle extends Component {
constructor(props){
super(props)
this.ref = React.createRef()
}
componentDidMount(){
this.measure()
}
componentDidUpdate(prevProps){
if (this.props.page.title !== prevProps.page.title) {
this.measure()
}
}
measure() {
const { offsetWidth: width, offsetHeight: height } = this.ref.current
const { id } = this.props.page
console.log(id, width, height)
this.props.onMeasure({ id, width, height })
}
render() {
const { graph, page, bounds, box, onMouseDown } = this.props
let style;
if (box) {
style = {
top: (bounds.height) * box.y,
left: (bounds.width) * box.x,
}
} else {
style = {
top: (bounds.height) * Math.min(page.settings.y, 0.95),
left: (bounds.width) * Math.min(page.settings.x, 0.95),
}
}
const url = '/' + graph.path + '/' + page.path
// console.log(style)
return (
history.push(url)}
style={style}
>
{page.title}
{'>'}
)
}
}
const mapStateToProps = state => ({
graph: state.graph,
})
const mapDispatchToProps = dispatch => ({
graphActions: bindActionCreators({ ...graphActions }, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(GraphEditor)