تحديد الصور وتخزينها مؤقتًا وعرضها على الخريطة

في هذه المقالة ، قررت أن أصف كيفية تنفيذ وظيفة اختيار وعرض الصور في مكان معين على الخريطة في خدمة الصور 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) { // Use lock to avoid extracting from and adding to the cache more than once. lock (SyncObject) { photos = (List<Photo>)CachedAreas[hash]; if (photos == null) { photos = PhotoList.GetAllFromRect(latMin, lngMin, latMax, lngMax, count); // Adding information about photos to the cache with a storage time of 2 minutes with a high storage priority. CachedAreas.Add(hash, photos, null, DateTime.Now.AddSeconds(120), Cache.NoSlidingExpiration, CacheItemPriority.High, null); } } } // Further usage of CachedAreas[hash] 

يصف هذا القسم وظائف الخادم لاسترداد الصور من قاعدة البيانات وحفظها في ذاكرة التخزين المؤقت. يصف القسم التالي ما يحدث في جانب العميل في المتصفح.


جانب العميل


لتصور الخريطة والصور عليها ، تم استخدام واجهة برمجة تطبيقات خرائط 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; // a constant in Google's map projection var west = sw.lng(); var east = ne.lng(); var angle = east - west; if (angle < 0) { angle += 360; } return Math.round(Math.log($('#map_canvas').width() * 360 / angle / GLOBE_WIDTH) / Math.LN2); } 

على الخادم ، يتم حساب المناطق اعتمادًا على عنوان 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)) ، ويتم حساب الحجم وفقًا على مستوى التكبير الحالي على النحو التالي:


 // The initial window at which initMapSizeLat and initMapSizeLng were calculated var initDefaultDimX = 1000, var initDefaultDimY = 800; // The current default viewport which depends on the size of the areas. var currentDefaultDimX = 1080, var currentDefaultDimY = 500; var initMapSizeLat = 0.0003019; var initMapSizeLng = 0.00067055; // The coefficient of size reduction (increase). var initRatio = 0.75; // To calculate the size of the smallest caching area, the map was zoomed in to the maximum zoom level // Ie initMapSizeLat and initMapSizeLng were calculated empirically. var initZoomSize = new google.maps.Size( initMapSizeLat / initDefaultDimX * currentDefaultDimX * initRatio, initMapSizeLng / initDefaultDimY * currentDefaultDimY * initRatio); // All subsequent sizes of areas can be calculated based only on the smallest area (by multiplying each size by 2, because with increasing the zoom level by 1, the linear dimensions increase by 2 times, and the quadratic dimensions increase by 4 times). function initZoomSizes() { zoomSizes = []; var coef = 1; for (var i = 21; i >= 0; i--) { zoomSizes[i] = new google.maps.Size(initZoomSize.width * coef, initZoomSize.height * coef); coef *= 2; } } 

وبالتالي ، عند كل مستوى تكبير / تصغير ، يكون حجم المنطقة المستطيلة 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) { // lat and normalizeLng(lng) coordinates are the coordinates of the overlapping rectangles. // Longitude normalization is used because the right boundary can be greater than 180 or the left boundary can be less than -180. loadIfNeeded(lat, normalizeLng(lng)); lng += s.height; } lat += s.width; } function normalizeLng(lng) { var rtn = lng % 360; if (rtn <= 0) rtn += 360; if (rtn > 180) rtn -= 360; return rtn; } 

بعد ذلك ، يتم استدعاء الوظيفة التالية لكل منطقة ، والتي ترسل الطلب إلى الخادم ، إذا لزم الأمر. ترجع صيغة حساب التجزئة قيمة فريدة لكل منطقة ، لأن نقطة البداية والأبعاد ثابتة.


 function loadIfNeeded(lat, lng) { var hash = calculateHash(lat, lng, zoom); if (!(hash in items)) { // Send a query to the database and put this cell in the client cache. } else { // Do nothing. } } function calculateHash(lat, lng, zoom) { // lat: [-90..90] // lng: [-180..180] return (lat + 90) + ((lng + 180) * 180) + (zoom * 64800); } 

إعادة رسم الصور المعروضة


بعد تنزيل جميع الصور أو استخراجها من ذاكرة التخزين المؤقت ، يلزم إعادة رسم بعضها. مع وجود عدد كبير من الصور ، أو العلامات ، في مكان واحد ، يجب إخفاء بعضها ، ولكن بعد ذلك يصبح من غير الواضح عدد الصور الموجودة في هذا المكان. لحل هذه المشكلة ، قررنا دعم نوعين من العلامات: العلامات التي تعرض الصور والعلامات التي توضح وجود صور في هذا المكان. بالإضافة إلى ذلك ، إذا كانت جميع العلامات مخفية عند تغيير الحدود ، ثم أعيد عرضها ، فقد يلاحظ المستخدم الخفقان. لحل هذه المشكلات ، تم تطوير الخوارزمية التالية:


  1. استخراج جميع الصور المرئية من ذاكرة التخزين المؤقت للعميل إلى صفيف visMarks . تم وصف حساب هذه المناطق مع الصور أعلاه.
  2. فرز العلامات المستلمة حسب الشعبية.
  3. البحث عن علامات متداخلة باستخدام markerSize و SmallMarkerSize و minPhotoDistRatio و pixelDistance .
  4. إنشاء صفيف من العلامات الكبيرة مع maxBigVisPhotosCount وعلامات صغيرة مع maxSmlVisPhotosCount .
  5. تحديد العلامات القديمة التي يجب إخفاؤها وإضافتها إلى smlMarksToHide و bigMarksToHide باستخدام refreshMarkerArrays .
  6. تحديث وضوح مؤشر التكبير والتصغير zIndex للعلامات الجديدة التي يجب عرضها باستخدام updateMarkersVis .
  7. إضافة الصور ، التي أصبحت مرئية في الوقت الحالي ، إلى الخلاصة باستخدام 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; // Extracting all visible markers. while (lat <= latMax) { lng = beginLng; while (lng <= lngMax) { var hash = calcHash(lat, normLng(lng), zoom); if (!(hash in curItems)) { } else { var item = curItems[hash]; for (photo in item.photos) { if (bounds.contains(item.photos[photo].latLng)) { visMarks[i] = item.photos[photo]; visMarks[i].overlapCount = 0; i++; } } } k++; lng += s.height; } lat += s.width; } // Sorting markers by popularity. visMarks.sort(function (a, b) { if (b.priority !== a.priority) { return b.priority - a.priority; } else if (b.popularity !== a.popularity) { return b.popularity - a.popularity; } else { return b.id - a.id; } }); // Finding overlapping markers and markers that exceed a certain specified number. var curInd; var contains; var contains2; var dist; visBigMarks2 = []; visSmlMarks2 = []; for (i = 0; i < visMarks.length; i++) { contains = false; contains2 = false; visMarker = visMarks[i]; for (j = 0; j < visBigMarks2.length; j++) { dist = pixelDistance(visMarker.latLng, visBigMarks2[j].latLng, zoom); if (dist <= markerSize * minPhotoDistRatio) { contains = true; if (contains && contains2) break; } if (dist <= (markerSize + smallMarkerSize) / 2) { contains2 = true; if (contains && contains2) break; } } if (!contains) { if (visBigMarks2.length < maxBigVisPhotosCount) { smlMarksToHide[smlMarksToHide.length] = visMarker; visBigMarks2[visBigMarks2.length] = visMarker; } } else { bigMarksToHide[bigMarksToHide.length] = visMarker; if (!contains2 && visSmlMarks2.length < maxSmlVisPhotosCount) { visSmlMarks2[visSmlMarks2.length] = visMarker; } else { visBigMarks2[j].overlapCount++; } } } // Adding markers that should be hidden to smlMarksToHide and bigMarksToHide. refreshMarkerArrays(visibleSmallMarkers, visSmlMarks2, smlMarksToHide); refreshMarkerArrays(visibleBigMarkers, visBigMarks2, bigMarksToHide); // Hiding invisible markers and displaying visible markers when zIndex changes. var curZInd = maxBigVisPhotosCount + 1; curZInd = updateMarkersVis(visBigMarks2, bigMarksToHide, true, curZInd); curZInd = 0; curZInd = updateMarkersVis(visSmlMarks2, smlMarksToHide, false, curZInd); visibleBigMarkers = visBigMarks2; visibleSmallMarkers = visSmlMarks2; // Adding visible photos to the feed. trPhotosOnMap.innerHTML = ''; for (var marker in visBigMarks2) { addPhotoToRibbon(visBigMarks2[marker]); } isRedrawing = false; } function refreshMarkerArrays(oldArr, newArr, toHide) { for (var j = 0; j < oldArr.length; j++) { contains = false; var visMarker = oldArr[j]; for (i = 0; i < newArr.length; i++) { if (newArr[i].id === visMarker.id) { contains = true; break; } } if (!contains) { toHide[toHide.length] = visMarker; } } } function updateMarkersVis(showArr, hideArr, big, curZInd) { var marker; var bounds = map.getBounds(); for (var i = 0; i < showArr.length; i++) { var photo = showArr[i]; if (big) { marker = photo.bigMarker; $('#divOvlpCount' + photo.id).html(photo.overlapCount); } else { marker = photo.smlMarker; } marker.setZIndex(++curZInd); if (marker.getMap() === null) { marker.setMap(map); } } for (i = 0; i < hideArr.length; i++) { marker = big ? hideArr[i].bigMarker : hideArr[i].smlMarker; if (marker.getMap() !== null) { marker.setMap(null); marker.setZIndex(0); if (!bounds.contains(hideArr[i].latLng)) hideArr[i].priority = 0; } } return curZInd; } function addPhotoToRibbon(marker) { var td = createColumn(marker); if (isLatLngValid(marker.latLng)) { trPhotosOnMap.appendChild(td); } else { trPhotosNotOnMap.appendChild(td); if (photoViewMode == 'user') { var img = $("#photo" + marker.id).children()[0]; $('#photo' + marker.id).draggable({ helper: 'clone', appendTo: $('#map_canvas'), stop: function (e) { var mapBoundingRect = document.getElementById("map_canvas").getBoundingClientRect(); var point = new google.maps.Point(e.pageX - mapBoundingRect.left, e.pageY - mapBoundingRect.top); var latLng = overlay.getProjection().fromContainerPixelToLatLng(point); marker.latLng = latLng; marker.priority = ++curPriority; placeMarker(marker); }, containment: 'parent', distance: 5 }); } } } 

المسافة على الخريطة


لحساب المسافة بين نقطتين على الخريطة بالبكسل ، يتم استخدام الوظيفة التالية:


 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.


الخاتمة


اتضح أنه من أجل عرض الصور على الخريطة بسرعة وبشكل صحيح ، كنا بحاجة إلى حل مشكلات مثيرة للاهتمام وغير تافهة للغاية تتعلق بالتخزين المؤقت والهندسة الكروية. على الرغم من حقيقة أنه لم يتم استخدام كل الطرق الموصوفة بالفعل في مشروعنا ، إلا أن الوقت لم يضيع ، لأن التجربة التي قد نحصل عليها قد تكون مفيدة في مشاريع أخرى ، وقد تكون مفيدة أيضًا لأولئك الذين قرأوا هذا المقال وفهموه.

Source: https://habr.com/ru/post/ar440410/


All Articles