持续搞【附近】—长连接坐标流和“地理围栏”(五)

  • 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…具体细节代码,你们自己丰富?