SQlite รับตำแหน่งที่ใกล้ที่สุด (ด้วยละติจูดและลองจิจูด)


87

ฉันมีข้อมูลที่มีละติจูดและลองจิจูดที่เก็บไว้ในฐานข้อมูล SQLite ของฉันและฉันต้องการรับตำแหน่งที่ใกล้ที่สุดกับพารามิเตอร์ที่ฉันใส่ไว้ (เช่นตำแหน่งปัจจุบันของฉัน - lat / lng ฯลฯ )

ฉันรู้ว่าสิ่งนี้เป็นไปได้ใน MySQL และฉันได้ทำการวิจัยมาพอสมควรแล้วว่า SQLite ต้องการฟังก์ชันภายนอกที่กำหนดเองสำหรับสูตร Haversine (การคำนวณระยะทางบนทรงกลม) แต่ฉันไม่พบสิ่งใดที่เขียนใน Java และใช้งานได้ .

นอกจากนี้ถ้าฉันต้องการเพิ่มฟังก์ชันที่กำหนดเองฉันต้องใช้org.sqlite. jar (สำหรับorg.sqlite.Function) และนั่นเป็นการเพิ่มขนาดที่ไม่จำเป็นให้กับแอป

อีกด้านหนึ่งคือฉันต้องการฟังก์ชั่น Order by จาก SQL เนื่องจากการแสดงระยะทางเพียงอย่างเดียวนั้นไม่ได้เป็นปัญหามากนัก - ฉันทำมันใน SimpleCursorAdapter ที่กำหนดเองแล้ว แต่ฉันไม่สามารถเรียงลำดับข้อมูลได้เพราะฉัน ไม่มีคอลัมน์ระยะทางในฐานข้อมูลของฉัน นั่นหมายถึงการอัปเดตฐานข้อมูลทุกครั้งที่มีการเปลี่ยนแปลงตำแหน่งและเป็นการสิ้นเปลืองแบตเตอรี่และประสิทธิภาพ ดังนั้นหากใครมีความคิดในการจัดเรียงเคอร์เซอร์ด้วยคอลัมน์ที่ไม่ได้อยู่ในฐานข้อมูลฉันก็รู้สึกขอบคุณเช่นกัน!

ฉันรู้ว่ามีแอพ Android มากมายที่ใช้ฟังก์ชั่นนี้ แต่ใครช่วยอธิบายความมหัศจรรย์ให้หน่อยได้ไหม

อย่างไรก็ตามฉันพบทางเลือกนี้: ค้นหาเพื่อรับบันทึกตาม Radius ใน SQLite?

แนะนำให้สร้าง 4 คอลัมน์ใหม่สำหรับค่า cos และ sin ของ lat และ lng แต่มีวิธีอื่นที่ไม่ซ้ำซ้อนหรือไม่?


คุณตรวจสอบว่า org.sqlite.Function เหมาะกับคุณหรือไม่ (แม้ว่าสูตรจะไม่ถูกต้อง)
Thomas Mueller

ไม่ฉันพบทางเลือก (ซ้ำซ้อน) (โพสต์ที่แก้ไข) ซึ่งฟังดูดีกว่าการเพิ่ม. jar ขนาด 2,6MB ในแอป แต่ฉันยังคงค้นหาวิธีแก้ปัญหาที่ดีกว่า ขอบคุณ!
Jure

ประเภทหน่วยระยะทางกลับคืออะไร?

นี่คือการใช้งานเต็มรูปแบบสำหรับการสร้างแบบสอบถาม SQlite บน Android ตามระยะทางระหว่างตำแหน่งของคุณและตำแหน่งของวัตถุ
EricLarch

คำตอบ:


113

1)ในตอนแรกกรองข้อมูล SQLite ของคุณด้วยการประมาณที่ดีและลดจำนวนข้อมูลที่คุณต้องประเมินในโค้ดจาวาของคุณ ใช้ขั้นตอนต่อไปนี้เพื่อจุดประสงค์นี้:

เพื่อให้มีเกณฑ์กำหนดและตัวกรองข้อมูลที่แม่นยำยิ่งขึ้นจะเป็นการดีกว่าที่จะคำนวณตำแหน่ง 4 แห่งที่อยู่ในหน่วยradiusเมตรของทิศเหนือทิศตะวันตกทิศตะวันออกและทิศใต้ของจุดศูนย์กลางของคุณในรหัสจาวาของคุณจากนั้นตรวจสอบได้อย่างง่ายดายโดยน้อยกว่าและมากกว่า ตัวดำเนินการ SQL (>, <)เพื่อตรวจสอบว่าจุดของคุณในฐานข้อมูลอยู่ในรูปสี่เหลี่ยมนั้นหรือไม่

วิธีนี้จะcalculateDerivedPosition(...)คำนวณคะแนนเหล่านั้นให้คุณ (p1, p2, p3, p4 ในภาพ)

ป้อนคำอธิบายภาพที่นี่

/**
* Calculates the end-point from a given source at a given range (meters)
* and bearing (degrees). This methods uses simple geometry equations to
* calculate the end-point.
* 
* @param point
*           Point of origin
* @param range
*           Range in meters
* @param bearing
*           Bearing in degrees
* @return End-point from the source given the desired range and bearing.
*/
public static PointF calculateDerivedPosition(PointF point,
            double range, double bearing)
    {
        double EarthRadius = 6371000; // m

        double latA = Math.toRadians(point.x);
        double lonA = Math.toRadians(point.y);
        double angularDistance = range / EarthRadius;
        double trueCourse = Math.toRadians(bearing);

        double lat = Math.asin(
                Math.sin(latA) * Math.cos(angularDistance) +
                        Math.cos(latA) * Math.sin(angularDistance)
                        * Math.cos(trueCourse));

        double dlon = Math.atan2(
                Math.sin(trueCourse) * Math.sin(angularDistance)
                        * Math.cos(latA),
                Math.cos(angularDistance) - Math.sin(latA) * Math.sin(lat));

        double lon = ((lonA + dlon + Math.PI) % (Math.PI * 2)) - Math.PI;

        lat = Math.toDegrees(lat);
        lon = Math.toDegrees(lon);

        PointF newPoint = new PointF((float) lat, (float) lon);

        return newPoint;

    }

และตอนนี้สร้างแบบสอบถามของคุณ:

PointF center = new PointF(x, y);
final double mult = 1; // mult = 1.1; is more reliable
PointF p1 = calculateDerivedPosition(center, mult * radius, 0);
PointF p2 = calculateDerivedPosition(center, mult * radius, 90);
PointF p3 = calculateDerivedPosition(center, mult * radius, 180);
PointF p4 = calculateDerivedPosition(center, mult * radius, 270);

strWhere =  " WHERE "
        + COL_X + " > " + String.valueOf(p3.x) + " AND "
        + COL_X + " < " + String.valueOf(p1.x) + " AND "
        + COL_Y + " < " + String.valueOf(p2.y) + " AND "
        + COL_Y + " > " + String.valueOf(p4.y);

COL_Xคือชื่อของคอลัมน์ในฐานข้อมูลที่เก็บค่าละติจูดและใช้COL_Yสำหรับลองจิจูด

ดังนั้นคุณจึงมีข้อมูลบางส่วนที่อยู่ใกล้จุดศูนย์กลางของคุณพร้อมค่าประมาณที่ดี

2)ตอนนี้คุณสามารถวนซ้ำกับข้อมูลที่กรองแล้วเหล่านี้และตรวจสอบว่าข้อมูลเหล่านี้อยู่ใกล้จุดของคุณจริงๆ (ในวงกลม) หรือไม่โดยใช้วิธีการต่อไปนี้:

public static boolean pointIsInCircle(PointF pointForCheck, PointF center,
            double radius) {
        if (getDistanceBetweenTwoPoints(pointForCheck, center) <= radius)
            return true;
        else
            return false;
    }

public static double getDistanceBetweenTwoPoints(PointF p1, PointF p2) {
        double R = 6371000; // m
        double dLat = Math.toRadians(p2.x - p1.x);
        double dLon = Math.toRadians(p2.y - p1.y);
        double lat1 = Math.toRadians(p1.x);
        double lat2 = Math.toRadians(p2.x);

        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2)
                * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        double d = R * c;

        return d;
    }

สนุก!

ฉันใช้และปรับแต่งการอ้างอิงนี้และทำให้เสร็จสมบูรณ์


ตรวจสอบหน้าเว็บที่ยอดเยี่ยมของ Chris Veness หากคุณกำลังมองหาการใช้ Javascript ตามแนวคิดข้างต้นนี้ movable-type.co.uk/scripts/latlong.html
barneymc

@Menma x คือละติจูดและ y คือลองจิจูด รัศมี: รัศมีของวงกลมที่แสดงในภาพ
บ็อบ

วิธีแก้ปัญหาที่ให้ไว้ข้างต้นถูกต้องและใช้งานได้ ลองดู ... :)
YS

1
นี่คือวิธีแก้ปัญหาโดยประมาณ! ให้ผลลัพธ์โดยประมาณสูงผ่านการสืบค้น SQL ที่รวดเร็วและเป็นมิตรกับดัชนี มันจะให้ผลลัพธ์ที่ไม่ถูกต้องในสถานการณ์ที่รุนแรงบางอย่าง เมื่อคุณมีอากาศขนาดเล็กจำนวนผลประมาณในพื้นที่หลายกิโลเมตรใช้ช้ากว่าวิธีที่แม่นยำยิ่งขึ้นในการกรองผลลัพธ์เหล่านั้น อย่าใช้เพื่อกรองด้วยรัศมีที่ใหญ่มากหรือหากแอปของคุณจะถูกใช้บ่อยที่เส้นศูนย์สูตร!
user1643723

1
แต่ฉันไม่เข้าใจ CalculateDerivedPosition กำลังเปลี่ยนพิกัด lat, lng เป็นคาร์ทีเซียนจากนั้นคุณในแบบสอบถาม SQL คุณกำลังเปรียบเทียบค่าคาร์ทีเซียนเหล่านี้กับค่า lat และค่า long พิกัดทางเรขาคณิตต่างกันสองอัน? วิธีนี้ทำงานอย่างไร? ขอบคุณ.
Misgevolution

70

คำตอบของ Chris มีประโยชน์มาก (ขอบคุณ!) แต่จะใช้ได้ผลก็ต่อเมื่อคุณใช้พิกัดเชิงเส้นตรง (เช่นการอ้างอิงตาราง UTM หรือ OS) หากใช้องศาสำหรับ lat / lng (เช่น WGS84) ข้างต้นจะทำงานที่เส้นศูนย์สูตรเท่านั้น ที่ละติจูดอื่นคุณต้องลดผลกระทบของลองจิจูดที่มีต่อลำดับการจัดเรียง (ลองนึกภาพว่าคุณอยู่ใกล้ขั้วโลกเหนือ ... ระดับของละติจูดยังคงเท่าเดิม แต่ระดับของลองจิจูดอาจสูงเพียงไม่กี่ฟุตซึ่งหมายความว่าลำดับการจัดเรียงไม่ถูกต้อง)

หากคุณไม่ได้อยู่ที่เส้นศูนย์สูตรให้คำนวณฟัดจ์แฟคเตอร์ล่วงหน้าตามละติจูดปัจจุบันของคุณ:

<fudge> = Math.pow(Math.cos(Math.toRadians(<lat>)),2);

จากนั้นสั่งซื้อโดย:

((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)

ยังคงเป็นเพียงการประมาณ แต่ดีกว่าแบบแรกมากดังนั้นความไม่ถูกต้องในการเรียงลำดับจะหายากกว่ามาก


3
นั่นเป็นจุดที่น่าสนใจจริงๆเกี่ยวกับเส้นยาวที่มาบรรจบกันที่เสาและทำให้ผลลัพธ์ที่อยู่ใกล้คุณได้รับมากขึ้น การแก้ไขที่ดี
Chris Simpson

1
ดูเหมือนว่าจะใช้ cursor = db.getReadableDatabase (). rawQuery ("เลือก nome, id เป็น _id," + "(" + latitude + "- lat) * (" + latitude + "- lat) + (" + longitude + "- lon) * (" + ลองจิจูด + "- lon) *" + fudge + "as distanza" + "จากไคลเอนต์" + "สั่งโดย distanza asc", null);
max4ever

ควร((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) + (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN) * <fudge>)น้อยกว่าdistanceหรือdistance^2?
บ็อบ

ปัจจัยเหลวไหลเป็นเรเดียนและคอลัมน์เป็นองศาไม่ใช่หรือ ไม่ควรแปลงเป็นหน่วยเดียวกัน?
Rangel Reale

1
ไม่ปัจจัยเหลวไหลคือปัจจัยมาตราส่วนซึ่งมีค่าเป็น 0 ที่ขั้วและ 1 ที่เส้นศูนย์สูตร มันไม่ได้เป็นองศาหรือเรเดียนมันเป็นเพียงจำนวนหน่วย ฟังก์ชัน Java Math.cos ต้องการอาร์กิวเมนต์เป็นเรเดียนและฉันถือว่า <lat> เป็นองศาดังนั้นฟังก์ชัน Math.toRadians แต่โคไซน์ที่ได้นั้นไม่มีหน่วย
Teasel

68

ฉันรู้ว่าสิ่งนี้ได้รับคำตอบและยอมรับ แต่คิดว่าฉันจะเพิ่มประสบการณ์และวิธีแก้ปัญหาของฉัน

ในขณะที่ฉันยินดีที่จะใช้ฟังก์ชัน haversine บนอุปกรณ์เพื่อคำนวณระยะทางที่ถูกต้องระหว่างตำแหน่งปัจจุบันของผู้ใช้และตำแหน่งเป้าหมายใด ๆ ที่เฉพาะเจาะจง แต่ก็จำเป็นต้องจัดเรียงและ จำกัด ผลการสืบค้นตามลำดับระยะทาง

วิธีแก้ปัญหาที่น่าพอใจน้อยกว่าคือการส่งคืนล็อตและเรียงลำดับและกรองตามความเป็นจริง แต่จะส่งผลให้เคอร์เซอร์ตัวที่สองและผลลัพธ์ที่ไม่จำเป็นจำนวนมากจะถูกส่งคืนและทิ้งไป

วิธีแก้ปัญหาที่ฉันต้องการคือส่งตามลำดับค่าเดลต้ากำลังสองของ long และ lats:

((<lat> - LAT_COLUMN) * (<lat> - LAT_COLUMN) +
 (<lng> - LNG_COLUMN) * (<lng> - LNG_COLUMN))

ไม่จำเป็นต้องทำ haversine แบบเต็มเพียงเพื่อเรียงลำดับและไม่จำเป็นต้องสแควร์รูทผลลัพธ์ดังนั้น SQLite จึงสามารถจัดการการคำนวณได้

แก้ไข:

คำตอบนี้ยังคงได้รับความรัก ในกรณีส่วนใหญ่ใช้งานได้ดี แต่ถ้าคุณต้องการความแม่นยำอีกเล็กน้อยโปรดดูคำตอบของ @Teasel ด้านล่างซึ่งเพิ่มปัจจัย "เหลวไหล" ที่แก้ไขความไม่ถูกต้องที่เพิ่มขึ้นเมื่อละติจูดเข้าใกล้ 90


คำตอบที่ดี คุณช่วยอธิบายได้ไหมว่ามันทำงานอย่างไรและอัลกอริทึมนี้เรียกว่าอะไร
iMatoria

3
@iMatoria - นี่เป็นเพียงทฤษฎีบทที่มีชื่อเสียงของพีทาโกรัสที่ถูกตัดลง ด้วยพิกัดสองชุดความแตกต่างระหว่างค่า X ทั้งสองแสดงถึงด้านหนึ่งของสามเหลี่ยมมุมฉากและความแตกต่างระหว่างค่า Y คืออีกด้านหนึ่ง ในการหาด้านตรงข้ามมุมฉาก (ดังนั้นระยะห่างระหว่างจุด) คุณต้องเพิ่มกำลังสองของค่าทั้งสองนี้เข้าด้วยกันจากนั้นจึงกำหนดรากที่สองผลลัพธ์ ในกรณีของเราเราไม่ทำบิตสุดท้าย (การรูทที่สอง) เพราะเราทำไม่ได้ โชคดีที่นี่ไม่จำเป็นสำหรับการเรียงลำดับ
Chris Simpson

6
ในแอป BostonBusMap ของฉันฉันใช้วิธีนี้เพื่อแสดงจุดจอดที่ใกล้กับตำแหน่งปัจจุบันมากที่สุด อย่างไรก็ตามคุณต้องปรับขนาดระยะทางลองจิจูดโดยcos(latitude)ให้ละติจูดและลองจิจูดเท่ากันโดยประมาณ ดูen.wikipedia.org/wiki/…
noisecapella

0

เพื่อเพิ่มประสิทธิภาพให้มากที่สุดฉันขอแนะนำให้ปรับปรุงแนวคิดของ @Chris Simpson ด้วยORDER BYประโยคต่อไปนี้:

ORDER BY (<L> - <A> * LAT_COL - <B> * LON_COL + LAT_LON_SQ_SUM)

ในกรณีนี้คุณควรส่งผ่านค่าต่อไปนี้จากรหัส:

<L> = center_lat^2 + center_lon^2
<A> = 2 * center_lat
<B> = 2 * center_lon

และคุณควรจัดเก็บLAT_LON_SQ_SUM = LAT_COL^2 + LON_COL^2เป็นคอลัมน์เพิ่มเติมในฐานข้อมูล เติมข้อมูลเพื่อแทรกเอนทิตีของคุณลงในฐานข้อมูล สิ่งนี้ช่วยเพิ่มประสิทธิภาพเล็กน้อยในขณะที่ดึงข้อมูลจำนวนมาก


-3

ลองทำสิ่งนี้:

    //locations to calculate difference with 
    Location me   = new Location(""); 
    Location dest = new Location(""); 

    //set lat and long of comparison obj 
    me.setLatitude(_mLat); 
    me.setLongitude(_mLong); 

    //init to circumference of the Earth 
    float smallest = 40008000.0f; //m 

    //var to hold id of db element we want 
    Integer id = 0; 

    //step through results 
    while(_myCursor.moveToNext()){ 

        //set lat and long of destination obj 
        dest.setLatitude(_myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LATITUDE))); 
        dest.setLongitude(_myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LONGITUDE))); 

        //grab distance between me and the destination 
        float dist = me.distanceTo(dest); 

        //if this is the smallest dist so far 
        if(dist < smallest){ 
            //store it 
            smallest = dist; 

            //grab it's id 
            id = _myCursor.getInt(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_ID)); 
        } 
    } 

หลังจากนี้ id จะมีรายการที่คุณต้องการจากฐานข้อมูลเพื่อให้คุณสามารถดึงข้อมูลได้:

    //now we have traversed all the data, fetch the id of the closest event to us 
    _myCursor = _myDBHelper.fetchID(id); 
    _myCursor.moveToFirst(); 

    //get lat and long of nearest location to user, used to push out to map view 
    _mLatNearest  = _myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LATITUDE)); 
    _mLongNearest = _myCursor.getFloat(_myCursor.getColumnIndexOrThrow(DataBaseHelper._FIELD_LONGITUDE)); 

หวังว่าจะช่วยได้!

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.