在 LBS 開發中,可能經常要碰到這樣的問題,如何判斷一個指定的經緯度點是否落在一個多邊形區域內?比如在地圖上畫了一個多邊形區域,然後給出一個經緯度點,怎樣判斷這個點是否在這個多邊形範圍之內?
最近接到個考勤打卡場景需求:
- 使用者在差旅狀態下禁止打卡
- 使用者進入考勤範圍才允許打卡
第一點好解決:在使用者差旅狀態下禁止打卡互動就可以了,而第二點可能就有點複雜了: 如何來判斷使用者進入考勤範圍內呢?擴充下類似的需求還有外賣點餐判斷是否在商家配送範圍?判斷共享單車是否停靠在停車點?
這些需求拆分到最後都是 在判斷一個座標點是否在一個無規則的多邊形內的問題。
理論支援
需求: 判斷某點座標是否在多邊形內
方法: 求解通過該點的水平射線與多邊形各邊的交點個數
結果: 水平射線與多邊形交點為奇數,則在多邊形內部;交點為偶數,則在多邊形外部
接下來就是上程式碼。我們首先要做的就是與後端商定 app 與伺服器資料傳輸的規則:伺服器傳回包含五邊形點座標字串,這五個點按順序聯結框定出一個不規則的五邊形區域, 這個五邊形區域就是我們的打卡考勤有效範圍。
伺服器傳回的多邊形各點座標:
@"POLYGON((116.2310052844 39.9980477478,116.5143798001 40.0028565483,116.2460357549 39.8348654814,116.3976525318 39.7646827931,116.5157236632 39.8221811347))";
複製程式碼
我們先把這個字串處理成五個包含經度和緯度的 Coordinate 物件。Coordinate 物件的結構為:
@interface Coordinate:NSObject
@property (nonatomic, assign) double lon;
@property (nonatomic, assign) double lat;
@end
@implementation Coordinate
@end
複製程式碼
需要注意的是,我們在處理字串的時候,將火星座標轉化為百度座標。(服務傳回的座標為火星座標,專案中定位模組定到位後直接將經緯度轉化為了百度座標,這裡是為了保持與伺服器座標系的一致進行轉化,各位小夥伴需要根據自己專案實際情況進行座標轉換)
各地圖API座標系統科普與轉換
- WGS84座標系:即地球座標系,國際上通用的座標系。裝置一般包含GPS晶片或者北斗晶片獲取的經緯度為WGS84地理座標系,
- 谷歌地圖採用的是WGS84地理座標系(中國範圍除外);
- GCJ02座標系:即火星座標系,是由中國國家測繪局制訂的地理資訊系統的座標系統。由WGS84座標系經加密後的座標系。
- 谷歌中國地圖和搜搜中國地圖採用的是GCJ02地理座標系; BD09座標系:即百度座標系,GCJ02座標系經加密後的座標系;
- 搜狗座標系、圖吧座標系等,估計也是在GCJ02基礎上加密而成的。
處理伺服器返回的資料,並將火星座標轉化為百度座標:
//處理伺服器返回的資料
- (void)dealWithDotCoordinateWithString:(NSString *)locString{
//locString = @"POLYGON((116.2310052844 39.9980477478,116.5143798001 40.0028565483,116.2460357549 39.8348654814,116.3976525318 39.7646827931,116.5157236632 39.8221811347))";
locString = [locString stringByReplacingOccurrencesOfString:@"POLYGON((" withString:@""];
locString = [locString stringByReplacingOccurrencesOfString:@"))" withString:@""];
NSArray *locArray = [locString componentsSeparatedByString:@","];
NSMutableArray *locResult = [NSMutableArray new];
NSInteger index = 0;
for (NSString * str in locArray) {
NSArray *strArray = [str componentsSeparatedByString:@" "];
if (strArray.count > 1) {
Coordinate *lonAndLat = [Coordinate new] ;
NSString *lon = [strArray objectAtIndex:0];
lonAndLat.lon = [lon doubleValue];
NSString *lat = [strArray objectAtIndex:1];
lonAndLat.lat = [lat doubleValue];
//將伺服器的火星座標轉換為百度座標
Coordinate *baiduLoc = [self lonAndLatLocationBaiduFromMars:lonAndLat];
[locResult addObject:baiduLoc];
index ++;
}
}
}
複製程式碼
火星座標轉換為百度座標的方法:
//將火星座標轉換為百度座標的方法
- (Coordinate *)lonAndLatLocationBaiduToMars:(Coordinate *)coordinate{
double x_pi = M_PI * 3000.0 / 180.0;
double x = coordinate.lon, y = coordinate.lat;
double z = sqrt(x * x + y * y) + 0.00002 * sin(y * x_pi);
double theta = atan2(y, x) + 0.000003 * cos(x * x_pi);
coordinate.lon = z * cos(theta) + 0.0065;
coordinate.lat = z * sin(theta) + 0.006;
return coordinate;
}
複製程式碼
百度座標轉化為火星座標方法:
//百度座標轉化為火星座標
- (Coordinate *) lonAndLatLocationMarsToMars:(Coordinate *)coordinate{
double x_pi = M_PI * 3000.0 / 180.0;
double x = coordinate.lon - 0.0065, y = coordinate.lat - 0.006;
double z = sqrt(x * x + y * y) - 0.00002 * sin(y * x_pi);
double theta = atan2(y, x) - 0.000003 * cos(x * x_pi);
coordinate.lon = z * cos(theta);
coordinate.lat = z * sin(theta);
return coordinate;
}
複製程式碼
接下來就是重點,怎麼判斷座標在多邊形內部方法:
//判斷點是否在多邊形內部
- (BOOL)judgeLocationX:(double)locationX locationY:(double)locationY insideSignArea:(NSArray *)areaArray{
if(areaArray.count==0){
NSLog(@"考勤區域為空 直接返回 true");
return true;
}
NSMutableArray *xArray = [NSMutableArray new];
NSMutableArray *yArray = [NSMutableArray new];
for (Coordinate *coordinate in areaArray) {
[xArray addObject: [NSNumber numberWithDouble:coordinate.lon]];
[yArray addObject:[NSNumber numberWithDouble:coordinate.lat]];
}
BOOL flag = NO;
//取橫座標和縱座標的最大值和最小值,根據這四個值minX,maxX,minY,maxY,算出一個四邊形,判斷目標點是否在這個四邊形內,不滿足,直接返回false,證明該目標點不在此多邊形內部。
double minX = [[xArray valueForKeyPath:@"@min.doubleValue"] doubleValue];
double maxX = [[xArray valueForKeyPath:@"@max.doubleValue"] doubleValue];
double minY = [[yArray valueForKeyPath:@"@min.doubleValue"] doubleValue];
double maxY = [[yArray valueForKeyPath:@"@max.doubleValue"] doubleValue];
if (longitude < minX || longitude > maxX || latitude < minY || latitude > maxY ) {
return false;
}
//座標點畫條水平線射線計算與多邊形的交點個數,奇數在多邊形內, 偶數在多邊形外。
int count = (int) areaArray.count ;
for (int i = 0, j = count-1; i < count; j = i++) {
if ( ( ([yArray[i] doubleValue] > locationY) != ([yArray[j] doubleValue] > locationY)) &&
(locationX < ([xArray[j] doubleValue] - [xArray[i] doubleValue]) * (locationY-[yArray[i] doubleValue]) / ([yArray[j] doubleValue]-[yArray[i] doubleValue]) + [xArray[i] doubleValue]) )
flag = !flag;
}
NSLog(@"座標點是否在不規則區域內: %d",success);
return flag;
}
複製程式碼
方法內部對座標點進行判斷,判斷該點緯度是否在多邊形相鄰兩點緯度之間,如果在兩緯度之間則接著判斷該點單方向的水平射線與這兩相鄰點連結邊是否有交點。如果有交點則開始計數。接著遍歷判斷與多邊形其它邊是否有交點,這樣就可以得到該水平射線與多邊形邊交點的總個數,交點總數為奇數則該點在多邊形內部;交點總數為偶數則該點在多邊形外部。上面方法中並沒有統計交點個數而是直接使用 flag
記錄總數的奇偶性。
最後進行一些簡單的資料測試:
BOOL flag1 = [self judgeLocationX:116.3839694879 locationY:39.9274612554 insideSignArea:locResult]; //應返回 true
BOOL flag2 = [self judgeLocationX:116.4010873480 locationY:39.8485685476 insideSignArea:locResult]; //應返回 true
BOOL flag3 = [self judgeLocationX:116.5473037259 locationY:40.1688347176 insideSignArea:locResult]; //應返回 false
BOOL flag4 = [self judgeLocationX:116.1909733537 locationY:40.0254447029 insideSignArea:locResult]; //應返回 false
複製程式碼
總結
回想下我們剛才都做了些什麼:
- 處理座標字串
- 火星座標轉化為百度座標
- 判斷點座標是否在多邊形內部
- 簡單驗證