canvas实现路段连通和路段效果

canvas实现路段连通和路段效果_第1张图片

源码

// utils.js

/**
 * 通过判断type返回目标图片的地址
 * @param   {String}   type       图片类型
 * @returns {String}   url        目标图片的地址
 */
export function setImgUrl(type) {
    let url;
    switch (type) {
        case "track":
            url = require("./image/car.png");
            break;
        case "gantry":
            url = require("./image/equipmentIcon.png");
            break;
        case "station":
            url = require("./image/dataIcon.png");
            break;
        case "hub":
            url = require("./image/userIcon.png");
            break;
        case "hh-station":
            url = require("./image/homeIcon.png");
            break;
        case "icon-hub":
            url = require("./image/password.png");
            break;
        default:
            url = require("./image/user.png");
            break;
    }
    return url;
}

/**
 * 为数据对象添加位置标记字段
 * @param {Object.} data 原始数据对象,键为分组标识,值为对象数组
 * @returns {Object.} 处理后的新对象,数组元素添加 position 字段(start/end/transition)
 */
export function addPositionFields(data) {
    const result = {};

    // 遍历对象的每个属性
    for (const key in data) {
        if (data.hasOwnProperty(key)) {
            const array = data[key];
            result[key] = [];

            // 处理数组中的每个元素
            for (let i = 0; i < array.length; i++) {
                const item = { ...array[i] }; // 创建新对象避免修改原数据

                // 根据位置设置 position 字段
                if (i === 0) {
                    item.position = 'start';
                } else if (i === array.length - 1) {
                    item.position = 'end';
                } else {
                    item.position = 'transition';
                }

                result[key].push(item);
            }
        }
    }

    return result;
}

/**
 * 为数据对象添加链式连接关系
 * @param {Object.} data 原始数据对象,键为分组标识,值为对象数组
 * @returns {Object.} 处理后的新对象,数组元素添加 source(当前节点ID)和 target(下一节点ID)字段,
 *                                  格式为 { 分组标识: [{ source: "key-i", target: "key-(i+1)" }, ...] }
 * @example
 * // 输入
 * { group1: [{}, {}, {}] }
 * // 输出
 * { 
 *   group1: [
 *     { source: 'group1-0', target: 'group1-1' },
 *     { source: 'group1-1', target: 'group1-2' },
 *     { source: 'group1-2', target: null }
 *   ]
 * }
 */
export function setSourceAndTarget(data) {
    const result = {};
    // 遍历对象的每个属性
    for (const key in data) {
        if (data.hasOwnProperty(key)) {
            const array = data[key];
            result[key] = [];
            // 处理数组中的每个元素
            for (let i = 0; i < array.length; i++) {
                const item = { ...array[i] }; // 创建新对象避免修改原数据

                // 设置节点连接关系:
                // - 非末尾节点:target指向下一节点(key-(i+1))
                // - 末尾节点:target设为null表示终点
                if (i !== array.length - 1) {
                    item.source = `${key}-${i}`;
                    item.target = `${key}-${i + 1}`;
                } else {
                    item.source = `${key}-${i}`;
                    item.target = null;
                }
                result[key].push(item);
            }
        }
    }
    return result;
}

/**
 * 格式化并去重点位数据
 * @param   {Object.} pointData 原始点位数据对象,键为分组标识,值为点位数组
 * @returns {Array} 去重后的点位数组(根据 code 字段去重)
 */
export function pointFormat(pointData) {
    // let _pointData = [];
    // for (let key in pointData) {
    //     const gsPointList = pointData[key];
    //     // 获取点位数据(去重)
    //     gsPointList.forEach((ele) => {
    //         const info = _pointData.find(
    //             (item) => item.code === ele.code
    //         );
    //         if (!info) {
    //             _pointData.push(ele);
    //         }
    //     });
    // }
    // return _pointData;
    const uniquePoints = new Set();
    const result = [];

    for (const gsPointList of Object.values(pointData)) {
        for (const point of gsPointList) {
            if (!uniquePoints.has(point.code)) {
                uniquePoints.add(point.code);
                result.push(point);
            }
        }
    }

    return result;
}

/**
 * 将地理坐标转换为屏幕坐标
 * @param   {Array}  data    原始数据数组,需包含 lat(纬度)和 lon(经度)字段
 * @param   {number}         width   容器宽度(单位:像素)
 * @param   {number}         height  容器高度(单位:像素)
 * @returns {Array} 转换后的数据数组,新增屏幕坐标 x/y 和样式属性
 */
export function pointCoordinateSwitch(data, width, height) {
    // 过滤无效点位
    const _data = data.filter((ele) => ele.lat && ele.lon);
    if (!_data.length) return [];
    // 初始化最大最小值
    let latMin = Infinity,
        latMax = -Infinity;
    let lonMin = Infinity,
        lonMax = -Infinity;

    // 单次遍历数组,计算最大最小值
    for (let i = 0; i < data.length; i++) {
        const { lat, lon } = data[i];

        if (lat < latMin) latMin = lat;
        if (lat > latMax) latMax = lat;
        if (lon < lonMin) lonMin = lon;
        if (lon > lonMax) lonMax = lon;
    }
    // 此处 减去 200 为了保证点位都显示在容器内,后续点位的横纵坐标 +100
    width -= 200;
    height -= 200;
    return data.map((ele) => ({
        ...ele,
        imgType: "square",
        size: 10,
        x: ((ele.lon - lonMin) / (lonMax - lonMin)) * width + 100,
        y:
            height -
            ((ele.lat - latMin) / (latMax - latMin)) * height +
            100,
    }));
}

/**
 * 将图片中的相对坐标转换为页面中的绝对坐标
 * @param {Array} points - 点位数组,每个点位包含x,y坐标(相对图片的坐标)
 * @param {Object} imageInfo - 图片信息对象
 * @param {number} imageInfo.width - 图片原始宽度
 * @param {number} imageInfo.height - 图片原始高度
 * @param {Object} containerInfo - 容器信息对象
 * @param {number} containerInfo.width - 页面容器宽度
 * @param {number} containerInfo.height - 页面容器高度
 * @param {string} [mode='contain'] - 图片适配模式:'contain'(默认)|'fill'
 * @param {number} [margin=100] - 四周边距(像素)
 * @returns {Array} - 转换后的坐标数组
 */
export function convertImageCoordsToPageCoords(points, imageInfo, containerInfo, mode = 'contain', margin = 100) {
    const { width: imgWidth, height: imgHeight } = imageInfo;
    let { width: containerWidth, height: containerHeight } = containerInfo;

    // 应用边距,调整有效容器尺寸
    const effectiveWidth = Math.max(containerWidth - 2 * margin, 1);
    const effectiveHeight = Math.max(containerHeight - 2 * margin, 1);

    // 计算图片在有效容器区域中的实际显示尺寸和位置
    let displayWidth, displayHeight, offsetX = margin, offsetY = margin;
    const imgRatio = imgWidth / imgHeight;
    const containerRatio = effectiveWidth / effectiveHeight;

    if (mode === 'fill') {
        // 填充模式,直接拉伸填满有效容器区域
        displayWidth = effectiveWidth;
        displayHeight = effectiveHeight;
    } else {
        // 默认contain模式,保持比例完整显示在有效容器区域内
        if (imgRatio > containerRatio) {
            displayWidth = effectiveWidth;
            displayHeight = displayWidth / imgRatio;
            offsetY += (effectiveHeight - displayHeight) / 2;
        } else {
            displayHeight = effectiveHeight;
            displayWidth = displayHeight * imgRatio;
            offsetX += (effectiveWidth - displayWidth) / 2;
        }
    }

    // 计算缩放比例
    const scaleX = displayWidth / imgWidth;
    const scaleY = displayHeight / imgHeight;

    // 转换每个点的坐标
    return points.map(point => {
        return {
            ...point,
            x: offsetX + (point.x * scaleX),
            y: offsetY + (point.y * scaleY),
            // 保留原始数据
            originalX: point.x,
            originalY: point.y,
        };
    });
}

/**
 * 生成站点连接线基础数据
 * @param   {Array} linkData 原始链路数据,需包含 code(站点编码)和 target(目标站点编码)字段
 * @returns {Array} 连接线数组,每个元素包含:
 *                          - source: 源站点对象
 *                          - target: 目标站点对象
 *                          - gsName: 所属高速路名称
 */
function getLineData(linkData) {
    const res = [];

    // 创建一个站点代码与站点对象的映射
    const stationMap = linkData.reduce((map, station) => {
        map[station.code] = station;
        return map;
    }, {});

    // 遍历原始的站点列表来构建最终的结果
    for (let i = 0; i < linkData.length; i++) {
        const currentStation = linkData[i];
        const targetCode = currentStation.target;

        // 如果目标站点存在
        if (targetCode && stationMap[targetCode]) {
            const targetStation = stationMap[targetCode];
            // 创建一个新的对象,将source和target配对
            res.push({
                source: currentStation,
                target: targetStation,
                gsName: currentStation.gsName,
            });

            // 标记该站点的目标站点为null,防止重复配对
            stationMap[targetCode] = null;
        }
    }

    return res;
}

/**
 * 生成高速公路连接线数据集
 * @param   {Object.} pointObj  分组点位对象,键为高速路名称,值为该路段点位数组
 * @param   {Array} pointData                 全量点位数据集,用于查找箭头标记点
 * @returns {Array} 包含完整坐标信息的连接线数组,每个元素包含:
 *                  - gsName: 高速路名称
 *                  - source: 起点坐标及元数据
 *                  - target: 终点坐标及元数据
 */
export function getAllLinkLineHaveArrowData(pointObj, pointData) {
    // 获取连接线数据(遍历原数据集合,由于箭头点位在原数据集合中
    //   不存在,所以需要在遍历时为每一条高速添加上箭头点位)
    let _lineData = [];
    for (let key in pointObj) {
        let gsArrowPoint = pointData.find(
            (ele) => ele.gsName === key && !ele.type
        );
        gsArrowPoint.source = gsArrowPoint.code;
        // 修改箭头点位前面的一个点位的target值
        pointObj[key][pointObj[key].length - 1].target =
            gsArrowPoint.code;
        pointObj[key].push(gsArrowPoint);
        _lineData.push(...getLineData(pointObj[key]));
    }
    // 根据已获取到的连线数据,结合点位数据,设置x,y坐标
    return _lineData.map((ele) => {
        const _target = pointData.find(
            (item) => item.code === ele.target.code
        );
        const _source = pointData.find(
            (item) => item.code === ele.source.code
        );
        return {
            gsName: ele.gsName,
            source: { ...ele.source, x: _source.x, y: _source.y },
            target: { ...ele.target, x: _target.x, y: _target.y },
        };
    });
}

export function getAllLinkLineNoArrowData(pointObj, pointData) {
    // 获取连接线数据(遍历原数据集合,由于箭头点位在原数据集合中
    //   不存在,所以需要在遍历时为每一条高速添加上箭头点位)
    let _lineData = [];
    for (let key in pointObj) {
        let gsArrowPoint = pointData.find(
            (ele) => ele.gsName === key
        );
        gsArrowPoint.source = gsArrowPoint.code;
        _lineData.push(...getLineData(pointObj[key]));
    }
    // 根据已获取到的连线数据,结合点位数据,设置x,y坐标
    return _lineData.map((ele) => {
        const _target = pointData.find(
            (item) => item.code === ele.target.code
        );
        const _source = pointData.find(
            (item) => item.code === ele.source.code
        );
        return {
            gsName: ele.gsName,
            source: { ...ele.source, x: _source.x, y: _source.y },
            target: { ...ele.target, x: _target.x, y: _target.y },
        };
    });
}

export function calculateGSNamePosition(lastOne,
    lastTwo,
    label,
    distance,
    direction,
    type) {
    // 计算lastOne到lastTwo的向量
    const vx = lastOne.x - lastTwo.x;
    const vy = lastOne.y - lastTwo.y;

    // 计算lastOne到lastTwo的距离
    const dist = Math.sqrt(vx * vx + vy * vy);
    // 计算单位向量
    const unitX = vx / dist;
    const unitY = vy / dist;

    let newX, newY;

    if (direction === "front") {
        // 计算反向单位向量
        const reverseUnitX = -unitX;
        const reverseUnitY = -unitY;
        // 根据反向单位向量计算前一个点的位置,前一个点距离lastOne的横纵坐标为指定的距离
        newX = lastOne.x - reverseUnitX * distance;
        newY = lastOne.y - reverseUnitY * distance;
    } else if (direction === "after") {
        // 根据单位向量计算a3的位置,a3距离lastOne的横纵坐标都为200
        newX = lastOne.x + unitX * distance;
        newY = lastOne.y + unitY * distance;
    }
    if (type === "text") {
        return { x: newX, y: newY + 4, label, gsName: label };
    } else if (type === "arrow") {
        const num =
            new Date().getTime() + parseInt(Math.random() * 10000);
        return {
            x: newX,
            y: newY,
            // type: "station",
            type: null,
            gsName: label,
            code: num,
            source: lastOne.code,
            target: null,
        };
    }
}

export function addArrowPoint(data) {
    // 创建一个新的数组来存储最终的结果
    const result = [];

    // 遍历原始数组
    for (let i = 0; i < data.length; i++) {
        // 当前项
        const current = data[i];

        // // 如果position为"start",先插入type为"arrow"的数据
        // if (current.position === "start") {
        // 	if (data[i + 1].gsName === current.gsName) {
        // 		// 计算首部箭头坐标
        // 		const frontArrow = this.calculateGSNamePosition(
        // 			current,
        // 			data[i + 1],
        // 			current.gsName,
        // 			80,
        // 			"front",
        // 			"arrow"
        // 		);
        // 		result.push(frontArrow); // 插入箭头数据
        // 	}
        // }

        // 插入当前项
        result.push(current);

        // 如果position为"end",再插入type为"arrow"的数据
        if (current.position === "end") {
            if (data[i - 1].gsName === current.gsName) {
                // 计算尾部箭头坐标
                const afterArrow = calculateGSNamePosition(
                    current,
                    data[i - 1],
                    current.gsName,
                    50,
                    "after",
                    "arrow"
                );
                result.push(afterArrow); // 插入箭头数据
                current.target = afterArrow.code;
            }
        }
    }

    return result;
}

export function calculateMidPoints(trackList) {
    const midPoints = [];

    for (let i = 0; i < trackList.length - 1; i++) {
        const currentPoint = trackList[i];
        const nextPoint = trackList[i + 1];

        // 检查当前点和下一个点的type和position是否符合条件
        if (
            currentPoint.type &&
            nextPoint.type &&
            currentPoint.position !== "end"
        ) {
            // 计算中间点
            const midLat = (currentPoint.x + nextPoint.x) / 2;
            const midLon = (currentPoint.y + nextPoint.y) / 2;

            // 将中间点信息存储到数组中
            midPoints.push({
                x: midLat,
                y: midLon,
                label: `${(currentPoint.code + nextPoint.code) / 2}`,
                code: (currentPoint.code + nextPoint.code) / 2,
                type: "gantry",
                gsName: (currentPoint.code + nextPoint.code) / 2,
                position: "mid",
            });
        }
    }

    return midPoints;
}
 
  
// data.js

export const colorList = [
    "#409EFF",
    "#67C23A",
    // "#E6A23C",
    // "#F56C6C",
    // "#909399",
]

export const pointData = Object.freeze({
    '申嘉湖': [
        {
            lat: 30.577337,
            lon: 120.293738,
            label: "新市枢纽",
            code: 3206,
            source: 3206,
            target: 4053,
            road: 3,
            type: "icon-hub",
            gsName: "申嘉湖",
        },
        {
            lat: 30.552239,
            lon: 120.363261,
            label: "洲泉",
            code: 4053,
            source: 4053,
            target: 4055,
            road: 3,
            type: "gantry",
            gsName: "申嘉湖",
        },
        {
            lat: 30.561971,
            lon: 120.415528,
            label: "崇福北",
            code: 4055,
            source: 4055,
            target: 4057,
            road: 3,
            type: "gantry",
            gsName: "申嘉湖",
        },
        {
            lat: 30.560235,
            lon: 120.456098,
            label: "凤鸣",
            code: 4057,
            source: 4057,
            target: 4009,
            road: 3,
            type: "gantry",
            gsName: "申嘉湖",
        },
        {
            lat: 30.559178,
            lon: 120.487902,
            label: "凤鸣枢纽",
            code: 4009,
            source: 4009,
            target: null,
            road: [1, 2, 3],
            type: "icon-hub",
            gsName: "申嘉湖",
        },
    ],
    '申苏浙皖': [
        {
            lat: 30.719011,
            lon: 120.455571,
            label: "练市枢纽",
            code: 3203,
            source: 3203,
            target: 4447,
            road: 1,
            type: "icon-hub",
            gsName: "申苏浙皖",
        },
        {
            lat: 30.656565,
            lon: 120.475788,
            label: "乌镇南",
            code: 4447,
            source: 4447,
            target: 4449,
            road: 1,
            type: "gantry",
            gsName: "申苏浙皖",
        },
        {
            lat: 30.600793,
            lon: 120.482239,
            label: "梧桐",
            code: 4449,
            source: 4449,
            target: 4006,
            road: 1,
            type: "gantry",
            gsName: "申苏浙皖",
        },
        {
            lat: 30.559178,
            lon: 120.487902,
            label: "凤鸣枢纽",
            code: 4006,
            source: 4006,
            target: 4451,
            road: [1, 2, 3],
            type: "icon-hub",
            gsName: "申苏浙皖",
        },
        {
            lat: 30.542289,
            lon: 120.488243,
            label: "崇福",
            code: 4451,
            source: 4451,
            target: 4005,
            road: 2,
            type: "gantry",
            gsName: "申苏浙皖",
        },
        {
            lat: 30.509447,
            lon: 120.510563,
            label: "骑塘枢纽",
            code: 4005,
            source: 4005,
            target: null,
            road: 2,
            type: "icon-hub",
            gsName: "申苏浙皖",
        },
    ],

});

export const doubleTrackPointData = Object.freeze({
    '申嘉湖': [
        {
            lat: 30.577337,
            lon: 120.293738,
            label: "新市枢纽",
            code: 3206,
            source: 3206,
            target: 4053,
            road: 3,
            type: "icon-hub",
            gsName: "申嘉湖",
        },
        {
            lat: 30.552239,
            lon: 120.363261,
            label: "洲泉",
            code: 4053,
            source: 4053,
            target: 4055,
            road: 3,
            type: "gantry",
            gsName: "申嘉湖",
        },
        {
            lat: 30.561971,
            lon: 120.415528,
            label: "崇福北",
            code: 4055,
            source: 4055,
            target: 4057,
            road: 3,
            type: "gantry",
            gsName: "申嘉湖",
        },
        {
            lat: 30.560235,
            lon: 120.456098,
            label: "凤鸣",
            code: 4057,
            source: 4057,
            target: 4009,
            road: 3,
            type: "gantry",
            gsName: "申嘉湖",
        },
        {
            lat: 30.559178,
            lon: 120.487902,
            label: "凤鸣枢纽",
            code: 4009,
            source: 4009,
            target: null,
            road: [1, 2, 3],
            type: "icon-hub",
            gsName: "申嘉湖",
        },
    ],
    '申苏浙皖': [
        {
            lat: 30.719011,
            lon: 120.455571,
            label: "练市枢纽",
            code: 3203,
            source: 3203,
            target: 4447,
            road: 1,
            type: "icon-hub",
            gsName: "申苏浙皖",
        },
        {
            lat: 30.656565,
            lon: 120.475788,
            label: "乌镇南",
            code: 4447,
            source: 4447,
            target: 4449,
            road: 1,
            type: "gantry",
            gsName: "申苏浙皖",
        },
        {
            lat: 30.600793,
            lon: 120.482239,
            label: "梧桐",
            code: 4449,
            source: 4449,
            target: 4006,
            road: 1,
            type: "gantry",
            gsName: "申苏浙皖",
        },
        {
            lat: 30.559178,
            lon: 120.487902,
            label: "凤鸣枢纽",
            code: 4006,
            source: 4006,
            target: 4451,
            road: [1, 2, 3],
            type: "icon-hub",
            gsName: "申苏浙皖",
        },
        {
            lat: 30.542289,
            lon: 120.488243,
            label: "崇福",
            code: 4451,
            source: 4451,
            target: 4005,
            road: 2,
            type: "gantry",
            gsName: "申苏浙皖",
        },
        {
            lat: 30.509447,
            lon: 120.510563,
            label: "骑塘枢纽",
            code: 4005,
            source: 4005,
            target: null,
            road: 2,
            type: "icon-hub",
            gsName: "申苏浙皖",
        },
    ],
});
// index.vue






// canvasSecond.vue






// canvasRender.vue






transform 0.5s ease;
.el-icon-close {
position: absolute;
top: 0;
right: 0;
font-size: 16px;
}
.arrow {
position: absolute;
width: 0;
height: 0;
bottom: -8px;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 8px solid #fff; /* 这个颜色就是倒三角形的颜色 /
}
}
::v-deep .visible {
opacity: 1!important; /
完全显示 /
transform: scale(1)!important; /
正常大小 */
}
::v-deep .opacity-10 {
opacity: 1!important;
}
::v-deep .opacity-2 {
opacity: 0.2;
}
::v-deep .opacity-1 {
opacity: 0.1;
}
.empty {
height: 100%;
width: 100%;
display: flex;
position: absolute;
z-index: 10;
top: 0;
span {
margin: auto;
}
}
}

.action-panel {
    height: auto;
    width: auto;
    box-shadow: 0 4px 15px 0 rgba(0, 0, 0, .1);
    position: absolute;
    top: 12px;
    left: 12px;
    border-radius: 4px;
    display: flex;
    flex-direction: column;
    // align-items: center;
    background-color: #fff;
    padding: 12px;
    z-index: 99;
    .margin-right-6 {
        margin-right: 6px;
    }
    .container {
        height: 40px;
        display: flex;
        align-items: center;
        margin-top: 40px;
        .label {
            width: 60px;
            height: 40px;
            line-height: 40px;
            font-size: 14px;
            text-align: right;
        }
        .radio-button {
            height: 40px;
            display: flex;
            align-items: center;
        }
    }
    .track-info {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        padding-left: 60px;
        .el-radio {
            margin-top: 12px;
            margin-right: 0!important;
        }
    }
}

}

```

图片可以自行设置,按照对应的名称命名即可,这里就不上传图片

你可能感兴趣的:(canvas实现路段连通和路段效果)