In diesem Artikel habe ich beschlossen zu beschreiben, wie die FunktionalitÀt zum AuswÀhlen und Anzeigen von Fotos an einem bestimmten Ort auf der Karte in unserem Fotoservice gfranq.com implementiert wurde . Der Fotoservice funktioniert jetzt nicht.
Da wir viele Fotos in unserem Service hatten und jedes Mal, wenn die Ănderungen im Ansichtsfenster zu ressourcenintensiv waren, Anfragen an die Datenbank sendeten, war es logisch, die Karte in mehrere Bereiche zu unterteilen, die Informationen zu den abgerufenen Daten enthalten. Aus offensichtlichen GrĂŒnden haben diese Bereiche eine rechteckige Form (obwohl auch ein sechseckiges Gitter berĂŒcksichtigt wurde). Da die Bereiche in groĂem MaĂstab sphĂ€rischer werden, wurden auch Elemente der sphĂ€rischen Geometrie und Werkzeuge dafĂŒr berĂŒcksichtigt.
In diesem Artikel wurden folgende Probleme angesprochen:
- Speichern und Abrufen von Fotos aus der Datenbank und Zwischenspeichern auf dem Server (SQL, C #, ASP.NET).
- Laden Sie die erforderlichen Fotos auf der Clientseite herunter und speichern Sie sie im Client-Cache (JavaScript).
- Neuberechnung von Fotos, die ausgeblendet oder angezeigt werden mĂŒssen, wenn sich das Ansichtsfenster Ă€ndert.
- Elemente der sphÀrischen Geometrie.
Inhalt
Serverteil
Die folgenden Methoden zum AuswÀhlen und Speichern von Geoinformationen in der Datenbank wurden entwickelt:
- Integrierter Geografiedatentyp von SQL Server.
- Normale Auswahl mit EinschrÀnkungen.
- ZusÀtzliche Tabellen verwenden.
Ferner werden diese Verfahren ausfĂŒhrlich beschrieben.
Eingebaute Geotypen
Bekanntlich unterstĂŒtzt SQL Server 2008 Geografie- und Geometriedatentypen, mit denen geografische (auf der Kugel) und geometrische (auf der Ebene) Informationen wie Punkte, Linien, Polygone usw. angegeben werden können. . Um alle Fotos abzurufen, die von einem Rechteck mit den Koordinaten ( lngMin
latMin
) und ( latMax
lngMax
) eingeschlossen sind, können Sie die folgende Abfrage verwenden:
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
Beachten Sie, dass das Polygon gegen den Uhrzeigersinn ausgerichtet ist und der durch die Koordinaten definierte rÀumliche Index IX_Photo_geoTag
verwendet wird (auĂerdem werden rĂ€umliche Indizes mithilfe von B-BĂ€umen erstellt ).
Es stellte sich jedoch heraus, dass rÀumliche Indizes in Microsoft SQL Server 2008 nicht funktionieren, wenn die Spalte mit Geotypen NULL
Werte akzeptieren kann und ein zusammengesetzter Index keine Spalte mit dem Geografiedatentyp enthalten kann. Diese Frage wurde in Stackoverflow erörtert . Aus diesem Grund wird die Leistung solcher Abfragen (ohne Indizes) sehr gering.
Die folgenden AnsÀtze können dieses Problem lösen:
- Da
NULL
Werte nicht verwendet werden können, sind die Standardwerte fĂŒr diese Spalte Koordinaten (0, 0), die auf einen Ort im Atlantik in der NĂ€he von Afrika verweisen (Ausgangspunkt fĂŒr die Messung von LĂ€ngen- und Breitengraden). An diesem Ort und in der NĂ€he können sich jedoch die realen Punkte befinden, und die Fotos, die nicht von der Karte stammen, sollten ignoriert werden. Wenn Sie den Nullpunkt (0, 0) in den Ă€uĂersten Nordpunkt (0, 90) Ă€ndern, ist alles viel besser, da der 90. Breitengrad auf den Rand der Karte zeigt und Sie diesen Wert beim Erstellen des Gitters ignorieren sollten (d. H. bis zum Breitengrad 89 aufbauen). - Verwenden von SQL Server 2012 oder höher und Ăndern der KompatibilitĂ€tsstufe der Datenbank auf 110 oder höher durch AusfĂŒhren von
ALTER DATABASE database_name SET COMPATIBILITY_LEVEL = 110
. In dieser Version von SQL Server wurde der Fehler mit NULL
Werten von Geotypen behoben und die UnterstĂŒtzung von Polygonen mit unterschiedlichen Ausrichtungen (gegen den Uhrzeigersinn und im Uhrzeigersinn) hinzugefĂŒgt.
Trotz der groĂen Möglichkeiten von Geotypen (mit denen Sie nicht nur eine einfache Auswahl wie oben gezeigt treffen können, sondern auch Entfernungen und verschiedene Polygone verwenden können) haben wir sie in unserem Projekt nicht verwendet.
Normale Auswahl
Verwenden Sie die folgende Abfrage, um Fotos aus dem durch Koordinaten ( lngMin
latMin
) und ( latMax
lngMax
) begrenzten Bereich auszuwÀhlen:
SELECT TOP @Count id, url, ... FROM Photo WHERE latitude > @latMin AND longitude > @lngMin AND latitude < @latMax AND longitude < @lngMax ORDER BY popularity DESC
Beachten Sie, dass Sie in diesem Fall (im Gegensatz zur ersten Methode) beliebige Indizes fĂŒr latitude
und longitude
erstellen können, da ein gewöhnlicher Float-Datentyp verwendet wird. Diese Auswahl enthÀlt jedoch 4 Vergleichsoperationen.
ZusÀtzliche Hash-Tabelle verwenden
Die optimalste Lösung fĂŒr das Problem der Auswahl von Fotos aus bestimmten Bereichen besteht darin, zusĂ€tzliche Tabellen- Zooms
zu erstellen, in denen Zeichenfolgen gespeichert sind, die fĂŒr jeden Zoom Hashes von Bereichen enthalten, wie unten gezeigt.
Die folgende SQL-Abfrage kann verwendet werden ( zn
- aktuelle 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)
Der Nachteil dieses Ansatzes besteht darin, dass die zusÀtzliche Tabelle zusÀtzlichen Speicherplatz belegt.
Trotz der Vorteile der letzteren Methode haben wir die zweite Methode ( normale Auswahl ) auf dem Server verwendet, da sie eine gute Leistung zeigte.
Zwischenspeichern von Fotos fĂŒr den Multithread-Zugriff
Nach dem Extrahieren der Informationen aus der Datenbank auf die eine oder andere Weise werden Fotos mithilfe eines Synchronisierungsobjekts in den Server-Cache gestellt, um Multithreading wie folgt zu unterstĂŒtzen:
private static object SyncObject = new object(); ... List<Photo> photos = (List<Photo>)CachedAreas[hash]; if (photos == null) {
In diesem Abschnitt wurden die Serverfunktionen zum Abrufen und Speichern von Fotos aus der Datenbank beschrieben. Im nÀchsten Abschnitt wird beschrieben, was auf der Clientseite im Browser geschieht.
Client-Seite
Zur Visualisierung der Karte und der Fotos wurde die Google Maps-API verwendet. ZunÀchst muss die Benutzerkarte an einen bestimmten Ort verschoben werden, der der geografischen Position der Fotos entspricht.
Karte initialisieren
Es gibt zwei Möglichkeiten, die Geolokalisierung beim Initialisieren der Karte zu bestimmen: Verwenden Sie die Funktionen von HTML5 oder verwenden Sie vorberechnete Koordinaten fĂŒr Regionen.
Bestimmen der Geolokalisierung mit 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)); }
Der Nachteil dieses Ansatzes besteht darin, dass nicht alle Browser diese FunktionalitĂ€t von HTML5 unterstĂŒtzen und der Benutzer möglicherweise keinen Zugriff auf Geoinformationen auf seinem GerĂ€t zulĂ€sst.
Die Karte wird im folgenden Abschnitt des Quellcodes initialisiert, wobei bounds
die Koordinaten der vom Server zurĂŒckgegebenen Region (besiedeltes Gebiet, Region oder Land) sind. Die ungefĂ€hre getZoomFromBounds
wird in der Funktion getZoomFromBounds
(aus dem Stackoverflow entnommen ) berechnet .
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;
Auf dem Server werden die Regionen abhĂ€ngig von der IP-Adresse des Benutzers berechnet. Um alle Koordinaten der Grenzen fĂŒr jede Region zu aggregieren, wurde die Google-Geokodierungs-API verwendet, obwohl es nicht legitim ist, solche Informationen offline zu verwenden. DarĂŒber hinaus gibt es ein Limit von 2500 Anfragen pro Tag. FĂŒr jede Stadt, Region und jedes Land aus unserer Datenbank wurde eine Abfrage generiert, die die erforderlichen Grenzen des viewport
und der bounds
zurĂŒckgab. Sie unterscheiden sich nur fĂŒr groĂe Bereiche, die nicht vollstĂ€ndig in das Ansichtsfenster passen. Wenn der Server einen Fehler zurĂŒckgegeben hat, wurden andere Abfragen verwendet, in denen die Muttersprache dieser Region oder Englisch verwendet wurde, der Teil {BestĂŒckter Bereich} entfernt wurde usw. http://maps.googleapis.com/maps/api/geocode/xml?address={Country},{Region},{Populated area}&sensor=false
Beispiel fĂŒr die folgende Abfrage: http://maps.googleapis.com/maps/api/geocode/xml?address=Russia, Ivanovo% 20 area, Ivanovo & sensor = false
Die folgenden Koordinaten werden zurĂŒckgegeben (Fragment) ... <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> ...
Berechnung teilweise sichtbarer rechteckiger FlÀchen
Berechnung der GröĂe von Caching-Bereichen
Wie bereits erwĂ€hnt, werden alle Fotos sowohl auf der Client- als auch auf der Serverseite von rechteckigen Bereichen zwischengespeichert, deren Startpunkt ein beliebiger Punkt ist (in unserem Fall der Punkt mit den Koordinaten (0, 0)), und die GröĂe wird abhĂ€ngig berechnet auf der aktuellen Zoomstufe wie folgt:
Somit betrĂ€gt die GröĂe des rechteckigen Bereichs bei jeder 0.75^2=0.5625
gegenĂŒber der GröĂe des aktuellen Ansichtsfensters, wenn er eine Breite von 1080 Pixel und eine Höhe von 500 Pixel hat.
Verwenden der Verzögerung beim Neuzeichnen
Da das Neuzeichnen aller Fotos auf der Karte kein schneller Vorgang ist (wie spÀter gezeigt wird), haben wir uns entschlossen, dies nach der Benutzereingabe mit einiger Verzögerung zu tun:
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; });
Berechnung von Koordinaten und Hashes von teilweise sichtbaren Bereichen
Die Berechnung der Koordinaten und Hashes aller Rechtecke, die das sichtbare Fenster mit Koordinaten ( latMin
, lngMin
) und Dimensionen ĂŒberlappen, die mit dem zuvor beschriebenen Algorithmus berechnet wurden, erfolgt wie folgt:
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) {
Danach wird fĂŒr jeden Bereich die folgende Funktion aufgerufen, die bei Bedarf die Anfrage an den Server sendet. Die Formel der Hash-Berechnung gibt fĂŒr jeden Bereich einen eindeutigen Wert zurĂŒck, da der Startpunkt und die Abmessungen festgelegt sind.
function loadIfNeeded(lat, lng) { var hash = calculateHash(lat, lng, zoom); if (!(hash in items)) {
Neuzeichnen der angezeigten Fotos
Nachdem alle Fotos heruntergeladen oder aus dem Cache extrahiert wurden, mĂŒssen einige neu gezeichnet werden. Bei einer groĂen Anzahl von Fotos oder Markierungen an einem Ort sollten einige davon ausgeblendet sein, aber dann wird unklar, wie viele Fotos sich an diesem Ort befinden. Um dieses Problem zu lösen, haben wir uns entschieden, zwei Arten von Markierungen zu unterstĂŒtzen: Markierungen, die Fotos anzeigen, und Markierungen, die anzeigen, dass sich an dieser Stelle Fotos befinden. Wenn auĂerdem alle Markierungen ausgeblendet werden, wenn die Grenzen geĂ€ndert und dann erneut angezeigt werden, kann der Benutzer ein Flackern bemerken. Um diese Probleme zu lösen, wurde der folgende Algorithmus entwickelt:
- Extrahieren aller sichtbaren Fotos aus dem Client-Cache in das Array
visMarks
. Die Berechnung dieser FlÀchen mit Fotos wurde oben beschrieben. - Sortieren der empfangenen Marker nach Beliebtheit.
- Suchen ĂŒberlappender Marker mithilfe von
markerSize
, SmallMarkerSize
, minPhotoDistRatio
und pixelDistance
. - Erstellen von Arrays mit groĂen Markern mit
maxBigVisPhotosCount
und kleinen Markern mit maxSmlVisPhotosCount
. - Definieren Sie alte Marker, die ausgeblendet werden sollen, und fĂŒgen Sie sie mithilfe von
refreshMarkerArrays
zu smlMarksToHide
und bigMarksToHide
refreshMarkerArrays
. - Aktualisieren der Sichtbarkeit und des
zIndex
fĂŒr neue Markierungen, die mit updateMarkersVis
angezeigt werden updateMarkersVis
. - HinzufĂŒgen von Fotos, die zum aktuellen Zeitpunkt sichtbar wurden, zum Feed mit
addPhotoToRibbon
.
Algorithmus zur Neuberechnung sichtbarer Marker 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;
Entfernung auf der Karte
Um den Abstand zwischen zwei Punkten auf der Karte in Pixel zu berechnen, wird die folgende Funktion verwendet:
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); }
Diese Funktion wurde auch beim Stackoverflow gefunden.
Um die Markierungen wie Kreise mit Fotos aussehen zu lassen (wie vkontakte), wurde das Plugin RichMarker verwendet und dem div-Element ein beliebiger Stil hinzugefĂŒgt .
Fazit
Es stellte sich heraus, dass wir, um Fotos schnell und korrekt auf der Karte anzuzeigen, interessante und nicht triviale Probleme im Zusammenhang mit Caching und sphĂ€rischer Geometrie lösen mussten. Trotz der Tatsache, dass nicht alle beschriebenen Methoden tatsĂ€chlich in unserem Projekt verwendet wurden, wurde keine Zeit verschwendet, da die Erfahrungen, die wir sammeln, in anderen Projekten nĂŒtzlich sein können und auch fĂŒr diejenigen nĂŒtzlich sein können, die diesen Artikel gelesen und verstanden haben.