從拿到班車手冊.xls到搜索附近班車地點

原由

七月份要去某廠報道了,異地租房的時候發現想租一個有公司班車的地方,殊不知道哪裏有班車。展轉流傳出班車手冊後發現搜索實在是太不方便了,因而有了一個主義,想作一個能夠搜索房子地址,找出附近班車點(相似大衆點評的定位搜索附近餐館的功能)。如今作的差很少了,發現好像原本公司就有作這個東西。。權當學一下一些位置匹配的技術了。
最後成果是這樣子的:javascript

clipboard.png

大頭針是輸入的位置(福田中學),附近的藍點就是一個一個站點。因爲一個站點他會在上班下班夜班不一樣的線路的不一樣站點位置,會在不一樣時刻到達,所以聚合爲多個同一站點的數據會聚合爲一個點。點擊藍色的站點就會在下面顯示出這個站點所在的全部線路。html

具體實現

下面將分爲幾個步驟講一下具體使用了什麼方法什麼技術:vue

1. 原始數據轉換成咱們須要的數據

一開始拿到的是excel手冊,因此咱們有的原始數據是長成這樣的(忽略的從excel中導出的步驟):
['A(B門口)(07:30)→C(政府前100米天橋下)(07:45)→D(2站臺前10米)→E→F(09:12)', ...路線二, ...路線三]
而後咱們須要作的事情是:java

  1. 從數組裏把每一條線路的站點拆分紅一個個獨立的單元
    這一步比較簡單,str.split('→')
  2. 每個單元分離出站點和時間
    這一步要作的就多一點點了,須要用到正則匹配,並且由於站點的名字實際上是有多種的,須要考慮到多種狀況。所以個人方法是:api

    1. 先用/(.*)(\([0-9:]*\))/分離時間和站點,由於只有時間是左右括號內只包含數字和:的。
    2. 實際上站點名稱裏有一些非法字符,所以還須要進行一步過濾station.replace(/([^\u4e00-\u9fa5\(\)\d])/g, '')
  3. 每個站點獲取到經緯度
    這個就沒啥好說的了。。調用騰訊地圖的api,不過因爲調用api有每秒請求數和每日請求數的限制,用異步回調加定時器的方式模擬了休眠,而後運行腳本慢慢等結果返回就行了。

2. 怎麼在一堆經緯度表示的點裏找出附近的點呢(geohash)

我參考的資料
簡單介紹一下geohash就是,把經緯度按照必定的規則去映射出一個hash字符串,在後續搜索的時候,只要hash字符串匹配程度足夠高就能夠認爲這兩個點是相近的。具體的內容能夠閱讀上面的參考資料。下面給我javascript代碼的實現。數組

function geoHashCode (num, range) {
    range = [-range, range]
    let retCode = []
    for (let i = 1; i <= 20; i++) {
        let middle = (range[0] + range[1]) / 2
        let code = num < middle ? '0' : '1'
        if (code === '0') {
            range[1] = middle
        } else {
            range[0] = middle
        }
        retCode.push(code)
    }
    return retCode
}
function geoHash ({ lng, lat }) { // lng: 經度, lat: 緯度
    let lngCode = geoHashCode(lng, 180)
    let latCode = geoHashCode(lat, 90)

    // 偶數位放經度,奇數位放緯度,把2串編碼組合生成新串
    let code = []
    for (let i = 0; i < 40; i++) {
        if (i % 2 === 0) { // 偶數
            code[i] = lngCode[i / 2]
        } else {
            code[i] = latCode[(i - 1) / 2]
        }
    }

    const base32 = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

    let newCode = []
    const splitLen = 5
    for (let i = 0; i < 8; i++) {
        newCode.push(code.slice(i * 5, i * 5 + 5).join(''))
    }

    // base32編碼
    newCode = newCode.map(item => base32[parseInt(item, 2)]).join('')

    return newCode
}

通過上述步驟,咱們能夠獲得什麼呢?
一個很大的list,每個單元爲緩存

{
    station:班車名字,
    location:該點的經緯度,
    name:屬於上班下班夜班中的哪個,
    lineIndex:屬於該班車類型的拿一條線路,
    stationIndex:屬於該線路里的第幾個站點,
    time:到站時間,
    geohash:該點經緯度映射出的的geohash
}

到這一步其實已經能夠作到輸入一個點,匹配出附近班車的點了,只要把輸入的點經過api查詢出經緯度,再轉化成geohash,最後遍歷這個list把匹配程度足夠高的點挑出來就能夠了。
可是其實咱們有5000個這樣的點,在頁面上不斷作這種遍歷匹配我以爲挺蠢的,因而我想到構建一個匹配樹。把一組hash映射成一個匹配森林,而後輸入點的geohash不斷尋找匹配節點去遍歷這個森林的時候能夠徹底避開不匹配的項去提升匹配效率。舉個例子就是:異步

clipboard.png

咱們根據左側的hashList映射出右側的匹配森林,因爲geohash的精度關係是會出現多個站點的geohash是同樣的。所以我在葉子節點裏用一個數組存放全部的對應站點信息。當咱們要匹配'wsc2'時咱們能夠一直搜索到葉子節點,取出‘站點1,站點2’,可是有時候咱們要搜索的geohash沒辦法匹配到葉子節點,咱們就要先判斷當前精度是否足夠高,偏差會不會太大,好比咱們認爲匹配了三個前綴字符的時候精度就足夠高了,那麼搜索'ws11'的時候因爲只匹配到兩個,不該返回結果。而匹配'wsc3'的時候,能夠匹配到前綴字符'wsc',雖然沒有到葉子節點,可是咱們能夠認爲以'wsc'爲根(大概是那個意思大家應該明白)的樹的全部葉子節點均可以認爲是這個geohash的附近節點,也就是返回'站點1,站點2,站點6'。至於偏差範圍能夠看上面的參考文獻。async

3.構建頁面須要的內容

  1. 騰訊地圖或者其餘地圖的開放接口
  2. 獲取輸入地址轉化爲經緯度和geohash
  3. 查找樹獲取匹配的地址在list中的index
  4. 聚合相同經緯度的點爲一個繪製點
    將經緯度做爲鍵名構建一個map
  5. 繪製,附近的點爲藍色,輸入的點爲大頭針,綁定附近的點的點擊事件(渲染列表,生成該點的全部線路信息)

其餘

這個小玩具就這麼結束了,中間其實還有一些值得一提的地方。我也就一塊兒記下來了,感受仍是挺有趣的,作一些好玩的東西。編碼

定時器+異步模擬休眠

  • 必備知識點: sync/await(只是由於這麼寫看起來很爽,沒有別的意思

function sleep () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve()
        }, 500)
    }) 
}
(async function () {
    let i = locationList.length // 計數器
    let newList = []
    while (i !== -1) {
        let item = locationList.pop() // 取出要查詢的點
        let location
        try {
            if (locationMap[item.station]) { // 若是這個點請求過了就直接用緩存信息
                location = locationMap[item.station]
            } else {
                location = await getXY(item.station) // 調用api獲取經緯度
                locationMap[item.tation] = location // 緩存經緯度信息
                await sleep() // 休眠
            }
            item.location = location
            item.geoHash = geoHash(location) // 獲取geohash
            newList.push(item)
        } catch (e) { // 請求失敗了,把這個點推回去從新請求
            console.log(e)
            locationList.push(item)
            i++
        }
        i--
        console.log(i)
    }
})()

數據劫持

其實一開始設計的時候沒有查詢地點附近的班車站點功能的。而是顯示上班線路下班線路的功能。不一樣線路之間的轉換用了數據劫持的方式,也就是vue實現數據綁定的Object.defineProperty,還真的挺有意思的,建議你們也能夠用這個試一下。另外還有單頁應用路由裏面的hashchange事件。這些都是些能夠再創造的api。

相關文章
相關標籤/搜索