<head>
    <style> body {
        margin: 0;
    } </style>
    <script src="//unpkg.com/jquery"></script>
    <script src="//unpkg.com/3d-force-graph"></script>
    <script src="//unpkg.com/three"></script>
    <script src="//unpkg.com/three/examples/js/renderers/CSS2DRenderer.js"></script>
    <script src="//unpkg.com/three-spritetext"></script>

    <style>
        .node-div {
            font-size: 1rem;
            padding: 1px 4px;
            border-radius: 4px;
            background-color: rgba(0, 0, 0, 0.5);
            user-select: none;
        }

        .node-data-div {
            background-color: rgba(0, 0, 0, 0.9);
        }
    </style>
</head>

<body>
<div id="3d-graph"></div>

<script id="vars">
    const typeNodeColors = {
        'ble_server': '#0066ff',

        'ssid': 'transparent',
        'station': '#ffcc33',
        'access_point': '#ff9900',

        'endpoint': '#33cc33',
        'gateway': '#006600'
    };

    const typeColors = {
        'ble_server': '#0066ff',

        'ssid': '#ffff99',
        'station': '#ffcc33',
        'access_point': '#ff9900',

        'endpoint': '#33cc33',
        'gateway': '#006600'
    };

    var targetNode = null;
</script>

<script id="functions">
    function renderNodeHTML(node) {
        const nodeDiv = document.createElement('div');
        switch (node.type) {
            case 'ssid':
                nodeDiv.innerHTML = `<small>${node.entity}</small>`
                break;

            case 'access_point':
                var ap = node.entity;
                nodeDiv.innerHTML = `
                        <center>
                            <b>${ap.hostname}</b> (${ap.encryption})
                            <br/>
                            ${ap.mac}
                            ${ap.vendor? '<br/>(' + ap.vendor + ')' : ''}
                            ${ap.wps.length? '<br/>' + JSON.stringify(ap.wps) : ''}
                        </center>`;
                break;

            case 'station':
                var sta = node.entity;
                nodeDiv.innerHTML = `
                        <center>
                            ${sta.mac}
                            ${sta.vendor? '<br/>(' + sta.vendor + ')' : ''}
                        </center>`;
                break;

            case 'ble_server':
                var dev = node.entity;
                nodeDiv.innerHTML = `
                        <center>
                            ${dev.mac}
                            ${dev.vendor? '<br/>(' + dev.vendor + ')' : ''}
                        </center>`;
                break;

            case 'endpoint':
                var ip = node.entity;
                nodeDiv.innerHTML = `
                        <center>
                            ${ip.hostname? '<b>' + ip.hostname + '</b><br/>' : ''}
                            ${ip.ipv4? ip.ipv4 + '<br/>' : ''}
                            ${ip.ipv6? ip.ipv6 + '<br/>' : ''}
                            <br/>
                            ${ip.mac}
                            ${ip.vendor? '<br/>(' + ip.vendor + ')' : ''}
                            ${ip.meta.values.length? '<br/>' + JSON.stringify(ip.meta.values) : ''}
                        </center>`;
                break;

            case 'gateway':
                var ip = node.entity;
                nodeDiv.innerHTML = `
                        <center>
                            ${ip.hostname? '<b>' + ip.hostname + '</b><br/>' : ''}
                            ${ip.ipv4? ip.ipv4 + '<br/>' : ''}
                            ${ip.ipv6? ip.ipv6 + '<br/>' : ''}
                            <br/>
                            ${ip.mac}
                            ${ip.vendor? '<br/>(' + ip.vendor + ')' : ''}
                            ${ip.meta.values.length? '<br/>' + JSON.stringify(ip.meta.values) : ''}
                        </center>`;
                break;

            default:
                nodeDiv.innerHTML = `<b>${node.id}</b>`
        }

        nodeDiv.style.color = typeColors[node.type];
        nodeDiv.className = 'node-div';

        const dataDiv = document.createElement('div');

        dataDiv.id = 'datadiv_' + node.id;
        dataDiv.className = 'node-data-div';
        dataDiv.style.visibility = 'hidden';
        dataDiv.style.display = 'none';

        dataDiv.innerHTML = '<br/><pre>' + node.type + ' ' + JSON.stringify(node.entity, null, 2) + '</pre>';

        nodeDiv.appendChild(dataDiv);

        return new THREE.CSS2DObject(nodeDiv);
    }
</script>

<script id="graph">

    const Graph = ForceGraph3D({
        extraRenderers: [new THREE.CSS2DRenderer()],
        controlType: 'orbit'
    })
    (document.getElementById('3d-graph'))
        .jsonUrl('bettergraph.json')
        .nodeLabel('id')
        .nodeColor(node => typeNodeColors[node.type])
        .linkDirectionalArrowLength(3.5)
        .linkDirectionalArrowRelPos(1)
        /*
        .linkThreeObjectExtend(true)
        .linkThreeObject(link => {
            const sprite = new SpriteText(link.edge.type);
            sprite.color = 'lightgrey';
            sprite.textHeight = 1.5;
            return sprite;
        })
        .linkPositionUpdate((sprite, {start, end}) => {
            const middlePos = Object.assign(...['x', 'y', 'z'].map(c => ({
                [c]: start[c] + (end[c] - start[c]) / 2 // calc middle point
            })));
            Object.assign(sprite.position, middlePos);
        })
         */
        .nodeThreeObject(renderNodeHTML)
        .nodeThreeObjectExtend(true)
        .onNodeClick(node => {
            if( targetNode != null ) {
                const prev = document.getElementById('datadiv_' + targetNode.id);
                prev.style.visibility = 'hidden';
                prev.style.display = 'none';
            }
            targetNode = node;

            const curr = document.getElementById('datadiv_' + targetNode.id);
            curr.style.visibility = 'visible';
            curr.style.display = 'block';

            // Aim at node from outside it
            const distance = 40;
            const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z);

            Graph.cameraPosition(
                { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }, // new position
                node, // lookAt ({ x, y, z })
                3000  // ms transition duration
            );
        });
</script>
</body>