vadim-web

上一章说了如何制作一个线路图,当然上一章是手写的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

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

 

 

分类:

技术点:

相关文章:

  • 2021-11-07
  • 2019-09-25
  • 2021-12-04
  • 2021-10-18
  • 2021-11-03
  • 2021-04-26
  • 2021-04-24
  • 2021-11-28
猜你喜欢
  • 2021-09-04
  • 2021-11-18
  • 2021-03-16
  • 2019-10-12
  • 2021-10-16
  • 2021-11-15
  • 2021-11-02
相关资源
相似解决方案