vr.js

/**
 *
 * PRSM VR map view
 * generates a 'three dimensional' view of a PRSM map for VR, build on threejs and A-FRAME
 *
 *
 * */

import * as Y from 'yjs'
import {WebsocketProvider} from 'y-websocket'
import {object_equals, standardize_color, timeAndDate} from './utils.js'
import Quill from 'quill'
import {QuillDeltaToHtmlConverter} from 'quill-delta-to-html'

var room
const doc = new Y.Doc()
var websocket = 'wss://www.prsm.uk/wss' // web socket server URL
var clientID // unique ID for this browser
var yNodesMap // shared map of nodes
var yEdgesMap // shared map of edges

window.addEventListener('load', () => {
	startY()
})
/**
 * create a new shared document and connect to the WebSocket provider
 */
function startY() {
	// get the room number from the URL, or if none, complain
	let url = new URL(document.location)
	room = url.searchParams.get('room')
	if (room == null || room == '') alert('No room name')
	else room = room.toUpperCase()
	document.title = document.title + ' ' + room
	// don't bother with a local storage provider
	const wsProvider = new WebsocketProvider(websocket, 'prsm' + room, doc)
	wsProvider.on('sync', () => {
		console.log(exactTime() + ' remote content loaded')
	})
	wsProvider.on('status', (event) => {
		console.log(exactTime() + event.status + (event.status == 'connected' ? ' to' : ' from') + ' room ' + room) // logs when websocket is "connected" or "disconnected"
	})

	yNodesMap = doc.getMap('nodes')
	yEdgesMap = doc.getMap('edges')

	clientID = doc.clientID
	console.log('My client ID: ' + clientID)

	/* 
	for convenience when debugging
	 */
	window.clientID = clientID
	window.yNodesMap = yNodesMap
	window.yEdgesMap = yEdgesMap

	// we only read changes made by other clients; this client never writes anything to the shared document
	yNodesMap.observe(() => {
		showForceGraph()
	})
	yEdgesMap.observe(() => {
		showForceGraph()
	})
} // end startY()

function exactTime() {
	let d = new Date()
	return `${d.toLocaleTimeString()}:${d.getMilliseconds()} `
}
/* set up a div to aid conversion of notes from Quill to HTML */
let dummyDiv = document.createElement('div')
dummyDiv.id = 'dummy-div'
dummyDiv.style.display = 'none'
document.querySelector('body').appendChild(dummyDiv)
let qed = new Quill('#dummy-div')
/**
 * Convert a node from the normal PRSM format to the one required by the 3d display
 * @param {Object} node
 * @returns Object
 */
function convertNode(node) {
	let note = ''
	if (node.created || node.modified || node.note) {
		note = '<div style="padding: 12px; border-radius: 4px; border: 2px grey solid; background-color: white">'
		if (node.created) note += `<p>Created at ${timeAndDate(node.created.time, true)} by ${node.created.user}</p>`
		if (node.modified)
			note += `<p>Modified at ${timeAndDate(node.modified.time, true)} by ${node.modified.user}</p>`
		if (node.note) {
			qed.setContents(node.note)
			// convert Quill formatted note to HTML, escaping all "
			note += new QuillDeltaToHtmlConverter(qed.getContents().ops, {
				inlineStyles: true,
			})
				.convert()
				.replaceAll('"', '""')
		}
		note += '</div>'
	}
	return {
		id: node.id,
		label: node.label,
		color: node.color.background,
		fontColor: node.font.color,
		val: 5,
		note: note,
	}
}
/**
 * Convert an edge from the normal PRSM format to the one required by the 3d display
 * @param {object} edge
 * @returns object
 */
function convertEdge(edge) {
	return {
		source: edge.from,
		target: edge.to,
		// black links are invisible on a black background, so change them to grey,
		// so that they are visible on either white or black backgrounds
		color: standardize_color(edge.color.color) === '#000000' ? 'lightgrey' : edge.color.color,
	}
}
/**
 * Convert the map data from PRSM format to 3d display format
 * Avoid cluster nodes
 * Add lists of links and neighbours to each node
 * @return {object} {nodes; links}
 */
function convertData() {
	let graphNodes = Array.from(yNodesMap.values())
		.filter((n) => !n.isCluster)
		.map((n) => {
			return convertNode(n)
		})
	let graphEdges = Array.from(yEdgesMap.values()).map((e) => {
		return convertEdge(e)
	})
	return {nodes: graphNodes, links: graphEdges}
}
var previousData

function showForceGraph() {
	let graphData = convertData()
	if (object_equals(previousData, graphData)) return
	previousData = graphData
	const fgEl = document.getElementById('fg')
	fgEl.replaceChildren()
	fgEl.setAttribute('forcegraph', {
		nodes: JSON.stringify(graphData.nodes),
		links: JSON.stringify(graphData.links),
		nodeResolution: 32,
		nodeRelSize: 6,
		nodeOpacity: 1.0,
		linkWidth: 0,
		linkOpacity: 1.0,
		linkDirectionalArrowLength: 1.5,
		linkDirectionalArrowRelPos: 1,
		onEngineStop: console.log('Engine stopped'),
		onEngineTick: console.log('tick'),
	})
	// draw a sphere entity around each node
	fgEl.setAttribute('spherize', {})
}