zoukankan      html  css  js  c++  java
  • d3.js 地铁轨道交通项目实战

    上一章说了如何制作一个线路图,当然上一章是手写的JSON数据,当然手写的json数据有非常多的好处,例如可以应对客户的各种BT需求,但是大多数情况下我们都是使用地铁公司现成的JSON文件,话不多说我们先看一下百度官方线路图

    就是这样的,今天我们就来完成它的大部分需求,以及地铁公司爸爸提出来的需求。

    需求如下:
    1.按照不同颜色显示地铁各线路,显示对应站点。
    2.用户可以点击手势缩放和平移(此项目为安卓开发)。
    3.用户在线路menu里点击线路,对应线路平移值屏幕中心并高亮。
    4.根据后台数据,渲染问题路段。
    5.点击问题路段站点,显示问题详情。

    大致需求就是这些,下面看看看代码

    1.定义一些常量和变量

    const dataset = subwayData; //线路图数据源
    let subway = new Subway(dataset); //线路图的类文件
    let baseScale = 2; //基础缩放倍率
    let deviceScale = 1400 / 2640; //设备与画布宽度比率
    let width = 2640; //画布宽
    let height = 1760; //画布高
    let transX = 1320 + 260; //地图X轴平移(将画布原点X轴平移)
    let transY = 580; //地图X轴平移(将画布原点Y轴平移)
    let scaleExtent = [0.8, 4]; //缩放倍率限制
    let currentScale = 2; //当前缩放值
    let currentX = 0; //当前画布X轴平移量
    let currentY = 0; //当前画布Y轴平移量
    let selected = false; //线路是否被选中(在右上角的线路菜单被选中)
    let scaleStep = 0.5; //点击缩放按钮缩放步长默认0.5倍
    let tooltip = d3.select('#tooltip'); //提示框
    let bugArray = []; //问题路段数组
    let svg = d3.select('#sw').append('svg'); //画布
    let group = svg.append('g').attr('transform', `translate(${transX}, ${transY}) scale(1)`);//定义组并平移
    let whole = group.append('g').attr('class', 'whole-line') //虚拟线路(用于点击右上角响应线路可以定位当视野中心,方法不唯一)
    let path = group.append('g').attr('class', 'path'); //定义线路
    let point = group.append('g').attr('class', 'point'); //定义站点
    const zoom = d3.zoom().scaleExtent(scaleExtent).on("zoom", zoomed); //定义缩放事件

    这就是我们需要使用的一些常量和变量。注意transX不是宽度的一半,是因为北京地铁线路网西线更密集。

    2.读官方JSON

    使用d3.js数据必不可少,然而官方的数据并不通俗易懂,我们先解读一下官方JSON数据。

    每条线路对象都有一个l_xmlattr属性和一个p属性,l_xmlattr是整条线路的属性,p是站点数组,我们看一下站点中我们需要的属性。ex是否是中转站,lb是站名,sid是站的id,rx、ry是文字偏移量,st是是否为站点(因为有的点不是站点而是为了渲染贝塞尔曲线用的),x、y是站点坐标。

    3.构造自己的类方法

    官方给了我们数据,但是并不是我们能直接使用的,所以我们需要构造自己的方法类

    class Subway {
        constructor(data) {
            this.data = data;
            this.bugLineArray = [];
        }
        getInvent() {} //获取虚拟线路数据
        getPathArray() {} //获取路径数据
        getPointArray() {} //获取站点数组
        getCurrentPathArray() {} //获取被选中线路的路径数组
        getCurrentPointArray() {} //获取被选中线路的站点数组
        getLineNameArray() {} // 获取线路名称数组
        getBugLineArray() {} //获取问题路段数组
    }
    下面是我们方法内容,里面的操作不是很优雅(大家将就看啦)
    getInvent() {
        let lineArray = [];
        this.data.forEach(d => {
            let { loop, lc, lbx, lby, lb, lid} = d.l_xmlattr;
            let allPoints = d.p.slice(0);
            loop && allPoints.push(allPoints[0]);
            let path = this.formatPath(allPoints, 0, allPoints.length - 1);
            lineArray.push({
                lid: lid,
                path: path,
            })
        })
        return lineArray;
    }
    getPathArray() {
        let pathArray = [];
        this.data.forEach(d => {
            let { loop, lc, lbx, lby, lb, lid} = d.l_xmlattr;
            let allPoints = d.p.slice(0);
            loop && allPoints.push(allPoints[0])
            let allStations = [];
            allPoints.forEach((item, index) => item.p_xmlattr.st && allStations.push({...item.p_xmlattr, index}))
            let arr = [];
            for(let i = 0; i < allStations.length - 1; i++) {
                let path = this.formatPath(allPoints, allStations[i].index, allStations[i + 1].index);
                arr.push({
                    lid: lid,
                    id: `${allStations[i].sid}_${allStations[i + 1].sid}`,
                    path: path,
                    color: lc.replace(/0x/, '#')
                })
            }
            pathArray.push({
                path: arr,
                lc: lc.replace(/0x/, '#'),
                lb,lbx,lby,lid
            })
        })
        return pathArray;
    }
    getPointArray() {
        let pointArray = [];
        let tempPointsArray = [];
        this.data.forEach(d => {
            let {lid,lc,lb} = d.l_xmlattr;
            let allPoints = d.p;
            let allStations = [];
            allPoints.forEach(item => {
                if(item.p_xmlattr.st && !item.p_xmlattr.ex) {
                    allStations.push({...item.p_xmlattr, lid, pn: lb, lc: lc.replace(/0x/, '#')})
                } else if (item.p_xmlattr.ex) {
                    if(tempPointsArray.indexOf(item.p_xmlattr.sid) == -1) {
                        allStations.push({...item.p_xmlattr, lid, pn: lb, lc: lc.replace(/0x/, '#')})
                        tempPointsArray.push(item.p_xmlattr.sid);
                    }
                }
            });
            pointArray.push(allStations);
        })
        return pointArray;
    }
    getCurrentPathArray(name) {
        let d = this.data.filter(d => d.l_xmlattr.lid == name)[0];
        let { loop, lc, lbx, lby, lb, lid} = d.l_xmlattr;
        let allPoints = d.p.slice(0);
        loop && allPoints.push(allPoints[0])
        let allStations = [];
        allPoints.forEach((item, index) => item.p_xmlattr.st && allStations.push({...item.p_xmlattr, index}))
        let arr = [];
        for(let i = 0; i < allStations.length - 1; i++) {
            let path = this.formatPath(allPoints, allStations[i].index, allStations[i + 1].index);
            arr.push({
                lid: lid,
                id: `${allStations[i].sid}_${allStations[i + 1].sid}`,
                path: path,
                color: lc.replace(/0x/, '#')
            })
        }
        return {
            path: arr,
            lc: lc.replace(/0x/, '#'),
            lb,lbx,lby,lid
        }
    }
    getCurrentPointArray(name) {
        let d = this.data.filter(d => d.l_xmlattr.lid == name)[0];
        let {lid,lc,lb} = d.l_xmlattr;
        let allPoints = d.p;
        let allStations = [];
        allPoints.forEach(item => {
            if(item.p_xmlattr.st && !item.p_xmlattr.ex) {
                allStations.push({...item.p_xmlattr, lid, pn: lb, lc: lc.replace(/0x/, '#')})
            } else if (item.p_xmlattr.ex) {
                allStations.push({...item.p_xmlattr, lid, pn: lb, lc: lc.replace(/0x/, '#')})
            }
        });
        return allStations;
    }
    getLineNameArray() {
        let nameArray = this.data.map(d => {
            return {
                lb: d.l_xmlattr.lb,
                lid: d.l_xmlattr.lid,
                lc: d.l_xmlattr.lc.replace(/0x/, '#')
            }
        })
        return nameArray;
    }
    getBugLineArray(arr) {
        if(!arr || !arr.length) return [];
        this.bugLineArray = [];
        arr.forEach(item => {
            let { start, end, cause, duration, lid, lb } = item;
            let lines = [];
            let points = [];
            let tempObj = this.data.filter(d => d.l_xmlattr.lid == lid)[0];
            let loop = tempObj.l_xmlattr.loop;
            let lc = tempObj.l_xmlattr.lc;
            let allPoints = tempObj.p;
            let allStations = [];
            allPoints.forEach(item => {
                if(item.p_xmlattr.st) {
                    allStations.push(item.p_xmlattr.sid)
                }
            });
            loop && allStations.push(allStations[0]);
            for(let i=allStations.indexOf(start); i<=allStations.lastIndexOf(end); i++) {
                points.push(allStations[i])
            }
            for(let i=allStations.indexOf(start); i<allStations.lastIndexOf(end); i++) {
                lines.push(`${allStations[i]}_${allStations[i+1]}`)
            }
            this.bugLineArray.push({cause,duration,lid,lb,lines,points,lc: lc.replace(/0x/, '#'),start: points[0],end:points[points.length - 1]});
        })
        return this.bugLineArray;
    
    这种方法大家也不必看懂,知道传入了什么,输入了什么即可,这就是我们的方法类。

    4.d3渲染画布并添加方法

    这里是js的核心代码,既然class文件都写完了,这里的操作就方便了很多,主要就是下面几个人方法,
    renderInventLine(); //渲染虚拟新路
    renderAllStation(); //渲染所有的线路名称(右上角)
    renderBugLine(); //渲染问题路段
    renderAllLine(); //渲染所有线路
    renderAllPoint(); //渲染所有点
    renderCurrentLine() //渲染当前选中的线路
    renderCurrentPoint() //渲染当前选中的站点
    zoomed() //缩放时执行的方法
    getCenter() //获取虚拟线中心点的坐标
    scale() //点击缩放按钮时执行的方法
    下面是对应的方法体
    svg.call(zoom);
    svg.call(zoom.transform, d3.zoomIdentity.translate((1 - baseScale) * transX, (1 - baseScale) * transY).scale(baseScale));
    
    let pathArray = subway.getPathArray();
    let pointArray = subway.getPointArray();
    
    renderInventLine();
    renderAllStation();
    renderBugLine();
    
    function renderInventLine() {
        let arr = subway.getInvent();
        whole.selectAll('path')
        .data(arr)
        .enter()
        .append('path')
        .attr('d', d => d.path)
        .attr('class', d => d.lid)
        .attr('stroke', 'none')
        .attr('fill', 'none')
    }
    
    function renderAllLine() {
        for (let i = 0; i < pathArray.length; i++) {
            path.append('g')
            .selectAll('path')
            .data(pathArray[i].path)
            .enter()
            .append('path')
            .attr('d', d => d.path)
            .attr('lid', d => d.lid)
            .attr('id', d => d.id)
            .attr('class', 'lines origin')
            .attr('stroke', d => d.color)
            .attr('stroke-width', 7)
            .attr('stroke-linecap', 'round')
            .attr('fill', 'none')
            path.append('text')
            .attr('x', pathArray[i].lbx)
            .attr('y', pathArray[i].lby)
            .attr('dy', '1em')
            .attr('dx', '-0.3em')
            .attr('fill', pathArray[i].lc)
            .attr('lid', pathArray[i].lid)
            .attr('class', 'line-text origin')
            .attr('font-size', 14)
            .attr('font-weight', 'bold')
            .text(pathArray[i].lb)
        }
    }
    
    function renderAllPoint() {
        for (let i = 0; i < pointArray.length; i++) {
            for (let j = 0; j < pointArray[i].length; j++) {
                let item = pointArray[i][j];
                let box = point.append('g');
                if (item.ex) {
                    box.append('image')
                    .attr('href', './trans.png')
                    .attr('class', 'points origin')
                    .attr('id', item.sid)
                    .attr('x', item.x - 8)
                    .attr('y', item.y - 8)
                    .attr('width', 16)
                    .attr('height', 16)
                } else {
                    box.append('circle')
                    .attr('cx', item.x)
                    .attr('cy', item.y)
                    .attr('r', 5)
                    .attr('class', 'points origin')
                    .attr('id', item.sid)
                    .attr('stroke', item.lc)
                    .attr('stroke-width', 1.5)
                    .attr('fill', '#ffffff')
                }
                box.append('text')
                .attr('x', item.x + item.rx)
                .attr('y', item.y + item.ry)
                .attr('dx', '0.3em')
                .attr('dy', '1.1em')
                .attr('font-size', 11)
                .attr('class', 'point-text origin')
                .attr('lid', item.lid)
                .attr('id', item.sid)
                .text(item.lb)
            }
        }
    }
    
    function renderCurrentLine(name) {
        let arr = subway.getCurrentPathArray(name);
        path.append('g')
        .attr('class', 'temp')
        .selectAll('path')
        .data(arr.path)
        .enter()
        .append('path')
        .attr('d', d => d.path)
        .attr('lid', d => d.lid)
        .attr('id', d => d.id)
        .attr('stroke', d => d.color)
        .attr('stroke-width', 7)
        .attr('stroke-linecap', 'round')
        .attr('fill', 'none')
        path.append('text')
        .attr('class', 'temp')
        .attr('x', arr.lbx)
        .attr('y', arr.lby)
        .attr('dy', '1em')
        .attr('dx', '-0.3em')
        .attr('fill', arr.lc)
        .attr('lid', arr.lid)
        .attr('font-size', 14)
        .attr('font-weight', 'bold')
        .text(arr.lb)
    }
    
    function renderCurrentPoint(name) {
        let arr = subway.getCurrentPointArray(name);
        for (let i = 0; i < arr.length; i++) {
            let item = arr[i];
            let box = point.append('g').attr('class', 'temp');
            if (item.ex) {
                box.append('image')
                .attr('href', './trans.png')
                .attr('x', item.x - 8)
                .attr('y', item.y - 8)
                .attr('width', 16)
                .attr('height', 16)
                .attr('id', item.sid)
            } else {
                box.append('circle')
                .attr('cx', item.x)
                .attr('cy', item.y)
                .attr('r', 5)
                .attr('id', item.sid)
                .attr('stroke', item.lc)
                .attr('stroke-width', 1.5)
                .attr('fill', '#ffffff')
            }
            box.append('text')
            .attr('class', 'temp')
            .attr('x', item.x + item.rx)
            .attr('y', item.y + item.ry)
            .attr('dx', '0.3em')
            .attr('dy', '1.1em')
            .attr('font-size', 11)
            .attr('lid', item.lid)
            .attr('id', item.sid)
            .text(item.lb)
        }
    }
    
    function renderBugLine(modal) {
        let bugLineArray = subway.getBugLineArray(modal);
        d3.selectAll('.origin').remove();
        renderAllLine();
        renderAllPoint();
        bugLineArray.forEach(d => {
            console.log(d)
            d.lines.forEach(dd => {
                d3.selectAll(`path#${dd}`).attr('stroke', '#eee');
            })
            d.points.forEach(dd => {
                d3.selectAll(`circle#${dd}`).attr('stroke', '#ddd')
                d3.selectAll(`text#${dd}`).attr('fill', '#aaa')
            })
        })
        d3.selectAll('.points').on('click', function () {
            let id = d3.select(this).attr('id');
            let bool = judgeBugPoint(bugLineArray, id);
            if (bool) {
                let x, y;
                if (d3.select(this).attr('href')) {
                    x = parseFloat(d3.select(this).attr('x')) + 8;
                    y = parseFloat(d3.select(this).attr('y')) + 8;
                } else {
                    x = d3.select(this).attr('cx');
                    y = d3.select(this).attr('cy');
                }
                let toolX = (x * currentScale + transX - ((1 - currentScale) * transX - currentX)) * deviceScale;
                let toolY = (y * currentScale + transY - ((1 - currentScale) * transY - currentY)) * deviceScale;
                let toolH = document.getElementById('tooltip').offsetHeight;
                let toolW = 110;
                if (toolY < 935 / 2) {
                    tooltip.style('left', `${toolX - toolW}px`).style('top', `${toolY + 5}px`);
                } else {
                    tooltip.style('left', `${toolX - toolW}px`).style('top', `${toolY - toolH - 5}px`);
                }
            }
        });
    }
    
    function judgeBugPoint(arr, id) {
        if (!arr || !arr.length || !id) return false;
        let bugLine = arr.filter(d => {
            return d.points.indexOf(id) > -1
        });
        if (bugLine.length) {
            removeTooltip()
            tooltip.select('#tool-head').html(`<span>${id}</span><div class="deletes" onclick="removeTooltip()">×</div>`);
            bugLine.forEach(d => {
                let item = tooltip.select('#tool-body').append('div').attr('class', 'tool-item');
                item.html(`
                    <div class="tool-content">
                        <div style="color: #ffffff;border-bottom: 2px solid ${d.lc};">
                            <span style="background: ${d.lc};padding: 4px 6px;">${d.lb}</span>
                        </div>
                        <div>
                            <div class="content-left">封路时间</div><div class="content-right">${d.duration}</div>
                        </div>
                        <div>
                            <div class="content-left">封路原因</div><div class="content-right">${d.cause}</div>
                        </div>
                        <div>
                            <div class="content-left">封路路段</div><div class="content-right">${d.start}-${d.end}</div>
                        </div>
                    </div>
                `)
            })
            d3.select('#tooltip').style('display', 'block');
            return true;
        } else {
            return false;
        }
    }
    
    function removeTooltip() {
        d3.selectAll('.tool-item').remove();
        d3.select('#tooltip').style('display', 'none');
    }
    
    function zoomed() {
        removeTooltip();
        let {x, y, k} = d3.event.transform;
        currentScale = k;
        currentX = x;
        currentY = y;
        group.transition().duration(50).ease(d3.easeLinear).attr("transform", () => `translate(${x + transX * k}, ${y + transY * k}) scale(${k})`)
    }
    
    function getCenter(str) {
        if (!str) return null;
        let x, y;
        let tempArr = [];
        let tempX = [];
        let tempY = [];
        str.split(' ').forEach(d => {
            if (!isNaN(d)) {
                tempArr.push(d)
            }
        })
    
        tempArr.forEach((d, i) => {
            if (i % 2 == 0) {
                tempX.push(parseFloat(d))
            } else {
                tempY.push(parseFloat(d))
            }
        })
        x = (d3.min(tempX) + d3.max(tempX)) / 2;
        y = (d3.min(tempY) + d3.max(tempY)) / 2;
        return [x, y]
    }
    
    function renderAllStation() {
        let nameArray = subway.getLineNameArray();
        let len = Math.ceil(nameArray.length / 5);
        let box = d3.select('#menu').append('div')
        .attr('class', 'name-box')
        for (let i = 0; i < len; i++) {
            let subwayCol = box.append('div')
            .attr('class', 'subway-col')
            let item = subwayCol.selectAll('div')
            .data(nameArray.slice(i * 5, (i + 1) * 5))
            .enter()
            .append('div')
            .attr('id', d => d.lid)
            .attr('class', 'name-item')
            item.each(function (d) {
                d3.select(this).append('span').attr('class', 'p_mark').style('background', d.lc);
                d3.select(this).append('span').attr('class', 'p_name').text(d.lb);
                d3.select(this).on('click', d => {
                    selected = true;
                    d3.selectAll('.origin').style('opacity', 0.1);
                    d3.selectAll('.temp').remove();
                    renderCurrentLine(d.lid);
                    renderCurrentPoint(d.lid);
                    let arr = getCenter(d3.select(`path.${d.lid}`).attr('d'));
                    svg.call(zoom.transform, d3.zoomIdentity.translate((width / 2 - transX) - arr[0] - (arr[0] + transX) * (currentScale - 1), (height / 2 - transY) - arr[1] - (arr[1] + transY) * (currentScale - 1)).scale(currentScale));
                })
            })
        }
    }
    
    function scale(type) {
        if (type && currentScale + scaleStep <= scaleExtent[1]) {
            svg.call(zoom.transform, d3.zoomIdentity.translate((1 - currentScale - scaleStep) * transX - ((1 - currentScale) * transX - currentX) * (currentScale + scaleStep) / currentScale, (1 - currentScale - scaleStep) * transY - ((1 - currentScale) * transY - currentY) * (currentScale + scaleStep) / currentScale).scale(currentScale + scaleStep));
        } else if (!type && currentScale - scaleStep >= scaleExtent[0]) {
            svg.call(zoom.transform, d3.zoomIdentity.translate((1 - (currentScale - scaleStep)) * transX - ((1 - currentScale) * transX - currentX) * (currentScale - scaleStep) / currentScale, (1 - (currentScale - scaleStep)) * transY - ((1 - currentScale) * transY - currentY) * (currentScale - scaleStep) / currentScale).scale(currentScale - scaleStep));
        }
    }

    上面是大部分代码,想看全部的可以查看demo。

    原文链接https://www.mrguo.link

    大家转载请注明一下原文郭志强的博客 谢谢大家

     

  • 相关阅读:
    不运用正则排除出现的特定数的数字
    重绘 贝赛尔曲线特效
    缓动类gs.TweenLite示例
    画方格(二维数组)
    递归函数
    鼠标经过延时出现Hint
    鼠标控制元件移动带缓动 鼠标点击发射子弹
    A碰到B之后持续加速度的时间问题
    播放完成之后移除动画
    hdu 1032 The 3n + 1 problem (数学)
  • 原文地址:https://www.cnblogs.com/vadim-web/p/11941568.html
Copyright © 2011-2022 走看看