Recently, a business scenario used to find people nearby, so I looked up the relevant information, and the use of PHP to achieve the relevant functions of a variety of ways and specific implementation of a technical summary, welcome you to put forward comments and error correction, let’s start to the main topic:

LBS(Location-based Services)

There is a bigger term for locating people nearby called LBS(location-based service). LBS refers to a value-added service that obtains the location information of mobile terminal users through the radio communication network of telecom and mobile operators or external positioning methods, and provides corresponding services for users with the support of GIS platform. Therefore, the first step is to obtain the user’s location. The user’s location can be obtained by means of GPS, carrier base station, WIFI, etc. Generally, the client obtains the longitude and latitude coordinates of the user’s location and uploats them to the application server. The application server saves the user coordinates. The application server filters and sorts the database based on the geographical location of the requestor and certain conditions (distance, gender, active time, etc.).

How do we get the distance between two points according to latitude and longitude?

We all know that the coordinates of two points in plane coordinates can be calculated by using the formula of plane coordinate distance, but the longitude and latitude is the spherical coordinate system of space on the earth defined by using the sphere of three dimensions. Assuming that the earth is a positive sphere, the calculation formula of spherical distance is as follows:

This article is recommended for those interested in the specific inference process: calculate the distance between two points on the ground according to latitude and longitude – mathematical formula and derivation

PHP function code is as follows:

@param $lat1 * @param $lng1 * @param $lat2 * @param $lng2 * @return float */ public static function getDistance($lat1, $lng1, $lat2, $lng2){ $earthRadius = 6367000; //approximate radius of earth in meters $lat1 = ($lat1 * pi() ) / 180; $lng1 = ($lng1 * pi() ) / 180; $lat2 = ($lat2 * pi() ) / 180; $lng2 = ($lng2 * pi() ) / 180; $calcLongitude = $lng2 - $lng1; $calcLatitude = $lat2 - $lat1; $stepOne = pow(sin($calcLatitude / 2), 2) + cos($lat1) * cos($lat2) * pow(sin($calcLongitude / 2), 2); $stepTwo = 2 * asin(min(1, sqrt($stepOne))); $calculatedDistance = $earthRadius * $stepTwo; return round($calculatedDistance); }Copy the code

MySQL:

SELECT id, (3959 * ACOS (COS (radians(78.3232)) * COS (radians(Lat)) * COS (radians(LNG) - radians(65.3234)) + sin ( Radians (78.3232)) * sin(radians(lat)))) AS distance FROM markers HAVING distance < 30 ORDER BY distance LIMIT 0, 20;Copy the code

In addition to the above obtained by calculating the spherical distance formula, we can use some database services, such as Redis and MongoDB:

Redis 3.2 provides the GEO location function, which can not only obtain the distance between two locations, but also easily obtain the geographic information location set within the specified location range. Redis command documentation

1. Add location

GEOADD key longitude latitude member [longitude latitude member ...]
Copy the code

2. Get your location

GEOPOS key member [member ...]
Copy the code

3. Get the distance between two geographical locations

GEODIST key member1 member2 [unit]
Copy the code

4. Obtain the location set of the specified longitude and latitude

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
Copy the code

5. Obtain the location set of the specified member

GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
Copy the code

MongoDB builds geospatial indexes specifically for such queries. 2D and 2dsphere indexes are for plane and sphere respectively. Mongo documents

1. Add data

db.location.insert( {uin : 1 , loc : { lon : 50 , lat : 50 } } )
Copy the code

2. Create an index

db.location.ensureIndex( { loc : "2d" } )
Copy the code

3. Find nearby points

db.location.find( { loc :{ $near : [50, 50] } )
Copy the code

4. Maximum distance and number of restrictions

db.location.find( { loc : { $near : [50, 50] , $maxDistance : 5 } } ).limit(20)
Copy the code

5. Use geoNear to return the distance between each point and the query point in the query result

db.runCommand( { geoNear : "location" , near : [ 50 , 50 ], num : 10, query : { type : "museum" } } )
Copy the code

6. GeoNear is used with query conditions and number of returned items. GeoNear does not support the function of paging related limit and SKIP parameters in find queries using runCommand

db.runCommand( { geoNear : "location" , near : [ 50 , 50 ], num : 10, query : { uin : 1 } })
Copy the code

PHP in many ways and concrete implementation

1. Based on MySql

Member addition method:

public function geoAdd($uin, $lon, $lat) { $pdo = $this->getPdo(); $sql = 'INSERT INTO `markers`(`uin`, `lon`, `lat`) VALUES (? ,? ,?) '; $stmt = $pdo->prepare($sql); return $stmt->execute(array($uin, $lon, $lat)); }Copy the code

Query nearby people (support query criteria and paging) :

public function geoNearFind($lon, $lat, $maxDistance = 0, $where = array(), $page = 0)
{
    $pdo = $this->getPdo();
    $sql = "SELECT  
              id, (  
                3959 * acos (  
                  cos ( radians(:lat) )  
                  * cos( radians( lat ) )  
                  * cos( radians( lon ) - radians(:lon) )  
                  + sin ( radians(:lat) )  
                  * sin( radians( lat ) )  
                )  
              ) AS distance  
            FROM markers";

    $input[':lat'] = $lat;
    $input[':lon'] = $lon;

    if ($where) {
        $sqlWhere = ' WHERE ';
        foreach ($where as $key => $value) {
            $sqlWhere .= "`{$key}` = :{$key} ,";
            $input[":{$key}"] = $value;
        }
        $sql .= rtrim($sqlWhere, ',');
    }

    if ($maxDistance) {
        $sqlHaving = " HAVING distance < :maxDistance";
        $sql .= $sqlHaving;
        $input[':maxDistance'] = $maxDistance;
    }

    $sql .= ' ORDER BY distance';

    if ($page) {
        $page > 1 ? $offset = ($page - 1) * $this->pageCount : $offset = 0;
        $sqlLimit = " LIMIT {$offset} , {$this->pageCount}";
        $sql .= $sqlLimit;
    }

    $stmt = $pdo->prepare($sql);
    $stmt->execute($input);
    $list = $stmt->fetchAll(PDO::FETCH_ASSOC);

    return $list;
}
Copy the code

2. Based on Redis(3.2 +)

PHP using Redis can install the Redis extension or install the Predis class library in Composer. This article uses the Redis extension.

Member addition method:

public function geoAdd($uin, $lon, $lat)
{
    $redis = $this->getRedis();
    $redis->geoAdd('markers', $lon, $lat, $uin);
    return true;
}
Copy the code

Query nearby people (query conditions and pagination are not supported) :

public function geoNearFind($uin, $maxDistance = 0, $unit = 'km') { $redis = $this->getRedis(); $options = ['WITHDIST']; $list = $redis->geoRadiusByMember('markers', $uin, $unit, $options); return $list; }Copy the code

3. Based on the mongo

PHP uses MongoDB extension mongo(document) and MongoDB (document), the two writing method is very different, the selection of good extension needs to see the corresponding document, because MongoDB extension is a new version, this paper chooses MongoDB extension.

Suppose we create a DB library and a location collection

Set index:

db.getCollection('location').ensureIndex({"uin":1},{"unique":true}) db.getCollection('location').ensureIndex({loc:"2d"}) #db.getCollection('location').ensureIndex({loc:"2d",uin:1})Copy the code

Member addition method:

public function geoAdd($uin, $lon, $lat) { $document = array( 'uin' => $uin, 'loc' => array( 'lon' => $lon, 'lat' => $lat, ), ); $bulk = new MongoDB\Driver\BulkWrite; $bulk->update( ['uin' => $uin], $document, [ 'upsert' => true] ); $manager = $this->getMongoManager(); $writeConcern = new MongoDB\Driver\WriteConcern(1, 100); //$writeConcern = new MongoDB\Driver\WriteConcern(MongoDB\Driver\WriteConcern::MAJORITY, 100); $result = $manager->executeBulkWrite('db.location', $bulk, $writeConcern); if ($result->getWriteErrors()) { return false; } return true; }Copy the code

Query nearby people (return results without distance, support query conditions, support paging)

public function geoNearFind($lon, $lat, $maxDistance = 0, $where = array(), $page = 0)
{
    $filter = array(
        'loc' => array(
            '$near' => array($lon, $lat),
        ),
    );
    if ($maxDistance) {
        $filter['loc']['$maxDistance'] = $maxDistance;
    }
    if ($where) {
        $filter = array_merge($filter, $where);
    }
    $options = array();
    if ($page) {
        $page > 1 ? $skip = ($page - 1) * $this->pageCount : $skip = 0;
        $options = [
            'limit' => $this->pageCount,
            'skip' => $skip
        ];
    }

    $query = new MongoDB\Driver\Query($filter, $options);
    $manager = $this->getMongoManager();
    $cursor = $manager->executeQuery('db.location', $query);
    $list = $cursor->toArray();
    return $list;
}
Copy the code

Query nearby person (return result with distance, support query condition, pay return quantity, do not support paging) :

public function geoNearFindReturnDistance($lon, $lat, $maxDistance = 0, $where = array(), $num = 0) { $params = array( 'geoNear' => "location", 'near' => array($lon, $lat), 'spherical' => true, // Spherical is set to false (default), the unit of DIS is consistent with the unit of coordinate (s), and the unit of DIS is arc 'distanceMultiplier' => 6371, // computed as km, Coordinates distanceMultiplier: 111. DistanceMultiplier: 6371); if ($maxDistance) { $params['maxDistance'] = $maxDistance; } if ($num) { $params['num'] = $num; } if ($where) { $params['query'] = $where; } $command = new MongoDB\Driver\Command($params); $manager = $this->getMongoManager(); $cursor = $manager->executeCommand('db', $command); $response = (array) $cursor->toArray()[0]; $list = $response['results']; return $list; }Copy the code

Matters needing attention:

1. Choose an extension. Mongo and mongodb extensions are written differently

2. Noreply occurs when data is written. Check the write confirmation level

3. Distance must be calculated for data queried using find. Pagination is not supported for data queried using geoNear

4. The distance queried using geoNear needs to be converted to KM using the Spherical and distanceMultiplier parameters

The above demo can be stamped here: demo

conclusion

The above three methods are introduced to realize the function of querying nearby people, and each method has its own applicable scenarios. For example, the data line is relatively small. For example, Mysql is enough to query the distance between the user and several cities. However, if there is a large amount of data and a variety of attribute screening conditions, it will be more convenient to use Mongo. The above is only a suggestion, and the specific implementation scheme should be reviewed according to the specific business.