Dalam artikel ini, saya memutuskan untuk menjelaskan bagaimana fungsionalitas memilih dan menampilkan foto pada tempat tertentu di peta diimplementasikan dalam layanan foto kami gfranq.com . Layanan foto tidak berfungsi sekarang.
Karena kami memiliki banyak foto di layanan kami dan mengirimkan permintaan ke database setiap kali perubahan viewport terlalu intensif sumber daya, logis untuk membagi peta menjadi beberapa area yang berisi informasi tentang data yang diambil. Untuk alasan yang jelas, area ini memiliki bentuk persegi panjang (meskipun kotak heksagonal dianggap juga). Ketika area menjadi lebih bulat pada skala besar, elemen geometri bola dan alat untuknya juga dipertimbangkan.
Dalam artikel ini, masalah berikut ini muncul:
- Menyimpan dan mengambil foto dari database dan menyimpannya di server (SQL, C #, ASP.NET).
- Mengunduh foto-foto yang diperlukan di sisi klien dan menyimpannya ke cache klien (JavaScript).
- Perhitungan ulang foto yang harus disembunyikan atau ditampilkan ketika viewport berubah.
- Elemen geometri bola.
Isi
Bagian server
Metode berikut untuk memilih dan menyimpan informasi geoinformasi di dalam basis data dirancang:
- Tipe data geografi bawaan SQL Server.
- Seleksi normal dengan batasan.
- Menggunakan tabel tambahan.
Selanjutnya, metode-metode ini akan dijelaskan secara rinci.
Geotipe bawaan
Seperti diketahui, SQL Server 2008 mendukung tipe data geografi dan geometri, yang memungkinkan untuk menentukan informasi geografis (di bola) dan geometri (di pesawat), seperti titik, garis, poligon, dll. . Untuk mengambil semua foto yang dilingkupi oleh persegi panjang dengan koordinat ( lngMin
latMin
) dan ( latMax
lngMax
), Anda dapat menggunakan kueri berikut:
DECLARE @h geography; DECLARE @p geography; SET @rect = geography::STGeomFromText('POLYGON((lngMin latMin, lngMax latMin, lngMax latMax, lngMin latMax, lngMin latMin))', 4326); SELECT TOP @cound id, image75Path, geoTag.Lat as Lat, geoTag.Long as Lng, popularity, width, height FROM Photo WITH (INDEX(IX_Photo_geoTag)) WHERE @rect.STContains(geoTag) = 1 ORDER BY popularity DESC
Perhatikan bahwa poligon berorientasi berlawanan arah jarum jam dan indeks spasial IX_Photo_geoTag
ditentukan oleh koordinat yang digunakan (selain itu, indeks spasial dibangun menggunakan B-tree ).
Namun, ternyata di Microsoft SQL Server 2008, indeks spasial tidak berfungsi jika kolom dengan geotipe dapat menerima nilai NULL
, dan indeks komposit tidak dapat berisi kolom dengan tipe data geografi, dan pertanyaan ini dibahas di Stackoverflow . Itu sebabnya kinerja permintaan seperti itu (tanpa indeks) menjadi sangat rendah.
Pendekatan berikut dapat memecahkan masalah ini:
- Karena nilai
NULL
tidak dapat digunakan, nilai default untuk kolom ini adalah koordinat (0, 0) yang menunjuk ke lokasi di Samudra Atlantik dekat Afrika (titik awal untuk mengukur lintang dan bujur). Namun, di tempat ini serta di dekat titik nyata dapat ditemukan, dan foto-foto yang bukan dari peta harus diabaikan. Jika Anda mengubah titik nol (0, 0) menjadi titik utara jauh (0, 90), maka semuanya akan jauh lebih baik, karena garis lintang 90 mengarah ke tepi peta, dan Anda harus mengabaikan nilai ini saat membangun kisi (mis. membangun hingga garis lintang 89). - Menggunakan SQL Server 2012 atau lebih tinggi dan mengubah tingkat kompatibilitas database menjadi 110 atau lebih tinggi dengan mengeksekusi
ALTER DATABASE database_name SET COMPATIBILITY_LEVEL = 110
. Dalam versi SQL Server ini, bug dengan nilai NULL
dari geotip diperbaiki dan dukungan poligon dengan orientasi yang berbeda (berlawanan arah jarum jam dan searah jarum jam) ditambahkan.
Terlepas dari berbagai kemungkinan geotipe (mereka memungkinkan Anda untuk membuat tidak hanya pilihan sederhana seperti yang ditunjukkan di atas, tetapi juga untuk menggunakan jarak dan poligon yang berbeda), kami tidak menggunakannya dalam proyek kami.
Seleksi normal
Untuk memilih foto dari area yang dibatasi oleh koordinat ( lngMin
latMin
) dan ( latMax
lngMax
), gunakan kueri berikut:
SELECT TOP @Count id, url, ... FROM Photo WHERE latitude > @latMin AND longitude > @lngMin AND latitude < @latMax AND longitude < @lngMax ORDER BY popularity DESC
Perhatikan bahwa dalam hal ini Anda dapat membuat indeks untuk bidang latitude
dan longitude
(berbeda dengan metode pertama), karena tipe data float biasa digunakan. Namun, ada 4 operasi perbandingan dalam pemilihan ini.
Menggunakan tabel hash tambahan
Solusi paling optimal untuk masalah memilih foto dari area tertentu adalah dengan membuat Zooms
tabel tambahan yang menyimpan string yang berisi hash area untuk setiap zoom, seperti yang ditunjukkan di bawah ini.
Query SQL berikut dapat digunakan ( zn
- level zoom saat ini):
DECLARE @hash float; SET @hash = (@latMin + 90) + (@lngMin + 180) * 180 + (@latMax + 90) * 64800 + (@lngMax + 180) * 11664000; SELECT TOP @Count id, url, ... FROM Photo WHERE id = (SELECT id FROM Zooms WHERE zn = @hash)
Kerugian dari pendekatan ini adalah bahwa tabel tambahan menempati ruang memori tambahan.
Terlepas dari kelebihan metode terakhir, kami menggunakan metode kedua ( pemilihan normal ) di server, karena menunjukkan kinerja yang baik.
Menyimpan foto untuk akses multi-utas
Setelah mengekstraksi informasi dari database dengan satu atau lain cara, foto ditempatkan di cache server menggunakan objek sinkronisasi untuk mendukung multithreading sebagai berikut:
private static object SyncObject = new object(); ... List<Photo> photos = (List<Photo>)CachedAreas[hash]; if (photos == null) {
Bagian ini menjelaskan fungsionalitas server untuk mengambil foto dari database dan menyimpannya ke cache. Bagian selanjutnya akan menjelaskan apa yang terjadi di sisi klien di browser.
Sisi klien
Untuk memvisualisasikan peta dan foto di dalamnya, Google Maps API digunakan. Pertama, peta pengguna harus dipindahkan ke tempat tertentu, sesuai dengan geolokasi foto.
Menginisialisasi peta
Ada dua cara untuk menentukan geolokasi saat menginisialisasi peta: untuk menggunakan kemampuan HTML5 atau menggunakan koordinat yang sudah dihitung sebelumnya untuk kawasan.
Menentukan geolokasi menggunakan HTML5
function detectRegion() { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(success); } else { map.setZoom(defaultZoom); map.setCenter(defaultPoint); } } function success(position) { ... map.setZoom(defaultZoom); map.setCenter(new google.maps.LatLng(position.coords.latitude, position.coords.longitude)); }
Kelemahan dari pendekatan ini adalah tidak semua browser mendukung fungsionalitas HTML5 ini dan pengguna mungkin tidak mengizinkan akses ke geoinformasi pada perangkatnya.
Peta diinisialisasi di bagian kode sumber di bawah ini, di mana bounds
adalah koordinat wilayah (area yang dihuni, wilayah atau negara) yang dikembalikan oleh server. Level perkiraan zoom dihitung dalam fungsi getZoomFromBounds
(diambil dari stackoverflow ).
var northEast = bounds.getNorthEast(); var southWest = bounds.getSouthWest(); var myOptions = { zoom: getZoomFromBounds(northEast, southWest), center: new google.maps.LatLng((northEast.lat() + southWest.lat()) / 2, (northEast.lng() + southWest.lng()) / 2), mapTypeId: google.maps.MapTypeId.ROADMAP, minZoom: 3, maxZoom: 19 } map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
function getZoomFromBounds(ne, sw) { var GLOBE_WIDTH = 256;
Di server, wilayah dihitung tergantung pada alamat IP pengguna. Untuk mengagregasi semua koordinat batas untuk setiap wilayah, google geocoding api digunakan, meskipun tidak sah untuk menggunakan informasi tersebut secara offline; selain itu, ada batas 2.500 permintaan per hari. Untuk setiap kota, wilayah, dan negara dari basis data kami, sebuah kueri dihasilkan yang mengembalikan batas-batas viewport
dan bounds
. Mereka berbeda hanya untuk area besar yang tidak dapat sepenuhnya masuk ke viewport. Jika server mengembalikan kesalahan, maka kueri lain digunakan di mana bahasa asli dari wilayah itu atau Inggris digunakan, bagian {Populated area} telah dihapus, dll. http://maps.googleapis.com/maps/api/geocode/xml?address={Country},{Region},{Populated area}&sensor=false
Misalnya, untuk permintaan berikut: http://maps.googleapis.com/maps/api/geocode/xml?address=Russia, wilayah Ivanovo% 20, Ivanovo & sensor = false
Koordinat berikut akan dikembalikan (fragmen) ... <location> <lat>56.9951313</lat> <lng>40.9796047</lng> </location> <location_type>APPROXIMATE</location_type> <viewport> <southwest> <lat>56.9420231</lat> <lng>40.8765941</lng> </southwest> <northeast> <lat>57.0703221</lat> <lng>41.0876169</lng> </northeast> </viewport> <bounds> <southwest> <lat>56.9420231</lat> <lng>40.8765941</lng> </southwest> <northeast> <lat>57.0703221</lat> <lng>41.0876169</lng> </northeast> </bounds> ...
Menghitung area persegi yang terlihat sebagian
Menghitung ukuran area caching
Jadi, seperti yang disebutkan sebelumnya, semua foto di sisi klien dan server di-cache oleh area persegi panjang, titik awal yang merupakan titik arbitrer (dalam kasus kami titik dengan koordinat (0, 0)), dan ukurannya dihitung tergantung pada tingkat zoom saat ini sebagai berikut:
Dengan demikian, pada setiap level zoom, ukuran area persegi panjang adalah 0.75^2=0.5625
dari ukuran viewport saat ini, jika memiliki lebar 1080px dan tinggi 500px.
Menggunakan penundaan saat menggambar ulang
Karena menggambar ulang semua foto di peta bukan operasi cepat (seperti yang akan ditampilkan nanti), kami memutuskan untuk melakukannya dengan beberapa penundaan setelah input pengguna:
google.maps.event.addListener(map, 'bounds_changed', function () { if (boundsChangedInverval != undefined) clearInterval(boundsChangedInverval); var zoom = map.getZoom(); boundsChangedInverval = setTimeout(function () { boundsChanged(); }, prevZoom === zoom ? moveUpdateDelay : zoomUpdateDelay); prevZoom = zoom; });
Menghitung koordinat dan hash area yang terlihat sebagian
Perhitungan koordinat dan hash dari semua persegi panjang yang tumpang tindih dengan jendela terlihat dengan koordinat ( latMin
, lngMin
) dan dimensi yang dihitung menggunakan algoritma yang dijelaskan sebelumnya dilakukan sebagai berikut:
var s = zoomSizes[zoom]; var beginLat = Math.floor((latMin - initPoint.x) / s.width) * s.width + initPoint.x; var beginLng = Math.floor((lngMin - initPoint.y) / s.height) * s.height + initPoint.y; var lat = beginLat; var lng = beginLng; if (lngMax <= beginLng) beginLng = beginLng - 360; while (lat <= maxlat) { lng = beginLng; while (lng <= maxLng) {
Setelah itu, untuk setiap area, fungsi berikut dipanggil, yang mengirimkan permintaan ke server, jika perlu. Rumus perhitungan hash mengembalikan nilai unik untuk setiap area, karena titik awal dan dimensi ditetapkan.
function loadIfNeeded(lat, lng) { var hash = calculateHash(lat, lng, zoom); if (!(hash in items)) {
Menggambar ulang foto yang ditampilkan
Setelah semua foto diunduh atau diekstrak dari cache, beberapa di antaranya perlu digambar ulang. Dengan sejumlah besar foto, atau spidol, di satu tempat, beberapa di antaranya harus disembunyikan, tetapi kemudian menjadi tidak jelas berapa banyak foto yang berada di tempat ini. Untuk mengatasi masalah ini, kami memutuskan untuk mendukung dua jenis penanda: penanda yang menampilkan foto dan penanda yang menunjukkan bahwa ada foto di tempat ini. Selain itu, jika semua penanda disembunyikan ketika batas diubah, dan kemudian ditampilkan kembali, maka pengguna dapat melihat kerlipan. Untuk mengatasi masalah ini, algoritma berikut dikembangkan:
- Mengekstrak semua foto yang terlihat dari cache klien ke array
visMarks
. Perhitungan area ini dengan foto dijelaskan di atas. - Menyortir marker yang diterima berdasarkan popularitas.
- Menemukan marker yang tumpang tindih menggunakan
markerSize
, SmallMarkerSize
, minPhotoDistRatio
dan pixelDistance
. - Membuat array marker besar dengan
maxBigVisPhotosCount
dan marker kecil dengan maxSmlVisPhotosCount
. - Menentukan marker lama yang harus disembunyikan dan menambahkannya ke
smlMarksToHide
dan bigMarksToHide
menggunakan refreshMarkerArrays
. - Memperbarui visibilitas dan indeks zoom
zIndex
untuk penanda baru yang harus ditampilkan menggunakan updateMarkersVis
. - Menambahkan foto, yang menjadi terlihat saat ini, ke umpan menggunakan
addPhotoToRibbon
.
Algoritma untuk perhitungan ulang marker yang terlihat function redraw() { isRedrawing = true; var visMarker; var visMarks = []; var visBigMarks2; var visSmlMarks2; var bigMarksToHide = []; var smlMarksToHide = []; var photo; var i, j; var bounds = map.getBounds(); var northEast = bounds.getNorthEast(); var southWest = bounds.getSouthWest(); var latMin = southWest.lat(); var lngMin = southWest.lng(); var latMax = northEast.lat(); var lngMax = northEast.lng(); var ratio = (latMax - latMin) / $("#map_canvas").height(); var zoom = map.getZoom(); visMarks = []; var k = 0; var s = zoomSizes[zoom]; var beginLat = Math.floor((latMin - initPoint.x) / s.width) * s.width + initPoint.x; var beginLng = Math.floor((lngMin - initPoint.y) / s.height) * s.height + initPoint.y; var lat = beginLat; var lng = beginLng; i = 0; if (lngMax <= beginLng) beginLng = beginLng - 360;
Jarak di peta
Untuk menghitung jarak antara dua titik pada peta dalam piksel , fungsi berikut digunakan:
var Offset = 268435456; var Radius = 85445659.4471; function pixelDistance(latLng1, latLng2, zoom) { var x1 = lonToX(latLng1.lng()); var y1 = latToY(latLng1.lat()); var x2 = lonToX(latLng2.lng()); var y2 = latToY(latLng2.lat()); return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)) >> (21 - zoom); } function lonToX(lng) { return Math.round(Offset + Radius * lng * Math.PI / 180); } function latToY(lat) { return Math.round(Offset - Radius * Math.log((1 + Math.sin(lat * Math.PI / 180)) / (1 - Math.sin(lat * Math.PI / 180))) / 2); }
Fungsi ini juga ditemukan pada stackoverflow.
Untuk membuat marker tampak seperti lingkaran dengan foto (seperti vkontakte), plugin RichMarker digunakan dengan penambahan gaya arbitrer ke elemen div.
Kesimpulan
Ternyata untuk menampilkan foto di peta dengan cepat dan benar, kami harus menyelesaikan masalah yang cukup menarik dan non-sepele terkait dengan caching dan geometri bola. Terlepas dari kenyataan bahwa tidak semua metode yang dijelaskan benar-benar digunakan dalam proyek kami, waktu tidak terbuang sia-sia, karena pengalaman yang kami dapatkan mungkin berguna dalam proyek lain, dan mungkin juga berguna bagi mereka yang telah membaca dan memahami artikel ini.