拖拽生成树+JSON序列化

7 篇文章 0 订阅

今年三月份的时候,我写过一篇博客 树的创建、遍历及可视化。这里我主要是通过代码生成树,接着把树对象序列化成 JSON,然后利用其它技术将把树可视化。当时觉得,这样做非常直观,能看到树到底是什么样,并且序列化后的 JSON 也可以持久化保存,想要恢复原来的树对象,也很简单。当时就自然的萌生出另一个想法:我现在是实现了 JSON -> 可视化树,如果反过来实现呢?即通过 可视化树 -> JSON。 当时考虑了一下,我是敲定了利用前端技术 Canvas 来做,但是因为我不是前端开发,只会一些简单的 js。后来,感觉挺麻烦的,就放弃了。不过这个想法,我一直记得,偶尔还是能想起。上个星期,正好有时间,我就又开始思考这个问题了,并且初步有了一些眉目,我感觉我的想法是可行的,不过需要解决一些技术上的难题。然后星期天我就开始正式写代码了,一直到今天,我终于完成了一个简单的 demo。

菩提本无树

演示

在这里插入图片描述

生成的 json 利用 Python 库的可视化(见上一篇博客中提到的几种可视化方法之一)
在这里插入图片描述

代码

复制下面代码,浏览器打开(Chrome、新版 Edge),打开控制台,拖拽形成树,然后控制台调用 Serializer() 方法。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

    <style>
        #canvas {
            border: 2px solid red;
        }
    </style>
</head>

<body>
    <div id="content">
        <canvas id="canvas" width="800" height="800">

        </canvas>
    </div>

    <script>
        // 定义一些需要的全局变量
        let canvas = document.getElementById("canvas");
        let ctx = canvas.getContext("2d");
        let WIDTH = canvas.width, HEIGHT = canvas.height;
        ctx.font = "25px Consolas";   // 设置字体

        let DISTANCE_CENTER = 10 * 10,        // 圆心区域大小平方
            DISTANCE_ROUND = 25 * 25,         // 原内区域大小平方
            ROOT = 0, CHILD = 1,              // 定义常量,区分根和孩子节点
            GLOBAL_IS_DOWN = false,           // 鼠标是否按下
            GLOBAL_IS_CENTER = false,         // 鼠标是否在节点中心
            GLOBAL_IS_ROUND = false,          // 鼠标是否在圆内(但不包括中心)
            GLOBAL_CURRENT_INDEX = -1,        // 鼠标选中的节点下标,默认是 -1
            GLOBAL_POINT = { x: -1, y: -1 },  // 记录上一个有效的点
            NODES = new Array(),              // 保存节点的数组,图的顶点
            EDGES = new Map();                // 保存箭头的数组,图的边


        // 鼠标按下
        canvas.onmousedown = e => {
            GLOBAL_IS_DOWN = true;
            GLOBAL_POINT = { x: -1, y: -1 };   // 每次鼠标按下,重新初始化

            let x = e.pageX - canvas.offsetLeft;
            let y = e.pageY - canvas.offsetTop;

            for (let i = 0; i < NODES.length; i++) {
                let node = NODES[i];
                dd = (node.x - x) * (node.x - x) + (node.y - y) * (node.y - y);

                if (dd <= DISTANCE_CENTER) {
                    GLOBAL_IS_CENTER = true;
                    GLOBAL_CURRENT_INDEX = i;
                    break;
                } else if (dd <= DISTANCE_ROUND) {
                    GLOBAL_IS_ROUND = true;
                    GLOBAL_CURRENT_INDEX = i;
                    break;
                }
            }
        }

        // 鼠标移动
        canvas.onmousemove = e => {
            if (!GLOBAL_IS_DOWN) {
                return
            }

            let x = e.pageX - canvas.offsetLeft;
            let y = e.pageY - canvas.offsetTop;

            if (GLOBAL_IS_CENTER) {
                // 绘制图形
                drawGraph();
                // 绘制线条
                drawTempArrow(x, y);
            }

            if (GLOBAL_IS_ROUND) {
                // 修改指定的节点
                NODES[GLOBAL_CURRENT_INDEX].x = x;
                NODES[GLOBAL_CURRENT_INDEX].y = y;
                // 重新绘制
                drawGraph();
            }
        }

        // 鼠标松开
        canvas.onmouseup = e => {
            // 判断点是否落在节点上
            let x = e.pageX - canvas.offsetLeft;
            let y = e.pageY - canvas.offsetTop;
            let lastIndex = -1;
            for (let i = 0; i < NODES.length; i++) {
                let node = NODES[i];
                dd = (node.x - x) * (node.x - x) + (node.y - y) * (node.y - y);

                if (dd <= DISTANCE_ROUND) {
                    GLOBAL_IS_ROUND = true;
                    lastIndex = GLOBAL_CURRENT_INDEX;
                    GLOBAL_CURRENT_INDEX = i;
                    break;
                }
            }

            if (GLOBAL_IS_ROUND && lastIndex != -1) {
                // 如果不是同一个点就绘制
                if (lastIndex != GLOBAL_CURRENT_INDEX) {
                    // 需要考虑边重复创建的问题和反向绘制的问题
                    createEdge(lastIndex, GLOBAL_CURRENT_INDEX);
                }
            }

            drawGraph();

            // 全局变量状态恢复
            GLOBAL_IS_DOWN = false;
            GLOBAL_IS_CENTER = false;
            GLOBAL_IS_ROUND = false;
            GLOBAL_CURRENT_INDEX = -1
        }

        class Node {
            constructor(name, x, y, radius, color, nodeType) {
                this.name = name;
                this.x = x;
                this.y = y;
                this.radius = radius;
                this.color = color;
                this.nodeType = nodeType;
                this.kind = "NODE";
            }
        }

        // 绘制所有的节点
        function drawNodes() {
            // 简单设置一些样式
            ctx.lineWidth = 2;
            // 绘制节点
            NODES.forEach(e => {
                ctx.fillText(e.name, e.x, e.y);
                ctx.strokeStyle = e.color;
                ctx.beginPath();
                ctx.arc(e.x, e.y, e.radius, 0, 2 * Math.PI);
                ctx.stroke();
            })

            // 绘制边
            EDGES.forEach((es, i) => {
                let head = NODES[i];
                es.forEach(e => {
                    let tail = NODES[e];
                    ctx.beginPath();
                    ctx.strokeStyle = "red";
                    ctx.moveTo(head.x, head.y);
                    // // 两个节点间的距离
                    let d = Math.sqrt((head.x - tail.x) * (head.x - tail.x) + (head.y - tail.y) * (head.y - tail.y));
                    let sinX = Math.abs(head.y - tail.y) / d;
                    let cosX = Math.abs(head.x - tail.x) / d;
                    // 所以,箭头与指向的节点的交点可由此计算得出
                    let x1 = tail.x + 25 * cosX;
                    let y1 = tail.y - 25 * sinX;
                    // 考虑在左右两种情况
                    if (x1 < Math.min(head.x, tail.x) || x1 > Math.max(head.x, tail.x)) {
                        x1 = tail.x - 25 * cosX;
                    }

                    if (y1 < Math.min(head.y, tail.y) || y1 > Math.max(head.y, tail.y)) {
                        y1 = tail.y + 25 * sinX;
                    }
                    ctx.lineTo(x1, y1);
                    ctx.stroke();
                    ctx.beginPath();
                    ctx.strokeStyle = "green";
                    ctx.moveTo(x1, y1);
                    ctx.lineTo(tail.x, tail.y);
                    ctx.stroke();
                })
            })
        }

        // 绘制临时箭头
        function drawTempArrow(x, y) {
            ctx.beginPath();
            ctx.moveTo(NODES[GLOBAL_CURRENT_INDEX].x, NODES[GLOBAL_CURRENT_INDEX].y);
            ctx.lineTo(x, y);
            ctx.stroke();
        }

        function createEdge(head, tail) {
            // 默认第一个节点是根节点,降低处理难度
            // 如果是从某节点指向根节点,直接报错
            if (tail == 0) {
                console.log("Are you kidding me?");
                return;
            }

            // 判断重复的指向问题,如果一个节点已经被指向
            // 例如:如果 A --> B,则其他节点无法再指向 B
            for (let values of EDGES.values()) {
                for (let i = 0; i < values.length; i++) {
                    if (values[i] == tail) {
                        console.log("Repeat narrow!")
                        return
                    }
                }
            }

            let edge = EDGES.get(head);
            if (edge) {
                edge.push(tail)
            } else {
                edge = new Array();
                edge.push(tail);
                EDGES.set(head, edge);
            }
        }


        function drawGraph() {
            // 清空canvas
            ctx.clearRect(0, 0, WIDTH, HEIGHT);
            // 重新绘制
            drawNodes();
        }


        function verify() {
            // TODO 验证当前图是否是一棵树
            // 因为已经做了处理,所以是无法形成环,所以
            // 我应该只需要要判断 n 个节点有 n-1 条边就可以了(这里不一定对)
            let count = 0;
            for (let values of EDGES.values()) {
                count += values.length;
            }

            if (count == NODES.length - 1) {
                return true;
            } else {
                return false;
            }
        }


        function Serializer() {
            // 生成树 JSON 表示
            // {
            //     "name": "",
            //     "children": [
            //         "name": "",
            //         "children": []
            //     ]
            // }
            if (!verify()) {
                console.log("This is not a tree.")
                return
            }
            tree = DFS(0);
            console.log(JSON.stringify(tree, null, 4));
        }

        function DFS(v) {
            // 采用递归的方式,深度遍历图的同时,生成图的json对象表示
            let child = {
                "name": NODES[v].name,  // 似乎数字类型用python的库无法正常显示
                "children": []
            }
            let vexs = EDGES.get(v)
            if (vexs) {
                for (let i = 0; i < vexs.length; i++) {
                    let sub_child = DFS(vexs[i])
                    if (sub_child) {
                        child["children"].push(sub_child)
                    }
                }
            }

            return child
        }

        function test() {
            let root = new Node('A', 200, 50, 25, 'green', ROOT);
            let child1 = new Node('B', 350, 50, 25, 'red', CHILD);
            let child2 = new Node('C', 500, 50, 25, 'red', CHILD);
            let child3 = new Node('D', 650, 50, 25, 'red', CHILD);
            let child4 = new Node('E', 200, 350, 25, 'red', CHILD);
            let child5 = new Node('F', 350, 350, 25, 'red', CHILD);
            let child6 = new Node('G', 500, 350, 25, 'red', CHILD);
            let child7 = new Node('H', 650, 350, 25, 'red', CHILD);
            let child8 = new Node('Z', 650, 550, 25, 'red', CHILD);


            NODES.push(root);
            NODES.push(child1);
            NODES.push(child2);
            NODES.push(child3);
            NODES.push(child4);
            NODES.push(child5);
            NODES.push(child6);
            NODES.push(child7);
            NODES.push(child8);

            // 绘制
            drawNodes(NODES);
        }

        test();

    </script>
</body>

</html>

思考

我刚开始做的时候,还不知道所有节点组成的是一个图(没有想到这里)。不过后来发现,这不就是数据结构里面的图吗?然后问题就转化为如何判定一个有向图是一棵树了。然后,我请教了人工智能,哈哈。这个答案的质量真是不错!
在这里插入图片描述

绘制对象

这里简单介绍一下,几个关键的元素。

节点:一个给定半径的圆。
箭头:从一个节点的圆心,指向另一个节点。(被指向的节点,箭头和和圆交点到圆心的绿色线条。)

节点,有两个点击区域:

  1. 圆心。
  2. 圆内不包括圆心。

绘制的逻辑是:

  1. 如果点击了圆心并且拖动,会绘制一根线条,相当于一个带指向的箭头,如果鼠标松开在另一个节点(当然不包括自身了)中,那么会绘制一个上述的箭头。
  2. 如果点击了圆内并且拖动,会拖动节点进行移动。
  3. 箭头是从一个节点指向另一个节点。如果一个节点已经被指向了,那么其他节点就无法再次指向它。 所以一个节点只能被一个节点指向,并且它自身可以指向其他多个节点。

绘制对象保存

所有的节点保存在一个全局的 NODES 数组中,所有的箭头保存在 EDGES Map 中。
EDGES 的一个简单示例:

{
	0: [1, 2],
	1: [3, 4],
	4: [5]
}

EDGES 就是图的邻接表结构了,不过,里面存放的是 NODES 节点的索引,不是 NODE 对象,这样处理起来方便一些。

序列化

关于判定图是树的问题,因为前面加了限制,反而简单了。我只需要判断这个条件即可:边数 == 节点数-1。所以,剩下的难题就是树的序列化了。这里是一个简洁的递归写法,我刚开始是使用的迭代写法。但是在迭代的时候生成树的结构真的难受,需要添加一些额外的处理。不然,孩子节点和双亲结点位置对不上了。后来,我去看了这种递归的写法,真是奇妙,你完全不用管其它的了。遍历的顺序就是每个对象的顺序,直接就生成了,很神奇。让我又想起来来那句著名的话:To iterate is human, to recurse, divine.

这里提供一下验证的 Python 程序
我是用 Python 先来验证想法可行,再移植到 JS 的,毕竟直接写的话,太为难我了。

from typing import Dict, List
import json


# A 是根节点
Graph = {
    "A": ["B", "C", "D"],
    "B": ["E", "F"],
    "F": ["G", "H"],
    "H": ["Q", "P"],
    "Q": ["U", "V"],
    "V": ["Y", "Z"]
}


def BFS(graph: Dict[str, List[str]]) -> str:
    """
    Graph DFS
    """
    vexs = graph["A"]

    queue = []
    children = []

    tree = {
        "name": "A",
        "children": []
    }

    queue.append(tree["name"])
    children.append(tree)
    while queue:
        vex = queue.pop(0)
        child = children.pop(0)
        vexs = graph.get(vex)
        if vexs:
            queue.extend(vexs)
            for vex in vexs:
                sub_child = {
                    "name": vex,
                    "children": []
                }
                child["children"].append(sub_child)
                children.append(sub_child)

    return json.dumps(tree, indent=4)


def DFS(v: str) -> Dict[str, List[str]]:
    child = {
        "name": v,
        "children": []
    }
    vexs = Graph.get(v)
    if vexs:
        for vex in vexs:
            sub_child = DFS(vex)
            if child:
                child["children"].append(sub_child)

    return child


if __name__ == "__main__":
    """
    单纯的遍历操作,递归和非递归的处理难度差不多。
    但是对于生成嵌套结构的方式,递归是最佳的方式。无需处理嵌套,它是自然而然的。
    人理解迭代,神理解递归。
    """
    tree_json = BFS(Graph)
    print(tree_json)

    tree_dic = DFS("A")
    print(json.dumps(tree_dic, indent=4))

总结

本来我还打算进一步完善这个代码的,增加删除节点、箭头的功能,不过因为前端技术实在是难为我这个后端开发了。所以我可能就放弃继续改进的想法了。也许,有时间会继续去完善它。这里就先把它给记录下来,也希望感兴趣的同学可以看一下。我觉得把这个功能做成一个静态网页也蛮好的。不过这里只是一个 demo,代码可能还有一堆问题呢,我反正已经是验证了那个想法了 —— 可视化树 -> JSON。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值