vue3實現模擬地圖上,站點名稱按需顯示的功能

邹琼俊發表於2024-06-21

很久很久沒有更新部落格了,因為實在是太忙了,每天都有公司的事情忙不完.......

最近在做車輛模擬地圖,在實現控制站點名稱按需顯示時,折騰了好一段時間,特此記錄一下。最終介面如下圖7路所示:

站點顯示需求:首末站必須顯示,從第一個站開始,如果站點名稱能顯示下,則顯示,如果站點名稱會重疊則隱藏,以此類推。當介面寬度變化時,車輛模擬地圖自動變化,保證站點名稱能夠最大限度的顯示。

最開始我用的比例換演算法,演算法複雜度是O,結果總是不準。儘管一開始我就覺得演算法的複雜度應該是O2。我之前卻一直想著只遍歷一次就算出來,我也嘗試過把需求描述得很詳細去問chatgpt,可是它就像個傻子一樣輸出各種演算法錯誤程式碼。

需要注意的地方:由於站點的名稱內容是千奇百怪的,可以有空格,各種特殊圖示,所以站點文字的長度計算是一個問題,這裡是透過canvas來計算的。還有,這裡我新增了一個限制,站點文字內容我最大顯示120px,超出隱藏並顯示省略號,站點名稱上新增了title顯示全稱。

/**
 * 獲取站點名稱
 * @param item 
 * @param showFullName 是否總是顯示站點全名
 */
/** */
export const getSiteName = (item: any,showFullName?:boolean=false) => {
  const { siteSign } = simulatedMapConf.value;
  let name = '';
  if (siteSign == 'firstWord') {
    name = getSubStrByPreNum(item.stationName);
  } else if (siteSign == 'order') {
    name = item.stationSeq + '';
  } else {
    if(showFullName){
      name=item.stationName;
    }else{
      name =item.show? item.stationName:''; //show控制站點名稱是否顯示
    }
  }
  return name || '';
}
/**
 * 獲取站點名稱寬度
 * @param item 站點物件
 * @param showFullName 是否總是顯示站點全名
 * @returns 
 */
export const getSiteNameWidth = (item: any,showFullName?:boolean=false) => {
  const name =showFullName?item.stationName: getSiteName(item,showFullName);
  const width= calculateStringWidth(name);
  return width>siteMaxWidth?siteMaxWidth:width;
}
/**
 * 根據字串計算出介面渲染的寬度
 * @param str 
 * @returns 
 */
function calculateStringWidth(str:string) {
  // 建立一個虛擬的 <canvas> 元素
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  // 設定字型樣式
  ctx.font = '12px sans-serif';
  // 使用 canvas 的 measureText 方法測量字串的寬度
  const width = ctx.measureText(str).width;
  // 返回計算出的寬度
  return width;
}

最核心的演算法:

    //計算上行站點,控制站點是否顯示在模擬地圖上
    function calcSite(station: any, lineWidth: number) {
        if (station.length < 1) return [];
        station.forEach((f: any, index) => {
            f.show=false;
        });
        let lastSiteLength = getSiteNameWidth(station[station.length - 1], true) / 2;//站點文字寬度
        let lastLeft = getSiteCx(station[station.length - 1], station.length - 1);//最後一個站點left
        lastLeft = toDecimal(lastLeft - lastSiteLength);
        station.forEach((f: any, index) => {
            let siteLength = getSiteNameWidth(f, true);//站點文字寬度
            let bigHalf = siteLength / 2;//獲取當前的半寬
            f.left = getSiteCx(f, index); 
            if (index == 0 || index == station.length - 1) { //第一項和最後一項必須顯示
                f.show = true;
            } else {
                const preShowIndex = getLastTrueIndex(station); //獲取前面最近一個顯示站點的索引
                const preEndLeft = toDecimal(station[preShowIndex].left + getSiteNameWidth(station[preShowIndex], true) / 2);//上一項顯示的站點名稱結束left位置
                f.show = toDecimal(f.left - bigHalf) >=preEndLeft && preEndLeft < lastLeft; //如果上一個顯示站點文字的結尾位置 小於等於 當前站點文字的開始位置  並且小於最後一個站點文字的開始位置
if (f.show && toDecimal(f.left + bigHalf) > lastLeft) {
                    f.show = false;
                }
            }
        })
    }

獲取前面最近一個顯示站點的索引:

    //獲取list集合中最後一項show的index位置
    function getLastTrueIndex(dataList: any) {
        // 從陣列末尾第2項開始向前遍歷
        for (let i = dataList.length - 2; i >= 0; i--) {
            if (dataList[i].show === true) {
                return i;  // 返回第一個找到的最後一個為true的索引
            }
        }
        return -1;  // 如果未找到符合條件的物件,返回-1
    }

下行站點的計算有些差別,因為下行站點是從右至左,所以left基本上是反著的:

    //計算下行站點,控制站點是否顯示在模擬地圖上 getDownSiteCx
    function calcDownSite(station: any, lineWidth: number) {
        if (station.length < 1) return [];
        station.forEach((f: any, index) => {
            f.show=false;
        });
        let lastSiteLength = getSiteNameWidth(station[station.length - 1], true) / 2;//站點文字寬度
        let lastLeft = getDownSiteCx(station[station.length - 1], station.length - 1);//最後一個站點left
        lastLeft = toDecimal(lastLeft + lastSiteLength);
        station.forEach((f: any, index) => {
            let siteLength = getSiteNameWidth(f, true);//站點文字寬度
            let bigHalf = siteLength / 2;//獲取當前的半寬
            f.left = getDownSiteCx(f, index); 
            if (index == 0 || index == station.length - 1) { //第一項和最後一項必須顯示
                f.show = true;
            } else {
                const preShowIndex = getLastTrueIndex(station); //獲取前面最近一個顯示站點的索引
                const preEndLeft = toDecimal(station[preShowIndex].left - getSiteNameWidth(station[preShowIndex], true) / 2);//上一項顯示站的的結束left位置
                f.show = toDecimal(f.left + bigHalf) <=preEndLeft && preEndLeft > lastLeft;
                if (f.show && toDecimal(f.left - bigHalf) < lastLeft) {
                    f.show = false;
                }
            }
        })
    }

另外獲取站點中心點位置的方法

    //獲取上行站點水平x位置
    const getSiteCx = (item: any, index: number) => {
        return startleft.value + dLayout.lineWidth * index;
    }
    //獲取下行站點水平x位置
    const getDownSiteCx = (item: any, index: number) => {
        return downStartleft.value - layout.endLine - dLayout.downLineWidth * index;
    }

說明:站點的佈局採用css絕對定位。第一個版本這塊我是採用的svg畫的,後來發現擴充套件起來越來越麻煩,週末就在家花了半天時間全部改造為html實現了。

我最開始的有問題程式碼是上下行站點共用的,最大的問題是會出現跳站點顯示的情況,程式碼如下的:

    //計算站點,控制站點是否顯示在模擬地圖上
    function calcSite(station: any, lineWidth: number) {
        let availableWidth = (station.length - 1) * lineWidth; //總長度
        //記錄顯示站點的長度
        let totalLength = 0;
        station.forEach((f: any, index) => {
            let siteLength = getSiteNameWidth(f, true);
            let bigHalf =siteLength / 2;//獲取比較大的半寬
            let bigHalfPre = 0;
            //計算上一項的文字半寬
            if (index >= 1) {
                let siteLengthPre = getSiteNameWidth(station[index - 1], true);
                bigHalfPre =siteLengthPre / 2;
            }
            f.left = toDecimal(lineWidth * index);
            f.show =index==0?true: f.left >=toDecimal(totalLength);
            if(index >= 1&&station[index-1].show&&bigHalf+bigHalfPre>lineWidth){
                f.show=false;
            }
            if (f.show) {
                let times = getDivisor(siteLength, lineWidth);
                totalLength += times * lineWidth;
            }
        })
    }
/**
 * 兩個數相除有餘數時結果加1
 * @param all 被除數 站點寬度
 * @param num 除數  線寬
 * @returns 
 */
export const getDivisor=( all:number,item:number)=>{
if(all<=item) return 1;
let diff:number=0;
if(item<=20){
diff=2.5;
}
if(item<=30){
diff=2;
}
else if(item<=40){
diff=1.5;
}
else if(item<=46){
diff=1.05;
}
else if(item<=50){
diff=1;
}
return all%item==0?(all/item):(Math.ceil(all/item)+diff);
}

完!

相關文章