持續搞【附近】—長連接坐標流和「地理圍欄」(五)

  • 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…具體細節程式碼,你們自己豐富?