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.