持續搞【附近】—長連接坐標流和「地理圍欄」(五)
- 2019 年 11 月 13 日
- 筆記
我們經過【附近】系列的二、三、四篇章後,已經基本了解了市面上用於解決LBS問題的幾種常見方案和做法,當然除了PostGre外… …那個有興趣的哥們可以考慮補一篇PostGre版本直接投稿。實際上前面的思路是很簡單的,算是循序漸進類型的,從MySQL到MongoDB再到ES,大概就是從GeoHASH到Google S2再到R樹們。我沒有在文章里顯式地說這些但是背後就是這些,往深處地挖掘全靠諸位自己了~
今天這篇可以徹底擺脫這些了,說句實話我自己都快噁心地想吐了,今兒個咱整點兒稍微不一樣的:

所以今天主要問題就兩個:
- 多邊形圍欄
- 長連接坐標流
眾所周知,我們在使用下面這款著名租車軟體的時候,總是會彈出下面的運營範圍提示,我貼一張圖你們感受一下:


一般是我們開完車後停車的時候,會提示我們:你停的這個地方尚在我們運營範圍之外,如果你非要這裡停車,我們會象徵性收取你5塊錢運營費之類云云。那麼,一般我們此時該怎麼辦?那就是掉頭往回騎一直到APP提醒你在運營範圍之內即可…
除此之外,在【次著名】已下架的陌生社交APP — 探探的卡牌介面上你有時候會看到如下提示:某某某在史各庄與曾您擦肩而過(由於本人從來沒有安裝過此類軟體,所以並不能提供截圖了)。
此處的一個關鍵技術點就是多邊形~我們在資料庫里添加一坨坐標,畫成一個閉合的多邊形。在使用APP的時候,APP與伺服器建立一個長連接,不斷地上報自己的坐標,一旦上報的坐標位於劃好的多邊形內部的時候,就算命中了某多邊形,根據這個結果就可以分別做我們自己的業務邏輯了。
長連接這種鬼東西,直接用四層的TCP是不可能的。一來是咱自己能力有限,悟性較低,始終無法【精通】這門學問;二來是咱不會寫APP demo,只能靠瀏覽器臨時客串當客戶端。綜上所屬,最佳選擇就是Websocket協議。
貴在能用
下面的環節是我們最愛的CV環節!
第一步:搞好資料庫
事到如今,我們讓是得辛苦MongoDB出來臨時客串頂一下幫我們存儲地理多邊形。我們創建一個Mongodb 2dsphere索引,其次插入一個地理多邊形。我們的資料庫momo,數據表是geo:
// 選擇momo資料庫 use momo; // 在fence欄位上建立2dsphere索引 db.geo.ensureIndex( { fence: "2dsphere" } ); // 查看一下momo.geo中的索引 db.geo.getIndexes(); // 如果不出問題的話,下面表示索引已經OK > db.geo.getIndexes(); [{ "v": 2, "key": { "_id": 1 }, "name": "_id_", "ns": "momo.geo" }, { "v": 2, "key": { "fence": "2dsphere" }, "name": "fence_2dsphere", "ns": "momo.geo", "2dsphereIndexVersion": 3 }]
我們在地圖上選擇四個點來封閉一下老李和巨蛀暫住的著名小區,入下圖所示,用黃褐色線條框起來的封閉四邊形:

// 構造這個四邊形 // 但是⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ // 需要五個點才能封閉住一個多邊形,起點和終點的坐標完全一樣,表示在此處封閉這個多邊形 > db.geo.insert( { fence:{ type:"Polygon", coordinates:[[ [116.3129886235,40.0614323370], [116.3139649476,40.0599789099], [116.3166234822,40.0606813756], [116.3158241839,40.0618350753], [116.3129886235,40.0614323370], ]] } } ); // 返回下面這個表示數據插入成功,多邊形構造完畢 > WriteResult({ "nInserted" : 1 }) // 不放心就查詢一下吧 > db.geo.find({}) > { "_id" : ObjectId("5d0f45eb0e495c7ff4dcf7a9"), "fence" : { "type" : "Polygon", "coordinates" : [ [ [ 116.3129886235, 40.061432337 ], [ 116.3139649476, 40.0599789099 ], [ 116.3166234822, 40.0606813756 ], [ 116.3158241839, 40.0618350753 ], [ 116.3129886235, 40.061432337 ] ] ] } } // 我們再依照復讀機方式,再次畫一個更大的多邊形 db.geo.insert( { fence:{ type:"Polygon", coordinates:[[ [116.3087829198,40.0634605138], [116.3179899688,40.0663338312], [116.3216392131,40.0599957157], [116.3084395970,40.0574989797], [116.3087829198,40.0634605138], ]] } } );
我們挑選一個多邊形內部的經緯度:[ 116.3148017968,40.0609848161 ]來查詢一下,看看是否能夠命中多邊形。當然了,最為demo一定是命中了的,要不這玩意真的沒法往下編了… …demo里命中的是ID為【5d0f45eb0e495c7ff4dcf7a9】的多邊形。
// 查詢某點是否在圍欄內外 db.geo.find( { fence:{ $geoIntersects:{ $geometry:{ "type" : "Point", "coordinates" : [ 116.3148017968,40.0609848161 ] } } } } ); // 回車後執行命令 > { "_id" : ObjectId("5d0f45eb0e495c7ff4dcf7a9"), "fence" : { "type" : "Polygon", "coordinates" : [ [ [ 116.3129886235, 40.061432337 ], [ 116.3139649476, 40.0599789099 ], [ 116.3166234822, 40.0606813756 ], [ 116.3158241839, 40.0618350753 ], [ 116.3129886235, 40.061432337 ] ] ] } }
第二步:構建Websocket伺服器
那個。。。今天我們既不用上古時代的C語言,也不用從群眾中來的PHP,今天我們走進新時代:Golang。
別小瞧咱老李,咱活兒全
package main import ( "log" "fmt" "net/http" "github.com/gorilla/websocket" "encoding/json" "context" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) type coordsStruct struct { Lat float64 `json:lat` Lng float64 `json:lng` } type fenceStruct struct { Id primitive.ObjectID "_id,omitempty" } // 配置一些websocket的option項 var upgrader = websocket.Upgrader{ CheckOrigin: func( r *http.Request ) bool { return true }, } func main() { http.HandleFunc( "/", fence ) log.Fatal( http.ListenAndServe( ":8000", nil ) ) } // 地理圍欄服務 func fence( w http.ResponseWriter, r *http.Request ) { conn, err := upgrader.Upgrade( w, r, nil ) // 這個地方已經要校驗失敗,err如果不校驗,後面會出錯 if err != nil { fmt.Println( "ws upgrade err:", err ) return } defer conn.Close() // 進入到ws服務無限循環中... for { messageType, message, _ := conn.ReadMessage() // 反序列化json var coords coordsStruct err := json.Unmarshal( []byte( message ), &coords ) if err != nil { fmt.Println( "json decode err : ", err ) } // 開始處理經緯度是否在多邊形中 var fence fenceStruct clientOptions := options.Client().ApplyURI( "mongodb://127.0.0.1" ) client, err := mongo.Connect( context.TODO(), clientOptions ) if err != nil { fmt.Println( "mongo connect err..." ) } geoCollection := client.Database("momo").Collection("geo") ret := geoCollection.FindOne( context.TODO(), bson.M{"fence":bson.M{"$geoIntersects":bson.M{"$geometry":bson.M{"type":"Point","coordinates":[]float64{coords.Lng,coords.Lat}}}}} ) if err := ret.Decode( &fence ); err != nil { fmt.Println( "Decode err : ", err ) return } fmt.Println( "收到坐標:", string( message ) ) response, err := json.Marshal( fence ) if err != nil { fmt.Println( "json marshal err : ", err ) return } conn.WriteMessage( messageType, []byte( response ) ) } }
將上面文件保存為ws.go,然後執行go run ws.go將Websocket伺服器啟動起來。
第三步:構建JS客戶端
JS程式碼太多了,我只放了關鍵部位的,老規矩所有程式碼將會放到github里。
var ws = new WebSocket("ws://t.ti-node.com:8000/"); ws.onopen = function( evt ) { console.log("Connection open ..."); //ws.send("Hello WebSockets!"); }; ws.onmessage = function( evt ) { console.log( "Received Message: " + evt.data ); alert( evt.data ); //ws.close(); }; ws.onclose = function(evt) { console.log("Connection closed."); }; if ( navigator.geolocation ) { function locationSuccess( position ) { var coords = position.coords; //alert( coords.latitude+':'+coords.longitude ); var coo = { lat : coords.latitude, lng : coords.longitude }; //alert( JSON.stringify( coo ) ); ws.send( JSON.stringify( coo ) ); } function locationError( error ){ switch(error.code) { case error.TIMEOUT: console.log("A timeout occured! Please try again!"); break; case error.POSITION_UNAVAILABLE: console.log('We can't detect your location. Sorry!'); break; case error.PERMISSION_DENIED: console.log('Please allow geolocation access for this to work.'); break; case error.UNKNOWN_ERROR: console.log('An unknown error occured!'); break; } } var options = { // 指示瀏覽器獲取高精度的位置,默認為false enableHighAcuracy: true, // 指定獲取地理位置的超時時間,默認不限時,單位為毫秒 timeout: 5000, // 最長有效期,在重複獲取地理位置時,此參數指定多久再次獲取位置。 maximumAge: 3000 }; setInterval( function() { navigator.geolocation.getCurrentPosition( locationSuccess, locationError, options ); }, 1000 ); // watchPosition只要設備位置發生變化,就會執行 //var watcherId = navigator.geolocation.watchPosition( locationSuccess, locationError, options ); //clearwatch用於終止watchPosition方法 //navigator.geolocation.clearWatch( watcher_id ); } else { alert("Your browser does not support Geolocation!"); }
CV黃龍:https://github.com/elarity/wechat-official-accounts-demo-code
上面的HTML && JS程式碼保存好後,請在手機瀏覽器上訪問該HTML頁面地址,因為手機瀏覽器可以獲取手機GPS數據~~~順利訪問後,結果分別如下圖所示:
服務端

客戶端

客戶端上報的經緯度一旦命中了多邊形,就會返回該多邊形的ID…具體細節程式碼,你們自己豐富?