في هذه المقالة ، قررت أن أصف كيفية تنفيذ وظيفة اختيار وعرض الصور في مكان معين على الخريطة في خدمة الصور gfranq.com لدينا. خدمة الصور لا تعمل الآن.
نظرًا لأن لدينا الكثير من الصور في خدمتنا وإرسال الطلبات إلى قاعدة البيانات في كل مرة كانت فيها تغييرات viewport تستهلك الكثير من الموارد ، كان من المنطقي تقسيم الخريطة إلى عدة مناطق تحتوي على معلومات حول البيانات التي تم استردادها. لأسباب واضحة ، يكون لهذه المناطق شكل مستطيل (على الرغم من أن الشبكة سداسية كانت تعتبر أيضًا). نظرًا لأن المناطق أصبحت أكثر كروية على نطاقات كبيرة ، تم أيضًا النظر في عناصر الهندسة الكروية والأدوات اللازمة لذلك.
في هذه المقالة ، تم طرح المشكلات التالية:
- تخزين واسترجاع الصور من قاعدة البيانات وتخزينها مؤقتًا على الخادم (SQL ، C # ، ASP.NET).
- تنزيل الصور الضرورية على جانب العميل وحفظها في ذاكرة التخزين المؤقت للعميل (JavaScript).
- إعادة حساب الصور التي يجب أن تكون مخفية أو تظهر عندما يتغير منفذ العرض.
- عناصر الهندسة الكروية.
المحتويات
جزء الخادم
تم تصميم الطرق التالية لتحديد وتخزين المعلومات الجغرافية في قاعدة البيانات:
- مزود خدمة البيانات الجغرافية المدمج في نوع البيانات.
- اختيار طبيعي مع قيود.
- باستخدام جداول إضافية.
علاوة على ذلك ، سيتم وصف هذه الأساليب في التفاصيل.
المدمج في geotypes
كما هو معروف ، يدعم SQL Server 2008 أنواع بيانات الجغرافيا والهندسة ، والتي تتيح تحديد المعلومات الجغرافية (على المجال) والمعلومات الهندسية (على المستوى) ، مثل النقاط والخطوط والمضلعات وما إلى ذلك. . لاسترداد جميع الصور المحاطة lngMin
latMin
( lngMin
latMin
) و ( latMax
lngMax
) ، يمكنك استخدام الاستعلام التالي:
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
لاحظ أن المضلع موجه نحو عكس اتجاه عقارب الساعة IX_Photo_geoTag
الفهرس المكاني IX_Photo_geoTag
المعرّف بواسطة الإحداثيات (إلى جانب ذلك ، يتم إنشاء فهارس مكانية باستخدام الأشجار B ).
ومع ذلك ، اتضح أنه في Microsoft SQL Server 2008 ، لا تعمل الفهارس المكانية إذا كان العمود ذي الأنماط الجغرافية يمكنه قبول قيم NULL
، ولا يمكن أن يحتوي الفهرس المركب على عمود يحتوي على نوع بيانات الجغرافيا ، وقد تمت مناقشة هذا السؤال على Stackoverflow . لهذا السبب يصبح أداء مثل هذه الاستعلامات (بدون فهارس) منخفضًا للغاية.
يمكن للنُهج التالية حل هذه المشكلة:
- نظرًا لأنه لا يمكن استخدام قيم
NULL
، فإن القيم الافتراضية لهذا العمود هي إحداثيات (0 ، 0) تشير إلى موقع في المحيط الأطلسي بالقرب من إفريقيا (نقطة البداية لقياس خطوط الطول والعرض). ومع ذلك ، في هذا المكان وكذلك في مكان قريب ، يمكن تحديد موقع النقاط الحقيقية ، ويجب تجاهل الصور غير الواردة من الخريطة. إذا قمت بتغيير نقطة الصفر (0 ، 0) إلى نقطة الشمال الأقصى (0 ، 90) ، فسيكون كل شيء أفضل بكثير ، لأن خط العرض 90 يشير إلى حافة الخريطة ، ويجب أن تتجاهل هذه القيمة عند إنشاء الشبكة (أي بناء حتى خط العرض 89). - باستخدام SQL Server 2012 أو أعلى وتغيير مستوى توافق قاعدة البيانات إلى 110 أو أعلى عن طريق تنفيذ
ALTER DATABASE database_name SET COMPATIBILITY_LEVEL = 110
. في هذا الإصدار من SQL Server ، تم إصلاح الخلل ذي القيم NULL
للأنماط الجيولوجية وتم إضافة دعم المضلعات ذات التوجهات المختلفة (عكس اتجاه عقارب الساعة وعكس عقارب الساعة).
على الرغم من الاحتمالات الواسعة للأنماط الجيولوجية (تسمح لك بإجراء اختيار بسيط ليس فقط كما هو موضح أعلاه ، ولكن أيضًا استخدام المسافات ومضلعات مختلفة) ، لم نستخدمها في مشروعنا.
اختيار طبيعي
لتحديد الصور من المنطقة lngMin
latMin
( lngMin
latMin
) و ( latMax
lngMax
) ، استخدم الاستعلام التالي:
SELECT TOP @Count id, url, ... FROM Photo WHERE latitude > @latMin AND longitude > @lngMin AND latitude < @latMax AND longitude < @lngMax ORDER BY popularity DESC
لاحظ أنه في هذه الحالة ، يمكنك إنشاء أي فهارس لحقول latitude
longitude
(على عكس الطريقة الأولى) ، لأنه يتم استخدام نوع بيانات التعويم العادي. ومع ذلك ، هناك 4 عمليات مقارنة في هذا الاختيار.
باستخدام جدول التجزئة إضافية
إن الحل الأمثل لمشكلة اختيار الصور من مناطق معينة هو إنشاء جدول إضافي Zooms
يقوم بتخزين السلاسل التي تحتوي على تجزئة المساحات لكل التكبير ، كما هو موضح أدناه.
يمكن استخدام استعلام SQL التالي ( zn
- مستوى التكبير الحالي):
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)
عيوب هذا الأسلوب هو أن الجدول الإضافي يشغل مساحة إضافية من الذاكرة.
على الرغم من مزايا الطريقة الأخيرة ، استخدمنا الطريقة الثانية ( التحديد العادي ) على الخادم ، حيث أظهرت أداءً جيدًا.
تخزين الصور مؤقتًا للوصول متعدد الخيوط
بعد استخراج المعلومات من قاعدة البيانات بطريقة أو بأخرى ، يتم وضع الصور في ذاكرة التخزين المؤقت للخادم باستخدام كائن متزامن لدعم تعدد العمليات على النحو التالي:
private static object SyncObject = new object(); ... List<Photo> photos = (List<Photo>)CachedAreas[hash]; if (photos == null) {
يصف هذا القسم وظائف الخادم لاسترداد الصور من قاعدة البيانات وحفظها في ذاكرة التخزين المؤقت. يصف القسم التالي ما يحدث في جانب العميل في المتصفح.
جانب العميل
لتصور الخريطة والصور عليها ، تم استخدام واجهة برمجة تطبيقات خرائط Google. أولاً ، يجب نقل خريطة المستخدم إلى مكان معين ، بما يتوافق مع الموقع الجغرافي للصور.
تهيئة الخريطة
هناك طريقتان لتحديد الموقع الجغرافي عند تهيئة الخريطة: لاستخدام قدرات HTML5 أو استخدام الإحداثيات المحسوبة مسبقًا للمناطق.
تحديد الموقع الجغرافي باستخدام 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)); }
عيب هذا النهج هو أنه ليست كل المتصفحات تدعم وظيفة HTML5 ولا يجوز للمستخدم السماح بالوصول إلى المعلومات الجغرافية على جهازه.
تتم تهيئة الخريطة في قسم الكود المصدري أدناه ، حيث bounds
هي إحداثيات المنطقة (المنطقة المأهولة بالسكان أو المنطقة أو البلد) التي يتم إرجاعها بواسطة الخادم. يتم حساب مستوى التكبير التقريبي في وظيفة getZoomFromBounds
(مأخوذة من getZoomFromBounds
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;
على الخادم ، يتم حساب المناطق اعتمادًا على عنوان IP الخاص بالمستخدم. لتجميع كل إحداثيات الحدود لكل منطقة ، تم استخدام google coding api ، على الرغم من أنه ليس من المشروع استخدام هذه المعلومات في وضع عدم الاتصال ؛ بالإضافة إلى ذلك ، يوجد 2500 طلب يوميًا. لكل مدينة ومنطقة وبلد من قاعدة البيانات الخاصة بنا ، تم إنشاء استعلام أعاد الحدود المطلوبة ل viewport
bounds
. إنها تختلف فقط عن المساحات الكبيرة التي لا يمكن وضعها بالكامل في إطار العرض. إذا أرجع الخادم خطأً ، عندئذٍ تم استخدام استعلامات أخرى تستخدم فيها اللغة الأصلية لتلك المنطقة أو الإنجليزية ، وتم إزالة الجزء {Populated area} ، إلخ. http://maps.googleapis.com/maps/api/geocode/xml?address={Country},{Region},{Populated area}&sensor=false
على سبيل المثال ، بالنسبة للاستعلام التالي: http://maps.googleapis.com/maps/api/geocode/xml؟address=Russia ، منطقة Ivanovo٪ 20 ، Ivanovo & sensor = false
سيتم إرجاع الإحداثيات التالية (جزء) ... <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> ...
حساب مناطق مستطيلة مرئية جزئيا
حساب حجم مناطق التخزين المؤقت
لذلك ، كما ذكرنا سابقًا ، يتم تخزين جميع الصور على كل من العميل والخادم في مناطق مستطيلة الشكل ، حيث تكون نقطة البداية نقطة تعسفية (في حالتنا ، نقطة الإحداثيات (0 ، 0)) ، ويتم حساب الحجم وفقًا على مستوى التكبير الحالي على النحو التالي:
وبالتالي ، عند كل مستوى تكبير / تصغير ، يكون حجم المنطقة المستطيلة 0.75^2=0.5625
من حجم منفذ العرض الحالي ، إذا كان عرضه 1080 بكسل وارتفاعه 500 بكسل.
استخدام التأخير عند إعادة الرسم
نظرًا لأن إعادة رسم جميع الصور على الخريطة ليست عملية سريعة (كما سيظهر لاحقًا) ، فقد قررنا القيام بذلك مع بعض التأخير بعد إدخال المستخدم:
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; });
حساب الإحداثيات والتجزئة من المناطق المرئية جزئيا
يتم حساب الإحداثيات والتجزئة لكل المستطيلات التي تتداخل مع النافذة المرئية مع الإحداثيات ( latMin
، lngMin
) والأبعاد المحسوبة باستخدام الخوارزمية الموضحة مسبقًا على النحو التالي:
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) {
بعد ذلك ، يتم استدعاء الوظيفة التالية لكل منطقة ، والتي ترسل الطلب إلى الخادم ، إذا لزم الأمر. ترجع صيغة حساب التجزئة قيمة فريدة لكل منطقة ، لأن نقطة البداية والأبعاد ثابتة.
function loadIfNeeded(lat, lng) { var hash = calculateHash(lat, lng, zoom); if (!(hash in items)) {
إعادة رسم الصور المعروضة
بعد تنزيل جميع الصور أو استخراجها من ذاكرة التخزين المؤقت ، يلزم إعادة رسم بعضها. مع وجود عدد كبير من الصور ، أو العلامات ، في مكان واحد ، يجب إخفاء بعضها ، ولكن بعد ذلك يصبح من غير الواضح عدد الصور الموجودة في هذا المكان. لحل هذه المشكلة ، قررنا دعم نوعين من العلامات: العلامات التي تعرض الصور والعلامات التي توضح وجود صور في هذا المكان. بالإضافة إلى ذلك ، إذا كانت جميع العلامات مخفية عند تغيير الحدود ، ثم أعيد عرضها ، فقد يلاحظ المستخدم الخفقان. لحل هذه المشكلات ، تم تطوير الخوارزمية التالية:
- استخراج جميع الصور المرئية من ذاكرة التخزين المؤقت للعميل إلى صفيف
visMarks
. تم وصف حساب هذه المناطق مع الصور أعلاه. - فرز العلامات المستلمة حسب الشعبية.
- البحث عن علامات متداخلة باستخدام
markerSize
و SmallMarkerSize
و minPhotoDistRatio
و pixelDistance
. - إنشاء صفيف من العلامات الكبيرة مع
maxBigVisPhotosCount
وعلامات صغيرة مع maxSmlVisPhotosCount
. - تحديد العلامات القديمة التي يجب إخفاؤها وإضافتها إلى
smlMarksToHide
و bigMarksToHide
باستخدام refreshMarkerArrays
. - تحديث وضوح مؤشر التكبير والتصغير
zIndex
للعلامات الجديدة التي يجب عرضها باستخدام updateMarkersVis
. - إضافة الصور ، التي أصبحت مرئية في الوقت الحالي ، إلى الخلاصة باستخدام
addPhotoToRibbon
.
خوارزمية لإعادة حساب العلامات المرئية 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;
المسافة على الخريطة
لحساب المسافة بين نقطتين على الخريطة بالبكسل ، يتم استخدام الوظيفة التالية:
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); }
تم العثور على هذه الوظيفة أيضًا على stackoverflow.
لجعل العلامات تبدو كدوائر بها صور (مثل vkontakte) ، تم استخدام المكون الإضافي RichMarker مع إضافة نمط تعسفي لعنصر div.
الخاتمة
اتضح أنه من أجل عرض الصور على الخريطة بسرعة وبشكل صحيح ، كنا بحاجة إلى حل مشكلات مثيرة للاهتمام وغير تافهة للغاية تتعلق بالتخزين المؤقت والهندسة الكروية. على الرغم من حقيقة أنه لم يتم استخدام كل الطرق الموصوفة بالفعل في مشروعنا ، إلا أن الوقت لم يضيع ، لأن التجربة التي قد نحصل عليها قد تكون مفيدة في مشاريع أخرى ، وقد تكون مفيدة أيضًا لأولئك الذين قرأوا هذا المقال وفهموه.