Dans cet article, j'ai décidé de décrire comment la fonctionnalité de sélection et d'affichage de photos sur un endroit spécifique sur la carte a été implémentée dans notre service photo gfranq.com . Le service photo ne fonctionne pas maintenant.
Ătant donnĂ© que nous avions beaucoup de photos dans notre service et que nous envoyions des demandes Ă la base de donnĂ©es chaque fois que les modifications de la fenĂȘtre Ă©taient trop gourmandes en ressources, il Ă©tait logique de diviser la carte en plusieurs zones contenant des informations sur les donnĂ©es rĂ©cupĂ©rĂ©es. Pour des raisons Ă©videntes, ces zones ont une forme rectangulaire (bien que la grille hexagonale ait Ă©galement Ă©tĂ© considĂ©rĂ©e). Comme les zones deviennent plus sphĂ©riques Ă grande Ă©chelle, des Ă©lĂ©ments de gĂ©omĂ©trie sphĂ©rique et des outils pour celle-ci ont Ă©galement Ă©tĂ© pris en compte.
Dans cet article, les problÚmes suivants ont été soulevés:
- Stocker et récupérer des photos de la base de données et les mettre en cache sur le serveur (SQL, C #, ASP.NET).
- Télécharger les photos nécessaires du cÎté client et les enregistrer dans le cache client (JavaScript).
- Recalcul des photos qui doivent ĂȘtre masquĂ©es ou affichĂ©es lorsque la fenĂȘtre change.
- ĂlĂ©ments de gĂ©omĂ©trie sphĂ©rique.
Table des matiĂšres
Partie serveur
Les méthodes suivantes de sélection et de stockage de la géoinformation dans la base de données ont été conçues:
- Type de données géographiques intégrées à SQL Server.
- Sélection normale avec restrictions.
- Utilisation de tableaux supplémentaires.
De plus, ces méthodes seront décrites en détail.
Géotypes intégrés
Comme on le sait, SQL Server 2008 prend en charge les types de données géographiques et géométriques, qui permettent de spécifier des informations géographiques (sur la sphÚre) et géométriques (sur le plan), telles que des points, des lignes, des polygones, etc. . Afin de récupérer toutes les photos entourées d'un rectangle avec les coordonnées ( lngMin
latMin
) et ( latMax
lngMax
), vous pouvez utiliser la requĂȘte suivante:
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
Notez que le polygone est orienté dans le sens antihoraire et que l'index spatial IX_Photo_geoTag
défini par les coordonnées est utilisé (en outre, les index spatiaux sont construits à l'aide d' arbres B ).
Cependant, il s'est avéré que dans Microsoft SQL Server 2008, les index spatiaux ne fonctionnent pas si la colonne avec des géotypes peut accepter des valeurs NULL
et qu'un index composite ne peut pas contenir une colonne avec un type de donnĂ©es gĂ©ographiques, et cette question a Ă©tĂ© discutĂ©e sur Stackoverflow . C'est pourquoi les performances de ces requĂȘtes (sans index) deviennent trĂšs faibles.
Les approches suivantes peuvent résoudre ce problÚme:
- Ătant donnĂ© que les valeurs
NULL
ne peuvent pas ĂȘtre utilisĂ©es, les valeurs par dĂ©faut pour cette colonne sont des coordonnĂ©es (0, 0) qui pointent vers un emplacement dans l'ocĂ©an Atlantique prĂšs de l'Afrique (le point de dĂ©part pour mesurer la latitude et la longitude). Cependant, Ă cet endroit ainsi qu'Ă proximitĂ©, les vrais points peuvent ĂȘtre localisĂ©s, et les photos ne provenant pas de la carte doivent ĂȘtre ignorĂ©es. Si vous changez le point zĂ©ro (0, 0) en point extrĂȘme nord (0, 90), alors tout ira beaucoup mieux, car la latitude 90 pointe vers le bord de la carte, et vous devez ignorer cette valeur lors de la construction de la grille (c.-Ă -d. construire jusqu'Ă la latitude 89). - Utilisation de SQL Server 2012 ou supĂ©rieur et modification du niveau de compatibilitĂ© de la base de donnĂ©es Ă 110 ou supĂ©rieur en exĂ©cutant
ALTER DATABASE database_name SET COMPATIBILITY_LEVEL = 110
. Dans cette version de SQL Server, le bogue avec les valeurs NULL
des géotypes a été corrigé et la prise en charge de polygones d'orientations différentes (sens antihoraire et horaire) a été ajoutée.
Malgré les vastes possibilités des géotypes (ils vous permettent de faire non seulement une simple sélection comme illustré ci-dessus, mais aussi d'utiliser des distances et différents polygones), nous ne les avons pas utilisés dans notre projet.
Sélection normale
Pour sélectionner des photos dans la zone délimitée par les coordonnées ( lngMin
latMin
) et ( latMax
lngMax
), utilisez la requĂȘte suivante:
SELECT TOP @Count id, url, ... FROM Photo WHERE latitude > @latMin AND longitude > @lngMin AND latitude < @latMax AND longitude < @lngMax ORDER BY popularity DESC
Notez que dans ce cas, vous pouvez créer des index pour les champs de latitude
et de longitude
(contrairement à la premiÚre méthode), car un type de données float ordinaire est utilisé. Cependant, il y a 4 opérations de comparaison dans cette sélection.
Utilisation d'une table de hachage supplémentaire
La solution la plus optimale au problÚme de sélection de photos dans certaines zones consiste à créer des Zooms
table Zooms
qui stockent des chaßnes contenant des hachages de zones pour chaque zoom, comme indiqué ci-dessous.
La requĂȘte SQL suivante peut ĂȘtre utilisĂ©e ( zn
- niveau de zoom actuel):
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)
L'inconvénient de cette approche est que la table supplémentaire occupe un espace mémoire supplémentaire.
Malgré les avantages de cette derniÚre méthode, nous avons utilisé la deuxiÚme méthode ( sélection normale ) sur le serveur, car elle montrait de bonnes performances.
Mise en cache des photos pour un accĂšs multi-thread
AprÚs avoir extrait les informations de la base de données d'une maniÚre ou d'une autre, les photos sont placées dans le cache du serveur à l'aide de l'objet de synchronisation pour prendre en charge le multithreading comme suit:
private static object SyncObject = new object(); ... List<Photo> photos = (List<Photo>)CachedAreas[hash]; if (photos == null) {
Cette section décrit la fonctionnalité du serveur pour récupérer des photos de la base de données et les enregistrer dans le cache. La section suivante décrira ce qui se passe du cÎté client dans le navigateur.
CÎté client
Pour visualiser la carte et les photos qu'elle contient, l'API Google Maps a Ă©tĂ© utilisĂ©e. Tout d'abord, la carte utilisateur doit ĂȘtre dĂ©placĂ©e Ă un certain endroit, correspondant Ă la gĂ©olocalisation des photos.
Initialisation de la carte
Il existe deux façons de déterminer la géolocalisation lors de l'initialisation de la carte: utiliser les capacités de HTML5 ou utiliser des coordonnées pré-calculées pour les régions.
Déterminer la géolocalisation à l'aide de 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)); }
L'inconvénient de cette approche est que tous les navigateurs ne prennent pas en charge cette fonctionnalité de HTML5 et l'utilisateur peut ne pas autoriser l'accÚs à la géoinformation sur son appareil.
La carte est initialisĂ©e dans la section du code source ci-dessous, oĂč les bounds
sont les coordonnées de la région (zone peuplée, région ou pays) renvoyées par le serveur. Le niveau de zoom approximatif est calculé dans la fonction getZoomFromBounds
(prise Ă partir de 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;
Sur le serveur, les rĂ©gions sont calculĂ©es en fonction de l'adresse IP de l'utilisateur. Pour agrĂ©ger toutes les coordonnĂ©es des limites de chaque rĂ©gion, l' API de gĂ©ocodage Google a Ă©tĂ© utilisĂ©e, bien qu'il ne soit pas lĂ©gitime d'utiliser ces informations hors ligne; en outre, il y a une limite de 2500 demandes par jour. Pour chaque ville, rĂ©gion et pays de notre base de donnĂ©es, une requĂȘte a Ă©tĂ© gĂ©nĂ©rĂ©e qui a renvoyĂ© les limites requises de la viewport
et des bounds
. Ils ne diffĂšrent que pour les grandes zones qui ne peuvent pas entrer complĂštement dans la fenĂȘtre Si le serveur a renvoyĂ© une erreur, d'autres requĂȘtes ont Ă©tĂ© utilisĂ©es dans lesquelles la langue maternelle de cette rĂ©gion ou l'anglais a Ă©tĂ© utilisĂ©e, la partie {Zone peuplĂ©e} a Ă©tĂ© supprimĂ©e, etc. http://maps.googleapis.com/maps/api/geocode/xml?address={Country},{Region},{Populated area}&sensor=false
Par exemple, pour la requĂȘte suivante: http://maps.googleapis.com/maps/api/geocode/xml?address=Russia, zone Ivanovo% 20, Ivanovo & sensor = false
Les coordonnées suivantes seront retournées (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> ...
Calcul de zones rectangulaires partiellement visibles
Calcul de la taille des zones de mise en cache
Ainsi, comme mentionné précédemment, toutes les photos cÎté client et cÎté serveur sont mises en cache par des zones rectangulaires, dont le point de départ est un point arbitraire (dans notre cas, le point avec les coordonnées (0, 0)), et la taille est calculée en fonction sur le niveau de zoom actuel comme suit:
Ainsi, Ă chaque niveau de zoom, la taille de la zone rectangulaire est de 0.75^2=0.5625
rapport Ă la taille de la fenĂȘtre courante, si elle a une largeur de 1080px et une hauteur de 500px.
Utilisation du délai lors du redessin
Ătant donnĂ© que le redessin de toutes les photos sur la carte n'est pas une opĂ©ration rapide (comme cela sera montrĂ© plus loin), nous avons dĂ©cidĂ© de le faire avec un certain retard aprĂšs l'entrĂ©e de l'utilisateur:
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; });
Calcul des coordonnées et des hachages des zones partiellement visibles
Le calcul des coordonnĂ©es et des hachages de tous les rectangles qui chevauchent la fenĂȘtre visible avec des coordonnĂ©es ( latMin
, lngMin
) et des dimensions calculées en utilisant l'algorithme décrit précédemment se fait comme suit:
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) {
AprÚs cela, pour chaque zone, la fonction suivante est appelée, qui envoie la demande au serveur, si nécessaire. La formule de calcul du hachage renvoie une valeur unique pour chaque zone, car le point de départ et les dimensions sont fixes.
function loadIfNeeded(lat, lng) { var hash = calculateHash(lat, lng, zoom); if (!(hash in items)) {
Redessiner les photos affichées
Une fois toutes les photos tĂ©lĂ©chargĂ©es ou extraites du cache, certaines doivent ĂȘtre redessinĂ©es. Avec un grand nombre de photos ou de marqueurs au mĂȘme endroit, certaines doivent ĂȘtre masquĂ©es, mais il devient alors difficile de savoir combien de photos se trouvent Ă cet endroit. Pour rĂ©soudre ce problĂšme, nous avons dĂ©cidĂ© de prendre en charge deux types de marqueurs: les marqueurs affichant des photos et les marqueurs indiquant qu'il y a des photos Ă cet endroit. En outre, si tous les marqueurs sont masquĂ©s lorsque les limites sont modifiĂ©es, puis sont rĂ©affichĂ©s, l'utilisateur peut remarquer un scintillement. Pour rĂ©soudre ces problĂšmes, l'algorithme suivant a Ă©tĂ© dĂ©veloppĂ©:
- Extraction de toutes les photos visibles du cache client vers le tableau
visMarks
. Le calcul de ces zones avec des photos a été décrit ci-dessus. - Tri des marqueurs reçus par popularité.
- Recherche de marqueurs superposés à l'aide de
markerSize
, SmallMarkerSize
, minPhotoDistRatio
et pixelDistance
. - Création de tableaux de grands marqueurs avec
maxBigVisPhotosCount
et de petits marqueurs avec maxSmlVisPhotosCount
. - DĂ©finir les anciens marqueurs qui doivent ĂȘtre masquĂ©s et les ajouter Ă
smlMarksToHide
et bigMarksToHide
aide de refreshMarkerArrays
. - Mise à jour de l'index de visibilité et de zoom
zIndex
pour les nouveaux marqueurs qui devraient ĂȘtre affichĂ©s Ă l'aide de updateMarkersVis
. - Ajout de photos, qui sont devenues visibles Ă l'heure actuelle, au flux Ă l'aide d'
addPhotoToRibbon
.
Algorithme de recalcul des marqueurs visibles 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;
Distance sur la carte
Pour calculer la distance entre deux points de la carte en pixels, la fonction suivante est utilisée:
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); }
Cette fonction a également été trouvée sur stackoverflow.
Pour que les marqueurs ressemblent à des cercles avec des photos (comme vkontakte), le plugin RichMarker a été utilisé avec l'ajout d'un style arbitraire à l'élément div.
Conclusion
Il s'est avĂ©rĂ© que pour afficher les photos sur la carte rapidement et correctement, nous devions rĂ©soudre des problĂšmes assez intĂ©ressants et non triviaux liĂ©s Ă la mise en cache et Ă la gĂ©omĂ©trie sphĂ©rique. MalgrĂ© le fait que toutes les mĂ©thodes dĂ©crites n'ont pas Ă©tĂ© rĂ©ellement utilisĂ©es dans notre projet, le temps n'a pas Ă©tĂ© perdu, car l'expĂ©rience que nous acquĂ©rons pourrait ĂȘtre utile dans d'autres projets, et elle pourrait Ă©galement ĂȘtre utile pour ceux qui ont lu et compris cet article.