Es en este punto cuando se comienza a buscar alguna solución a este “agradable” problema. Rápidamente te das cuenta que necesitas de alguna forma “agrupar” (cluster en inglés) información y por ahí comienzas a trabajar.
Muchos métodos de agrupamiento en el fondo se parecen. Básicamente se trata de agrupar datos siguiendo algún criterio. Para esto se necesita alguna ecuación que nos indique cuán parecidos son 2 elementos, se define un umbral y los datos que no sobrepasen dicho umbral conforman el clúster. En nuestro caso, el criterio que se sigue es hallar la distancia que existe entre 2 puntos, si esa distancia es menor que un umbral que se defina entonces se agrupan. En la siguiente figura se muestra el resultado final, también se puede ver en la dirección http://www.chil.es/yellowpages
Existe una biblioteca capaz de agrupar datos pero lo hace del lado del cliente lo cual no resulta beneficioso debido a la gran cantidad de datos que es necesario enviar en la respuesta HTTP.
Pues bien, manos a la obra. Lo primero que se necesita es hallar la distancia entre los puntos, esto se consigue a través de la fórmula de Haversine. Sucede que en nuestro caso necesitamos la distancia pero en píxeles, para esto usamos la proyección de Mercator. La siguiente clase encapsula los métodos necesarios para trabajar con las fórmulas de distancia:
1: public static class GeolocalizationUtils
2: {
3: public const int Offset = 268435456; //It is half of the earth circumference in pixels at zoom level 21
4:
5: public const double Radius = 85445659.4471; // offset / pi()
6:
7: public const double EartRadiusInMiles = 3956.0;
8:
9: public static double ToRadian(double val)
10: {
11: return val * (Math.PI / 180);
12: }
13:
14: public static double DiffRadian(double val1, double val2)
15: {
16: return ToRadian(val2) - ToRadian(val1);
17: }
18:
19: public static double ConvertFromMetersToMiles(int meters)
20: {
21: return meters * 0.000621371192237334;
22: }
23:
24: /// <summary>
25: /// Evaluate the distance between two points on the Earth (miles)
26: /// </summary>
27: /// <param name="lat1">Latitude 1</param>
28: /// <param name="lng1">Longitude 1</param>
29: /// <param name="lat2">Latitude 2</param>
30: /// <param name="lng2">Longitude 2</param>
31: /// <returns></returns>
32: public static double CalcDistance(double lat1, double lng1, double lat2, double lng2)
33: {
34: return EartRadiusInMiles * 2 * (Math.Asin(
35: Math.Min(1,
36: Math.Sqrt(
37: (
38: Math.Pow(Math.Sin((DiffRadian(lat1, lat2)) / 2.0), 2.0) +
39: Math.Cos(ToRadian(lat1)) * Math.Cos(ToRadian(lat2)) *
40: Math.Pow(Math.Sin((DiffRadian(lng1, lng2)) / 2.0), 2.0)
41: )
42: )
43: )
44: )
45: );
46: }
47:
48: public static int LonToX(double lon)
49: {
50: return (int)Math.Round(Offset + Radius * lon * Math.PI / 180);
51: }
52:
53: public static int LatToY(double lat)
54: {
55: return (int)Math.Round(Offset - Radius * Math.Log((1 + Math.Sin(lat * Math.PI / 180)) / (1 - Math.Sin(lat * Math.PI / 180))) / 2);
56: }
57:
58: /// <summary>
59: /// Evaluate the distance between two points (pixel)
60: /// </summary>
61: /// <param name="lat1">Latitude 1</param>
62: /// <param name="lng1">Longitude 1</param>
63: /// <param name="lat2">Latitude 2</param>
64: /// <param name="lng2">Longitude 1</param>
65: /// <param name="zoom">Zoom level</param>
66: /// <returns></returns>
67: public static int CalcPixelDistance(double lat1, double lng1, double lat2, double lng2, int zoom)
68: {
69: var x1 = LonToX(lng1);
70: var y1 = LatToY(lat1);
71:
72: var x2 = LonToX(lng2);
73: var y2 = LatToY(lat2);
74:
75: return (int)Math.Sqrt(Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2)) >> (21 - zoom);
76: }
77: }
1: public interface IClusterProcessor
2: {
3: /// <summary>
4: /// Cluster a list of items
5: /// </summary>
6: /// <typeparam name="E"></typeparam>
7: /// <param name="items">The list of items to cluster</param>
8: /// <param name="zoom">The zoom of the map</param>
9: /// <param name="pixelThreshold">Pixel threshold distance</param>
10: /// <param name="threshold">Threshold in meters for items in the same place</param>
11: /// <returns></returns>
12: ClusterResult<E> DoCluster<E>(IEnumerable<E> items, int zoom, int pixelThreshold = 40, int threshold = 5) where E : IGeolocalizable;
13: }
1: public interface IGeolocalizable
2: {
3: double Latitude { get; set; }
4:
5: double Longitude { get; set; }
6: }
1: public class ClusterResult<T>
2: where T : IGeolocalizable
3: {
4: public IEnumerable<T> Items { get; set; }
5:
6: public IEnumerable<Cluster<T>> Clusters { get; set; }
7: }
1: public class Cluster<T> : IGeolocalizable
2: {
3: public double Latitude { get; set; }
4:
5: public double Longitude { get; set; }
6:
7: public IEnumerable<T> Items { get; set; }
8:
9: public bool IsClusterInOnePoint { get; set; }
10:
11: public object ToJson()
12: {
13: return new
14: {
15: lat = Latitude,
16: lng = Longitude,
17: itemsInCluster = Items.Count(),
18: isClusterInOnePoint = IsClusterInOnePoint
19: };
20: }
21: }
1: public ClusterResult<E> DoCluster<E>(IEnumerable<E> items, int zoom, int pixelThreshold = 40, int threshold = 5)
2: where E : IGeolocalizable
3: {
4: var itemsWithoutCluster = new List<E>();
5: var clusters = new List<Cluster<E>>();
6: var list = items.ToList();
7: var thresholdInMiles = GeolocalizationUtils.ConvertFromMetersToMiles(threshold);
8:
9: while (list.Count > 0)
10: {
11: var item = list[0];
12: list.Remove(item);
13: if (item.Latitude <= 0 || item.Longitude <= 0)
14: continue;
15:
16: var count = 1;
17: var isClusterInOnePoint = true;
18: var clusteredItems = new List<E>();
19:
20: for (var i = 0; i < list.Count; i++)
21: {
22: var pixelDistance = GeolocalizationUtils.CalcPixelDistance(item.Latitude, item.Longitude, list[i].Latitude, list[i].Longitude, zoom);
23:
24: if (pixelThreshold > pixelDistance)
25: {
26: if (isClusterInOnePoint)
27: {
28: var d = GeolocalizationUtils.CalcDistance(item.Latitude, item.Longitude, list[i].Latitude, list[i].Longitude);
29: isClusterInOnePoint = d <= thresholdInMiles;
30: }
31:
32: clusteredItems.Add(item);
33: count++;
34: list.RemoveAt(i);
35: i--;
36: }
37: }
38:
39: if (count == 1) //No Cluster was found...
40: itemsWithoutCluster.Add(item);
41: else
42: clusters.Add(new Cluster<E>
43: {
44: Latitude = item.Latitude,
45: Longitude = item.Longitude,
46: IsClusterInOnePoint = isClusterInOnePoint,
47: Items = clusteredItems
48: });
49: }
50:
51: return new ClusterResult<E>
52: {
53: Clusters = clusters,
54: Items = itemsWithoutCluster
55: };
56: }
La lista que le debemos suministrar al método DoCluster debemos obtenerla de algún sitio, por ejemplo de una clase de servicio como se muestra a continuación:
1: public IEnumerable<User> GetByBounds(CoordsBound bound)
2: {
3: return repository.GetFilteredElements(u =>
4: (u.Longitude > bound.SouthWest.Longitude && u.Longitude < bound.NorthEast.Longitude) &&
5: (u.Latitude <= bound.NorthEast.Latitude && u.Latitude >= bound.SouthWest.Latitude));
6: }
1: public ActionResult GetGeolocalizedUsers(string northEast, string southWest, int zoom)
2: {
3: var coordsBound = CoordsBound.Parse(southWest, northEast);
4: IEnumerable<User> users = userService.GetByBounds(coordsBound);
5: var clusterResult = clusterProcessor.DoCluster(users, zoom, 40, 5);
6:
7: var result = new ArrayList();
8:
9: foreach (var user in clusterResult.Items)
10: {
11: result.Add(user.ToJson());
12: }
13:
14: foreach (var cluster in clusterResult.Clusters)
15: {
16: result.Add(cluster.ToJson());
17: }
18:
19: return Json(result);
20: }
Por ahora es suficiente, con esto quedaría solo proyectar la información en el mapa del lado del cliente lo cual queda fuera del alcance de esta entrada y que quizás en otro post comentemos.
Antes de finalizar comentar de otra técnica que se puede usar si no se quiere hacer clúster. Es posible obtener una serie de imágenes con la información que se quiere proyectar en el mapa y superponerlas a las imágenes del mapa que nos proporciona Google, esto se consigue trabajando con los “tiles” de Google Maps. Es de esta última forma como trabaja Panoramio pero esto lo mostraré en otro post, por ahora ya es algo para empezar y comenzar a divertirse con Google Maps o Bing (todo lo aquí explicado se aplica también a Bing).
Hector! Justo estoy trabajando en algo similar, tengo un dataset enorme (varias decenas de millones de registros) de tecnicos que reportan su posicion cada minuto; y se necesita reportar con umbrales variables de tiempo y diatancia para diferentes propositos.
ResponderEliminarPor ejemplo, en ciertos casos necesitas ver solo el recorrido del tecnico reportando puntos cada X metros (ignorando el resto) y/o cada Y minutos.
Estoy mirando el sorporte para spatial data en SQLServer 2008 con la esperanza de encontrar algo mas eficiente ahi, de lo contrario no me quedara otra que procesar todos los registros que entren en el marco de la consulta.
Un abrazo bro! Y sigue posteando!
Es raciel no????? sql server tiene buen soporte para datos geospatial y tiene muchos métodos implementados, de todas formas hay una librería q se llama SharpMap (o algo así) q tiene el mismo API (o bastante parecido, creo q es una recomendación estándar) q el de Sql Server, pero bueno... es cuestión de probar y tener suerte, jejeje, un abrazo h
ResponderEliminarpd: lo de postear lo tengo q retomar en breve, jajaja
Si! Soy yo. Sigue posteando, que de ultima no te perjudica, y te ayuda a poner las ideas mas en claro.
ResponderEliminarMira esto: http://channel9.msdn.com/Shows/HanselminutesOn9/Hanselminutes-on-9-Social-Networking-for-Developers-Part-1-Every-Developer-Needs-a-Blog
Yo hace tiempo quiero publicar algunas cosas, pero soy muy vago... pero ya me lo puse como objetivo.
Un abrazo, y sigue con el buen trabajo que estas haciendo!