();
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Data/Responses/MarkerInfoResponse.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using GoogleMaps.Net.Clustering.Data.Geometry;
4 |
5 | namespace GoogleMaps.Net.Clustering.Data.Responses
6 | {
7 | public class MarkerInfoResponse : ResponseBase
8 | {
9 | public string Id { get; set; }
10 | public string Content { get; set; }
11 | public int Type { get; set; }
12 | public double Lat { get; set; }
13 | public double Long { get; set; }
14 |
15 | public void BuildContent(MapPoint point)
16 | {
17 | if (point == null)
18 | {
19 | Content = "Marker could not be found";
20 | return;
21 | }
22 |
23 | Id = point.MarkerId.ToString();
24 | Type = point.MarkerType;
25 | Lat = point.Lat;
26 | Long = point.Long;
27 |
28 | var stringBuilder = new StringBuilder();
29 | stringBuilder.AppendLine("");
30 | stringBuilder.AppendFormat("Time: {0}
",DateTime.Now.ToString("HH:mm:ss"));
31 | stringBuilder.AppendFormat("Id: {0}
Type: {1}
", Id, Type);
32 | stringBuilder.AppendFormat("Lat: {0} Lon: {1}", point.Lat, point.Long);
33 | stringBuilder.AppendLine("
");
34 |
35 | Content = stringBuilder.ToString();
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Data/Responses/ResponseBase.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace GoogleMaps.Net.Clustering.Data.Responses
4 | {
5 | public abstract class ResponseBase
6 | {
7 | ///
8 | /// Server-side calculation elapsed time.
9 | ///
10 | public string Elapsed { get; set; }
11 |
12 | ///
13 | /// Operation result.
14 | ///
15 | public string OperationResult { get; set; }
16 |
17 | ///
18 | /// Error message.
19 | ///
20 | public string ErrorMessage { get; set; }
21 |
22 | ///
23 | /// Dev info.
24 | ///
25 | public string Debug { get; set; }
26 |
27 | ///
28 | /// Is data cached?
29 | ///
30 | [JsonProperty(Order = -2)] // makes it appear first in json response
31 | public bool? IsCached { get; set; }
32 |
33 | protected ResponseBase()
34 | {
35 | OperationResult = "1";
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Documents/Design/GoogleMapsClustering_SD_ver1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tihomir-kit/GoogleMaps.Net.Clustering/9ed96f748b792c584cd3691356dea553445288d6/GoogleMaps.Net.Clustering/Documents/Design/GoogleMapsClustering_SD_ver1.png
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Documents/Design/GoogleMapsClustering_SD_ver1.vsd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tihomir-kit/GoogleMaps.Net.Clustering/9ed96f748b792c584cd3691356dea553445288d6/GoogleMaps.Net.Clustering/Documents/Design/GoogleMapsClustering_SD_ver1.vsd
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Documents/Design/googlemaps-clustering-viewport_ver1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tihomir-kit/GoogleMaps.Net.Clustering/9ed96f748b792c584cd3691356dea553445288d6/GoogleMaps.Net.Clustering/Documents/Design/googlemaps-clustering-viewport_ver1.png
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Documents/Design/googlemaps-clustering-viewport_ver1.vsd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tihomir-kit/GoogleMaps.Net.Clustering/9ed96f748b792c584cd3691356dea553445288d6/GoogleMaps.Net.Clustering/Documents/Design/googlemaps-clustering-viewport_ver1.vsd
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Documents/FAQ.txt:
--------------------------------------------------------------------------------
1 | = Frequently Asked Questions =
2 |
3 | *Q. How do I get an overview of how this Google Maps Server-side clustering works?*
4 |
5 | A. Read my blog http://kunuk.wordpress.com/2011/11/05/google-map-server-side-clustering-with-asp-net/
6 |
7 | ---------------------------------
8 |
9 | *Q. What do I need to run this?*
10 |
11 | A. Visual Studio 2010 or later, .Net v4.
12 | For deployment the web.config is configured for IIS 7 or later.
13 |
14 | ---------------------------------
15 |
16 | *Q. Where is the latest version?*
17 |
18 | A. The latest version is in the SVN repository, the trunk.
19 | If you want the latest version you should do SVN checkout.
20 |
21 | ---------------------------------
22 |
23 | *Q. How many points can I use?*
24 |
25 | A. With current implementation, the upper limit should be around 300.000 where the delay is below 1 sec.
26 | The server running time with an average laptop was about 300 msec. for 300.000 points.
27 |
28 | It should be possible to increase the number of points and keep a delay below 1 sec.
29 | by implementing a spatial data structure such as range search but it would only help if you are zoomed in.
30 | If you are zoomed all the way out you will have to iterate all the points
31 | then you would have to rely on pre-clustering for time complexity improvement.
32 |
33 | ---------------------------------
34 |
35 | *Q. How do I control the grid size?*
36 |
37 | A. In the file mapclustering.js
38 | specific values are set for gridx and gridy in the settings.
39 |
40 | ---------------------------------
41 |
42 | *Q. How do I load the dataset?*
43 |
44 | A. By default the dataset are loaded from a csv file to the Application session.
45 | Look how it is done in {{{Kunukn.GooglemapsClustering.Web.Application_Start}}}
46 |
47 | The csv file is located at
48 | {{{GoogleMapsClustering/Kunukn.GooglemapsClustering.Web/AreaGMC/Files/Points.csv}}}
49 |
50 | The format of the csv file is
51 | longitude; latitude; point id in GUID format; type id.
52 |
53 | The point id and type id data in this example are dummy data.
54 | The longitude and latitude are parsed to double values.
55 | point id and type id are parsed as int values.
56 |
57 | There is also the possible to load from a serialized file programmatically but no example file is included.
58 | Look in the code {{{Kunukn.GoogleMaps.Net.Clustering.Utility.Dataset.LoadDatasetFromDatabase()}}}
59 |
60 | ---------------------------------
61 |
62 | *Q. How do I set the initial zoom-level and position?*
63 |
64 | A. In the file mapclustering.js
65 | those values are set in the settings area.
66 |
67 | ---------------------------------
68 |
69 | *Q. When does the clustering stops?*
70 |
71 | A. You can define at which zoom-level the clustering should stop.
72 | In the file mapclustering.js in the settings set a value for zoomlevelClusterStop.
73 |
74 | ---------------------------------
75 |
76 | *Q. What are the red grid lines used for?*
77 |
78 | A. I made them for debugging purpose, i.e. to make sure the clustering behaved as intented.
79 | By default there can only be either one mark inside a grid. Either a cluster-point or a single point.
80 |
81 | ---------------------------------
82 |
83 | *Q. How do I enable/disable the red gridlines?*
84 |
85 | A. In the file {{{mapclustering.js }}}
86 | set the showGridLines to false or disable it server-side
87 | setting {{{GmcGlobalKeySettings.config DoShowGridLinesInGoogleMap}}} to false.
88 | I recommend to disable it server-side which will reduce the json-data size for the webservice call.
89 |
90 | ---------------------------------
91 |
92 | *Q. How do I extend the viewport data send to the client?*
93 |
94 | A. Increase or decrease the value in
95 | {{{GmcGlobalKeySettings.config OuterGridExtend}}}
96 |
97 | ---------------------------------
98 |
99 | *Q. How do I control the cluster merging point?*
100 |
101 | A. Increase or decrease the value in
102 | {{{GmcGlobalKeySettings.config MergeWithin}}}
103 |
104 | ---------------------------------
105 |
106 | *Q. How do I set the minimum count of points level before a cluster is made?*
107 |
108 | A. Increase or decrease the value in
109 | {{{GmcGlobalKeySettings.config MinClusterSize}}}
110 |
111 | ---------------------------------
112 |
113 | *Q. Is the clustering broken if I change the window size?*
114 |
115 | A. The clustering might behave buggy if you increase the window size.
116 | A quick fix is to edit the property {{{Kunukn.GooglemapsClustering.Data.ClusterInfo.IsFilterData}}}
117 | increase to higher value e.g. {{{ZoomLevel>=7}}}. This property is also used for filtering data returned to client.
118 | if set to never filter, then too much data will be send back to the client when zoomed far in.
119 |
120 | ---------------------------------
121 |
122 | *Q. How did you get the idea for the clustering part?*
123 |
124 | A. Inspiration from
125 | http://www.crunchpanorama.com/ and
126 | http://code.google.com/intl/da-DK/apis/maps/articles/toomanymarkers.html
127 |
128 | ---------------------------------
129 |
130 | *Q. What are the webservices used?*
131 |
132 | A. There are 3 get REST services
133 |
134 | * *{{{gmc.svc/getmarkers}}}* - for all the markers.
135 | * *{{{gmc.svc/getmarkerinfo}}}* - for specific info for seletected marker.
136 | * *{{{gmc.svc/knn}}}* - for k-nearest neighbors.
137 |
138 | For usage examples see {{{IAjaxService}}} interface and {{{AjaxService}}} class
139 |
140 | ---------------------------------
141 |
142 | *Q. Who made the Google Maps search function in this example?*
143 |
144 | A. The example code is with inspiration from
145 | http://tech.cibul.net/geocode-with-google-maps-api-v3/
146 |
147 | ---------------------------------
148 |
149 | *Q. How many points are there in the demo example?*
150 |
151 | A. There are 130.530 points.
152 |
153 | ---------------------------------
154 |
155 | *Q. Where did you get the points?*
156 |
157 | A. The points are taken from the file cities1000 at http://download.geonames.org/export/dump/
158 |
159 | ---------------------------------
160 |
161 | *Q. How can I customize this to use most points and keep fast performance?*
162 |
163 | A.
164 | In the file: {{{GmcGlobalKeySettings.config}}}.
165 | Set {{{OuterGridExtend}}} to 0.
166 | Set {{{DoMergeGridIfCentroidsAreCloseToEachOther}}} to false.
167 | Set {{{DoShowGridLinesInGoogleMap}}} to false.
168 |
169 |
170 | In the mapclustering.js in the settings.
171 | set gridx and gridy to low numbers, e.g. 2 and 2.
172 | ---------------------------------
173 |
174 | *Q. How do I generate random data points?*
175 |
176 | A. Use the method: {{{Kunukn.GooglemapsClustering.TestConsole.Program.GenerateRandomDatasetToCSVFile()}}}
177 | to update information for how many points you want and in which area you want them.
178 |
179 | ---------------------------------
180 |
181 | *Q. What license type is this?*
182 |
183 | A. MIT License.
184 |
185 | ---------------------------------
186 |
187 | *Q. How much does it cost to use Google Maps?*
188 |
189 | A. Google knows the answer. Try this link https://developers.google.com/maps/faq
190 |
191 | ---------------------------------
192 |
193 | *Q. What is the complexity of time and space?*
194 |
195 | A. Time complexity is on average(m*n) and space complexity is O(m*n)
196 | where n is the number of points used in total and m is the number of grids returned to the client.
197 | You are welcome to analyse it yourself :) I use hashset and hashmap to minimize the time complexity.
198 |
199 | Time complexity is ~ O(n^2) on worst case but extremely unlikely,
200 | happens if most centroids are merged with neighbor-centroids
201 | (not a likely scenario for common dataset, where data only exists at grid borderlines).
202 |
203 | ---------------------------------
204 |
205 | *Q. Why do the red gridlines look wierd when zoomed far out?*
206 |
207 | A. Because the world wraps and multiple areas are displayed for same latlon values.
208 | Some more logic can be applied to draw the grid lines properly for those conditions but has been left out.
209 | This happens only on zoom level below 3.
210 |
211 | ---------------------------------
212 |
213 | *Q. Who made this?*
214 |
215 | A. Kunuk Nykjaer spend the time in the weekends to make
216 | a stationary grid-based clustering after preliminary research on different clustering approach.
217 | On my time on the job I made a marker-clusterer version
218 | which I later discontinued because I felt the grid-based worked better.
219 | All the server-side clustering logic is made by Kunuk Nykjaer.
220 | The JS GUI-part is largely adapted and reworked by Kunuk Nykjaer and the inspiration came partly
221 | from JS-implementation by Jonas Olsen, Lars Gundersen and misc. http://stackoverflow.com/ pages.
222 | The server-side webservice definition (asmx files) are with inspiration from Lasse Rasch's implementations.
223 | The JS logic-part (cluster points updating, lines drawing) was made by Kunuk Nykjaer).
224 | The address search part done with inspiration
225 | from http://tech.cibul.net/geocode-with-google-maps-api-v3/.
226 | The cluster images are taken from Google's example images.
227 | The marker images are taken from http://mapicons.nicolasmollet.com/
228 |
229 | * Kunuk Nykjaer http://dk.linkedin.com/in/kunukn/
230 | * Jonas Olsen http://dk.linkedin.com/in/joolz/
231 | * Lars Gundersen http://dk.linkedin.com/in/larsgundersen/
232 | * Lasse Rasch http://dk.linkedin.com/pub/lasse-rasch/7/991/a10/
233 |
234 | ---------------------------------
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Documents/License.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | The Software MIT License only includes
4 | the clustering implementation details.
5 | The images and JS address search implementation details
6 | is not included in this copyright.
7 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Extensions/DataExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 | using System.Linq;
5 | using GoogleMaps.Net.Clustering.Data.Geometry;
6 |
7 | namespace GoogleMaps.Net.Clustering.Extensions
8 | {
9 | internal static class DataExtension
10 | {
11 | static readonly CultureInfo CultureEnUs = new CultureInfo("en-US");
12 | const string S = "G";
13 |
14 | const double Pi2 = Math.PI * 2;
15 | public const int RoundConvertError = 5;
16 |
17 | public static double Round(this double d)
18 | {
19 | return Math.Round(d, Numbers.Round);
20 | }
21 |
22 | public static void Normalize(this List list)
23 | {
24 | foreach (var p in list) p.Normalize();
25 | }
26 |
27 | // Distance
28 | public static double AbsLat(this double beg, double end)
29 | {
30 | double b = beg;
31 | double e = end;
32 | if (b > e)
33 | {
34 | e += LatLongInfo.MaxLatLength;
35 | }
36 |
37 | double diff = e - b;
38 | if (diff < 0 || diff > LatLongInfo.MaxLatLength)
39 | {
40 | throw new ApplicationException(string.Format("Error AbsLat beg: {0} end: {1}", beg, end));
41 | }
42 |
43 | return diff;
44 | }
45 |
46 | // Distance
47 | public static double AbsLon(this double beg, double end)
48 | {
49 | double b = beg;
50 | double e = end;
51 | if (b > e)
52 | {
53 | e += LatLongInfo.MaxLonLength;
54 | }
55 | double diff = e - b;
56 | if (diff < 0 || diff > LatLongInfo.MaxLonLength)
57 | {
58 | throw new ApplicationException(string.Format("Error AbsLon beg: {0} end: {1}", beg, end));
59 | }
60 |
61 | return diff;
62 | }
63 |
64 | // positive version of lat, lon
65 | public static double Pos(this double latlon)
66 | {
67 | if (latlon < LatLongInfo.MinLonValue || latlon > LatLongInfo.MaxLonValue)
68 | {
69 | throw new ApplicationException("Pos");
70 | }
71 |
72 | if (latlon < 0)
73 | {
74 | return latlon + LatLongInfo.MaxWorldLength;
75 | }
76 |
77 | return latlon;
78 | }
79 |
80 |
81 | // Lat or Lon
82 | public static double LatLongToDegree(this double latlon)
83 | {
84 | if (latlon < LatLongInfo.MinLonValue || latlon > LatLongInfo.MaxLonValue)
85 | {
86 | throw new ApplicationException("LatLonToDegree");
87 | }
88 |
89 | return (latlon + LatLongInfo.AngleConvert + LatLongInfo.MaxWorldLength) % LatLongInfo.MaxWorldLength;
90 | }
91 | public static double DegreeToLatLong(this double degree)
92 | {
93 | if (degree < 0 || degree > 360)
94 | {
95 | throw new ApplicationException("DegreeToLatLon");
96 | }
97 |
98 | return (degree - LatLongInfo.AngleConvert);
99 | }
100 |
101 | public static double LatLongToRadian(this double latlon)
102 | {
103 | if (latlon < LatLongInfo.MinLonValue || latlon > LatLongInfo.MaxLonValue)
104 | {
105 | throw new ApplicationException("LatLongToRadian");
106 | }
107 |
108 | var degree = LatLongToDegree(latlon);
109 | var radian = DegreeToRadian(degree);
110 | return radian;
111 | }
112 |
113 | public static double RadianNormalize(this double r)
114 | {
115 | if (r < -Pi2 || r > Pi2)
116 | {
117 | throw new ApplicationException("RadianNormalize");
118 | }
119 |
120 | var radian = (r + Pi2) % Pi2;
121 | if (radian < 0 || radian > Pi2)
122 | {
123 | throw new ApplicationException("RadianNormalize");
124 | }
125 |
126 | return radian;
127 | }
128 |
129 | public static double DegreeNormalize(this double d)
130 | {
131 | if (d < -360 || d > 360)
132 | {
133 | throw new ApplicationException("DegreeNormalize");
134 | }
135 |
136 | var degree = (d + 360) % 360;
137 | if (degree < 0 || degree > 360)
138 | {
139 | throw new ApplicationException("DegreeNormalize");
140 | }
141 |
142 | return degree;
143 | }
144 |
145 |
146 | public static double RadianToLatLong(this double r)
147 | {
148 | var radian = RadianNormalize(r);
149 | if (radian < 0 || radian > Pi2)
150 | {
151 | throw new ApplicationException("RadianToLatLong");
152 | }
153 |
154 | var degree = DegreeNormalize(RadianToDegree(radian) );
155 | var degreeRounded = Math.Round(degree, RoundConvertError);
156 | var latlon = DegreeToLatLong(degreeRounded);
157 | return latlon;
158 | }
159 |
160 | public static double RadianToDegree(this double radian)
161 | {
162 | if (radian < 0 || radian > Pi2)
163 | {
164 | throw new ApplicationException("RadianToDegree");
165 | }
166 |
167 | return (radian / Math.PI) * 180.0;
168 | }
169 | public static double DegreeToRadian(this double degree)
170 | {
171 | if (degree < 0 || degree > 360)
172 | {
173 | throw new ApplicationException("DegreeToRadian");
174 | }
175 |
176 | return (degree * Math.PI) / 180.0;
177 | }
178 |
179 |
180 | // ]-180;180]
181 | // lon wrap around at value -180 and 180, nb. -180 = 180
182 | public static double NormalizeLongitude(this double lon)
183 | {
184 | // naive version
185 | //while(normalized LatLongInfo.MaxLonValue)
194 | {
195 | var m = lon % LatLongInfo.MaxLonValue;
196 | normalized = LatLongInfo.MinLonValue + m;
197 | }
198 |
199 | return normalized;
200 | }
201 |
202 | // [-90;90]
203 | // -90 is south pole, 90 is north pole thus -90 != 90
204 | // no wrap, because google map dont wrap on lat
205 | public static double NormalizeLatitude(this double lat)
206 | {
207 | double normalized = lat;
208 | if (lat < LatLongInfo.MinLatValue)
209 | {
210 | //var m = lat % -LatLonInfo.MaxLatValue;
211 | //normalized = LatLonInfo.MaxLatValue + m;
212 | normalized = LatLongInfo.MinLatValue;
213 | }
214 | if (lat > LatLongInfo.MaxLatValue)
215 | {
216 | //var m = lat % LatLonInfo.MaxLatValue;
217 | //normalized = -LatLonInfo.MaxLatValue + m;
218 | normalized = LatLongInfo.MaxLatValue;
219 | }
220 |
221 | return normalized;
222 | }
223 |
224 | public static double ToDouble(this string s)
225 | {
226 | return double.Parse(s, NumberStyles.Float, NumberFormatInfo.InvariantInfo);
227 | }
228 |
229 | public static int ToInt(this string s)
230 | {
231 | return int.Parse(s);
232 | }
233 |
234 | public static string DoubleToString(this double d)
235 | {
236 | double rounded = Math.Round(d, Numbers.Round);
237 | return rounded.ToString(S, CultureEnUs);
238 | }
239 |
240 | public static string ListToString(this List list )
241 | {
242 | return list.Aggregate("", (a, b) => a + "[" + b + "]\n");
243 | }
244 |
245 | public static int[] ToNumbers(this string s)
246 | {
247 | if(string.IsNullOrWhiteSpace(s)) return new int[]{};
248 |
249 | var arr = s.Split(new [] {";"}, StringSplitOptions.RemoveEmptyEntries);
250 | var ints = new int[arr.Length];
251 | for (var i = 0; i < arr.Length; i++)
252 | {
253 | ints[i] = int.Parse(arr[i]);
254 | }
255 |
256 | return ints;
257 | }
258 | }
259 | }
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Extensions/IEnumerableExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace GoogleMaps.Net.Clustering.Extensions
6 | {
7 | internal static class IEnumerableExtension
8 | {
9 | public static bool HasAny(this IEnumerable enumerable)
10 | {
11 | return enumerable != null && enumerable.Any();
12 | }
13 |
14 | public static bool None(this IEnumerable enumerable)
15 | {
16 | return enumerable == null || !enumerable.Any();
17 | }
18 |
19 | public static void ForEach(this IEnumerable enumerable, Action action)
20 | {
21 | Array.ForEach(enumerable.ToArray(), action);
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/GoogleMaps.Net.Clustering.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {CEE02707-BBC1-4C90-BB96-66CA79BA48D6}
8 | Library
9 | Properties
10 | GoogleMaps.Net.Clustering
11 | GoogleMaps.Net.Clustering
12 | v4.5
13 | 512
14 |
15 |
16 |
17 | true
18 | full
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | pdbonly
27 | true
28 | bin\Release\
29 | TRACE
30 | prompt
31 | 4
32 |
33 |
34 |
35 | ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll
36 | True
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
99 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Infrastructure/IMemCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace GoogleMaps.Net.Clustering.Infrastructure
4 | {
5 | public interface IMemCache // TODO: make internal?
6 | {
7 | T Get(string key) where T : class;
8 |
9 | bool Add(string key, T objectToCache, TimeSpan timespan);
10 |
11 | void Set(string key, T objectToCache, TimeSpan timespan);
12 |
13 | object Remove(string key);
14 |
15 | bool Exists(string key);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Infrastructure/IPointCollection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using GoogleMaps.Net.Clustering.Data.Geometry;
4 |
5 | namespace GoogleMaps.Net.Clustering.Infrastructure
6 | {
7 | public interface IPointCollection
8 | {
9 | IList Get(string pointType = null);
10 |
11 | bool Add(IList points, TimeSpan timespan, string pointType = null);
12 |
13 | void Set(IList points, TimeSpan timespan, string pointType = null);
14 |
15 | object Remove(string pointType = null);
16 |
17 | bool Exists(string pointType = null);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Infrastructure/MemCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using GoogleMaps.Net.Clustering.Contract;
3 | using System.Runtime.Caching;
4 | using GoogleMaps.Net.Clustering.Services;
5 |
6 | namespace GoogleMaps.Net.Clustering.Infrastructure
7 | {
8 | ///
9 | /// http://msdn.microsoft.com/library/system.runtime.caching.memorycache.aspx
10 | ///
11 | public class MemCache : IMemCache // TODO: make internal?
12 | {
13 | private readonly ObjectCache _cache = MemoryCache.Default;
14 |
15 | public T Get(string key) where T : class
16 | {
17 | var cachedObj = _cache[key];
18 | if (cachedObj == null)
19 | return default(T);
20 |
21 | return cachedObj as T;
22 | }
23 |
24 | ///
25 | /// Only add if not already added
26 | /// return whether it was added
27 | ///
28 | public bool Add(string key, T objectToCache, TimeSpan timespan)
29 | {
30 | return _cache.Add(key, objectToCache, DateTime.Now.Add(timespan));
31 | }
32 |
33 | public void Set(string key, T objectToCache, TimeSpan timespan)
34 | {
35 | _cache.Set(key, objectToCache, DateTime.Now.Add(timespan));
36 | }
37 |
38 | public object Remove(string key)
39 | {
40 | return _cache.Remove(key);
41 | }
42 | public bool Exists(string key)
43 | {
44 | return _cache.Get(key) != null;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Infrastructure/PointCollection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using GoogleMaps.Net.Clustering.Contract;
4 | using System.Runtime.Caching;
5 | using GoogleMaps.Net.Clustering.Data;
6 | using GoogleMaps.Net.Clustering.Data.Geometry;
7 | using GoogleMaps.Net.Clustering.Services;
8 | using GoogleMaps.Net.Clustering.Utility;
9 |
10 | namespace GoogleMaps.Net.Clustering.Infrastructure
11 | {
12 | ///
13 | /// http://msdn.microsoft.com/library/system.runtime.caching.memorycache.aspx
14 | ///
15 | public class PointCollection : IPointCollection
16 | {
17 | protected readonly IMemCache _memCache;
18 | //public IMemCache MemCache => _memCache;
19 |
20 | protected readonly object _threadsafe = new object();
21 |
22 | public PointCollection()
23 | {
24 | _memCache = new MemCache();
25 | }
26 |
27 | public IList Get(string pointType = null)
28 | {
29 | var key = GetPointKey(pointType);
30 | return _memCache.Get>(key);
31 | }
32 |
33 | ///
34 | /// Only add if not already added
35 | /// return whether it was added
36 | ///
37 | public bool Add(IList points, TimeSpan timespan, string pointType = null)
38 | {
39 | var key = GetPointKey(pointType);
40 | return _memCache.Add(key, RandomizePointOrder(points), timespan);
41 | }
42 |
43 | public void Set(IList points, TimeSpan timespan, string pointType = null)
44 | {
45 | var key = GetPointKey(pointType);
46 | _memCache.Set(key, RandomizePointOrder(points), timespan);
47 | }
48 |
49 | public object Remove(string pointType = null)
50 | {
51 | var key = GetPointKey(pointType);
52 | return _memCache.Remove(key);
53 | }
54 |
55 | public bool Exists(string pointType = null)
56 | {
57 | var key = GetPointKey(pointType);
58 | return _memCache.Exists(key);
59 | }
60 |
61 | ///
62 | /// Not really that important, could be ommited. Used only for ensuring visual
63 | /// randomness of marker display when not all can be displayed on screen.
64 | ///
65 | /// Randomize order, when limit take is used for max marker display
66 | /// random locations are selected
67 | /// http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
68 | ///
69 | /// Points to randomize.
70 | /// Points in randomized order.
71 | private IList RandomizePointOrder(IList points)
72 | {
73 | lock (_threadsafe)
74 | {
75 | var rnd = new Random();
76 | for (var i = 0; i < points.Count; i++)
77 | {
78 | MapPoint tmp = points[i];
79 | int r = rnd.Next(points.Count);
80 | points[i] = points[r];
81 | points[r] = tmp;
82 | }
83 |
84 | return points;
85 | }
86 | }
87 |
88 | private static string GetPointKey(string pointType)
89 | {
90 | return String.Concat(CacheKeys.PointsDatabase, pointType);
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("GoogleMaps.Net.Clustering")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("GoogleMaps.Net.Clustering")]
13 | [assembly: AssemblyCopyright("Copyright © 2016")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("cee02707-bbc1-4c90-bb96-66ca79ba48d6")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Services/ClusterService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using GoogleMaps.Net.Clustering.Algorithm;
6 | using GoogleMaps.Net.Clustering.Contract;
7 | using GoogleMaps.Net.Clustering.Data;
8 | using GoogleMaps.Net.Clustering.Data.Configuration;
9 | using GoogleMaps.Net.Clustering.Data.Geometry;
10 | using GoogleMaps.Net.Clustering.Data.Params;
11 | using GoogleMaps.Net.Clustering.Data.Responses;
12 | using GoogleMaps.Net.Clustering.Extensions;
13 | using GoogleMaps.Net.Clustering.Infrastructure;
14 | using GoogleMaps.Net.Clustering.Services;
15 | using GoogleMaps.Net.Clustering.Utility;
16 |
17 | namespace GoogleMaps.Net.Clustering.Services
18 | {
19 | public class ClusterService : IClusterService
20 | {
21 | private readonly IPointCollection _pointCollection;
22 | private readonly IMemCache _memCache;
23 |
24 | public ClusterService(IPointCollection pointCollection)
25 | {
26 | _pointCollection = pointCollection;
27 | _memCache = new MemCache();
28 | }
29 |
30 | public ClusterMarkersResponse GetClusterMarkers(GetMarkersParams getParams)
31 | {
32 | // Decorate with elapsed time
33 | var sw = new Stopwatch();
34 | sw.Start();
35 | var reply = GetMarkersHelper(getParams);
36 | sw.Stop();
37 | reply.Elapsed = sw.Elapsed.ToString();
38 | return reply;
39 | }
40 |
41 | ///
42 | /// Read Through Cache
43 | ///
44 | ///
45 | ///
46 | private ClusterMarkersResponse GetMarkersHelper(GetMarkersParams getParams)
47 | {
48 | try
49 | {
50 | var neLat = getParams.NorthEastLatitude;
51 | var neLong = getParams.NorthEastLongitude;
52 | var swLat = getParams.SouthWestLatitude;
53 | var swLong = getParams.SouthWestLongitude;
54 | var zoomLevel = getParams.ZoomLevel;
55 | var filter = getParams.Filter ?? "";
56 |
57 | // values are validated there
58 | var markersInput = new MarkersInput(neLat, neLong, swLat, swLong, zoomLevel, filter);
59 |
60 | var grid = GridCluster.GetBoundaryExtended(markersInput);
61 | var cacheKeyHelper = string.Format("{0}_{1}_{2}", markersInput.Zoomlevel, markersInput.FilterHashCode(), grid.GetHashCode());
62 | var cacheKey = CacheKeys.GetMarkers(cacheKeyHelper.GetHashCode());
63 |
64 | var response = new ClusterMarkersResponse();
65 |
66 | markersInput.Viewport.ValidateLatLon(); // Validate google map viewport input (should be always valid)
67 | markersInput.Viewport.Normalize();
68 |
69 | // Get all points from memory
70 | IList points = _pointCollection.Get(getParams.PointType); // _pointsDatabase.GetPoints();
71 |
72 | // Filter points
73 | points = FilterUtil.FilterByType(
74 | points,
75 | new FilterData { TypeFilterExclude = markersInput.TypeFilterExclude }
76 | );
77 |
78 |
79 |
80 | // Create new instance for every ajax request with input all points and json data
81 | var clusterAlgo = new GridCluster(points, markersInput);
82 |
83 | var clusteringEnabled = markersInput.IsClusteringEnabled
84 | || GmcSettings.Get.AlwaysClusteringEnabledWhenZoomLevelLess > markersInput.Zoomlevel;
85 |
86 | // Clustering
87 | if (clusteringEnabled && markersInput.Zoomlevel < GmcSettings.Get.ZoomlevelClusterStop)
88 | {
89 | IList markers = clusterAlgo.RunCluster();
90 |
91 | response = new ClusterMarkersResponse
92 | {
93 | Markers = markers,
94 | Polylines = clusterAlgo.GetPolyLines()
95 | };
96 | }
97 | else
98 | {
99 | // If we are here then there are no clustering
100 | // The number of items returned is restricted to avoid json data overflow
101 | IList filteredDataset = FilterUtil.FilterDataByViewport(points, markersInput.Viewport);
102 | IList filteredDatasetMaxPoints = filteredDataset.Take(GmcSettings.Get.MaxMarkersReturned).ToList();
103 |
104 | response = new ClusterMarkersResponse
105 | {
106 | Markers = filteredDatasetMaxPoints,
107 | Polylines = clusterAlgo.GetPolyLines(),
108 | Mia = filteredDataset.Count - filteredDatasetMaxPoints.Count,
109 | };
110 | }
111 |
112 | // if client ne and sw is inside a specific grid box then cache the grid box and the result
113 | // next time test if ne and sw is inside the grid box and return the cached result
114 | if (GmcSettings.Get.CacheServices)
115 | _memCache.Set(cacheKey, response, TimeSpan.FromMinutes(10)); // cache data
116 |
117 | return response;
118 | }
119 | catch (Exception ex)
120 | {
121 | return new ClusterMarkersResponse
122 | {
123 | OperationResult = "0",
124 | ErrorMessage = string.Format("MapService says: exception {0}", ex.Message)
125 | };
126 | }
127 | }
128 |
129 |
130 | public MarkerInfoResponse GetMarkerInfo(int uid, string pointType = null)
131 | {
132 | // Decorate with elapsed time
133 | var sw = new Stopwatch();
134 | sw.Start();
135 | var reply = GetMarkerInfoResponse(uid);
136 | sw.Stop();
137 | reply.Elapsed = sw.Elapsed.ToString();
138 | return reply;
139 | }
140 |
141 |
142 | ///
143 | /// Read Through Cache
144 | ///
145 | ///
146 | ///
147 | private MarkerInfoResponse GetMarkerInfoResponse(int uid, string pointType = null)
148 | {
149 | try
150 | {
151 | var cacheKey = CacheKeys.GetMarkerInfo(uid);
152 | var reply = _memCache.Get(cacheKey);
153 | if (reply != null)
154 | {
155 | // return cached data
156 | reply.IsCached = true;
157 | return reply;
158 | }
159 |
160 | MapPoint marker = _pointCollection.Get(pointType).SingleOrDefault(i => i.MarkerId == uid);
161 |
162 | reply = new MarkerInfoResponse { Id = uid.ToString() };
163 | reply.BuildContent(marker);
164 |
165 | if (GmcSettings.Get.CacheServices)
166 | _memCache.Set(cacheKey, reply, TimeSpan.FromMinutes(10)); // cache data
167 |
168 | return reply;
169 | }
170 | catch (Exception ex)
171 | {
172 | return new MarkerInfoResponse
173 | {
174 | OperationResult = "0",
175 | ErrorMessage = string.Format("MapService says: Parsing error param: {0}", ex.Message)
176 | };
177 | }
178 | }
179 | }
180 | }
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Services/IClusterService.cs:
--------------------------------------------------------------------------------
1 | using GoogleMaps.Net.Clustering.Data.Params;
2 | using GoogleMaps.Net.Clustering.Data.Responses;
3 |
4 | namespace GoogleMaps.Net.Clustering.Services
5 | {
6 | public interface IClusterService
7 | {
8 | ClusterMarkersResponse GetClusterMarkers(GetMarkersParams getParams);
9 |
10 | MarkerInfoResponse GetMarkerInfo(int uid, string pointType = null);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Utility/ExceptionUtil.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace GoogleMaps.Net.Clustering.Utility
4 | {
5 | internal static class ExceptionUtil
6 | {
7 | public static string GetException(Exception ex)
8 | {
9 | return String.Format("Msg:{0}\nStacktrace:{1}\nInnerExc:{2}", ex.Message, ex.StackTrace, ex.InnerException);
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Utility/FilterUtil.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using GoogleMaps.Net.Clustering.Data;
4 | using GoogleMaps.Net.Clustering.Data.Algo;
5 | using GoogleMaps.Net.Clustering.Data.Configuration;
6 | using GoogleMaps.Net.Clustering.Data.Geometry;
7 | using GoogleMaps.Net.Clustering.Extensions;
8 |
9 | namespace GoogleMaps.Net.Clustering.Utility
10 | {
11 | internal static class FilterUtil
12 | {
13 | ///
14 | /// Supports threads
15 | ///
16 | ///
17 | ///
18 | ///
19 | public static IList FilterByType(IList points, FilterData filterData)
20 | {
21 | if (filterData.TypeFilterExclude.Count == GmcSettings.Get.MarkerTypes.Count)
22 | {
23 | // Filter all
24 | return new List(); // empty
25 | }
26 | if (filterData.TypeFilterExclude.None())
27 | {
28 | // Filter none
29 | return points;
30 | }
31 |
32 | // Filter data by typeFilter value
33 | return FilterByTypeHelper(points, filterData);
34 | }
35 |
36 | // O(n)
37 | public static IList FilterDataByViewport(IList points, Boundary viewport)
38 | {
39 | return points
40 | .Where(i => MathTool.IsInside(viewport, i))
41 | .ToList();
42 | }
43 |
44 | // O(n)
45 | private static IList FilterByTypeHelper(IList points, FilterData filterData)
46 | {
47 | return points
48 | .Where(p => filterData.TypeFilterExclude.Contains(p.MarkerType) == false)
49 | .ToList();
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/Utility/MathTool.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using GoogleMaps.Net.Clustering.Data.Algo;
3 | using GoogleMaps.Net.Clustering.Data.Geometry;
4 | using GoogleMaps.Net.Clustering.Extensions;
5 |
6 | namespace GoogleMaps.Net.Clustering.Utility
7 | {
8 | ///
9 | /// Author: Kunuk Nykjaer
10 | ///
11 | internal static class MathTool
12 | {
13 | const double Exp = 2; // 2=euclid, 1=manhatten
14 |
15 | // Minkowski dist
16 | // if lat lon precise dist is needed, use Haversine or similar formulas
17 | // this is approx calc for clustering, no precise dist is needed
18 | public static double Distance(MapPoint a, MapPoint b)
19 | {
20 | // lat lon wrap, values don't seem needed to be normalized to [0;1] for better distance calc
21 | var absx = LatLonDiff(a.X, b.X);
22 | var absy = LatLonDiff(a.Y, b.Y);
23 |
24 | return Math.Pow(Math.Pow(absx, Exp) +
25 | Math.Pow(Math.Abs(absy), Exp), 1.0 / Exp);
26 | }
27 |
28 | // O(1) while loop is maximum 2
29 | public static double LatLonDiff(double from, double to)
30 | {
31 | double difference = to - from;
32 | while (difference < -LatLongInfo.MaxLengthWrap) difference += LatLongInfo.MaxWorldLength;
33 | while (difference > LatLongInfo.MaxLengthWrap) difference -= LatLongInfo.MaxWorldLength;
34 | return Math.Abs(difference);
35 |
36 | //var differenceAngle = (to - from) % 180; //not working for -170 to 170
37 | //return Math.Abs(differenceAngle);
38 | }
39 |
40 | public static double Haversine(MapPoint p1, MapPoint p2)
41 | {
42 | return Haversine(p1.Y, p1.X, p2.Y, p2.X);
43 | }
44 |
45 | // http://en.wikipedia.org/wiki/Haversine_formula
46 | // Approx dist between two points on earth
47 | //public static double Haversine(double lat1, double lon1, double lat2, double lon2)
48 | //{
49 | // const int r = 6371; // km
50 | // var dLat = ToRadians(lat2 - lat1);
51 | // var dLon = ToRadians(lon2 - lon1);
52 | // lat1 = ToRadians(lat1);
53 | // lat2 = ToRadians(lat2);
54 |
55 | // var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
56 | // Math.Sin(dLon / 2) * Math.Sin(dLon / 2) * Math.Cos(lat1) * Math.Cos(lat2);
57 | // var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
58 | // var d = r * c;
59 | // return d;
60 | //}
61 | // http://en.wikipedia.org/wiki/Haversine_formula
62 | // Approx dist between two points on earth
63 | // http://rosettacode.org/wiki/Haversine_formula
64 | public static double Haversine(double lat1, double long1, double lat2, double long2)
65 | {
66 | const double r = 6372.8; // In kilometers
67 | var dLat = ToRadians(lat2 - lat1);
68 | var dLong = ToRadians(long2 - long1);
69 | lat1 = ToRadians(lat1);
70 | lat2 = ToRadians(lat2);
71 |
72 | var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
73 | Math.Sin(dLong / 2) * Math.Sin(dLong / 2) * Math.Cos(lat1) * Math.Cos(lat2);
74 | var c = 2 * Math.Asin(Math.Sqrt(a));
75 | var d = r * c;
76 | return d;
77 | }
78 |
79 | public static double ToRadians(double angle)
80 | {
81 | return Math.PI * angle / 180.0;
82 | }
83 |
84 | public static bool IsLowerThanLatMin(double d)
85 | {
86 | return d < LatLongInfo.MinLatValue;
87 | }
88 | public static bool IsGreaterThanLatMax(double d)
89 | {
90 | return d > LatLongInfo.MaxLatValue;
91 | }
92 |
93 | ///
94 | /// Lat Lon specific rect boundary check, is x,y inside boundary?
95 | ///
96 | ///
97 | ///
98 | ///
99 | ///
100 | ///
101 | ///
102 | /// ///
103 | /// ///
104 | ///
105 | public static bool IsInside(double minX, double minY, double maxX, double maxY, double x, double y, bool isInsideDetectedX, bool isInsideDetectedY)
106 | {
107 | // Normalize because of widen function, world wrapping might have occured
108 | // calc in positive value range only, nb. lon -170 = 10, lat -80 = 10
109 | var nMinX = minX.NormalizeLongitude();
110 | var nMaxX = maxX.NormalizeLongitude();
111 |
112 | var nMinY = minY.NormalizeLatitude();
113 | var nMaxY = maxY.NormalizeLatitude();
114 |
115 | var nX = x.NormalizeLongitude();
116 | var nY = y.NormalizeLatitude();
117 |
118 | bool isX = isInsideDetectedX; // skip checking?
119 | bool isY = isInsideDetectedY;
120 |
121 | if (!isInsideDetectedY)
122 | {
123 | // world wrap y
124 | if (nMinY > nMaxY)
125 | {
126 | //sign depended check, todo merge equal lines
127 | // - -
128 | if (nMaxY <= 0 && nMinY <= 0)
129 | {
130 | isY = nMinY <= nY && nY <= LatLongInfo.MaxLatValue || LatLongInfo.MinLatValue <= nY && nY <= nMaxY;
131 | }
132 | // + +
133 | else if (nMaxY >= 0 && nMinY >= 0)
134 | {
135 | isY = nMinY <= nY && nY <= LatLongInfo.MaxLatValue || LatLongInfo.MinLatValue <= nY && nY <= nMaxY;
136 | }
137 | // + -
138 | else
139 | {
140 | isY = nMinY <= nY && nY <= LatLongInfo.MaxLatValue || LatLongInfo.MinLatValue <= nY && nY <= nMaxY;
141 | }
142 | }
143 |
144 | else
145 | {
146 | // normal, no world wrap
147 | isY = nMinY <= nY && nY <= nMaxY;
148 | }
149 | }
150 |
151 | if (!isInsideDetectedX)
152 | {
153 | // world wrap x
154 | if (nMinX > nMaxX)
155 | {
156 | //sign depended check, todo merge equal lines
157 | // - -
158 | if (nMaxX <= 0 && nMinX <= 0)
159 | {
160 | isX = nMinX <= nX && nX <= LatLongInfo.MaxLonValue || LatLongInfo.MinLonValue <= nX && nX <= nMaxX;
161 | }
162 | // + +
163 | else if (nMaxX >= 0 && nMinX >= 0)
164 | {
165 | isX = nMinX <= nX && nX <= LatLongInfo.MaxLonValue || LatLongInfo.MinLonValue <= nX && nX <= nMaxX;
166 | }
167 | // + -
168 | else
169 | {
170 | isX = nMinX <= nX && nX <= LatLongInfo.MaxLonValue || LatLongInfo.MinLonValue <= nX && nX <= nMaxX;
171 | }
172 | }
173 | else
174 | {
175 | // normal, no world wrap
176 | isX = nMinX <= nX && nX <= nMaxX;
177 | }
178 | }
179 |
180 | return isX && isY;
181 | }
182 |
183 | public static bool IsInside(Boundary b, MapPoint p)
184 | {
185 | return IsInside(b.MinX, b.MinY, b.MaxX, b.MaxY, p.X, p.Y, false, false);
186 | }
187 |
188 | // used by zoom level and deciding the grid size, O(halfSteps)
189 | // O(halfSteps) ~ O(maxzoom) ~ O(k) ~ O(1)
190 | // Google Maps doubles or halves the view for 1 step zoom level change
191 | public static double Half(double d, int halfSteps)
192 | {
193 | // http://en.wikipedia.org/wiki/Decimal_degrees
194 | const double meter11 = 0.0001; //decimal degrees
195 |
196 | double half = d;
197 | for (int i = 0; i < halfSteps; i++)
198 | {
199 | half /= 2;
200 | }
201 |
202 | var halfRounded = Math.Round(half, 4);
203 | // avoid grid span less than this level
204 | return halfRounded < meter11 ? meter11 : halfRounded;
205 | }
206 |
207 | // Value x which is in range [a,b] is mapped to a new value in range [c;d]
208 | public static double Map(double x, double a, double b, double c, double d)
209 | {
210 | var r = (x - a) / (b - a) * (d - c) + c;
211 | return r;
212 | }
213 |
214 | // Grid location are stationary, this gives first left or lower grid line from current latOrLon
215 | public static double FloorLatLon(double latOrlon, double delta)
216 | {
217 | var floor = ((int)(latOrlon / delta)) * delta;
218 | if (latOrlon < 0) floor -= delta;
219 |
220 | return floor;
221 | }
222 |
223 | //
224 | public static bool IsValidLat(double latitude)
225 | {
226 | return LatLongInfo.MinLatValue <= latitude && latitude <= LatLongInfo.MaxLatValue;
227 | }
228 | public static bool IsValidLong(double longitude)
229 | {
230 | return LatLongInfo.MinLonValue <= longitude && longitude <= LatLongInfo.MaxLonValue;
231 | }
232 |
233 | // Value must be within a and b
234 | public static double Constrain(double x, double a, double b)
235 | {
236 | var r = Math.Max(a, Math.Min(x, b));
237 | return r;
238 | }
239 |
240 | // Value must be within latitude boundary
241 | public static double ConstrainLatitude(double x, double offset = 0)
242 | {
243 | var r = Math.Max(LatLongInfo.MinLatValue + offset, Math.Min(x, LatLongInfo.MaxLatValue - offset));
244 | return r;
245 | }
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/GoogleMaps.Net.Clustering/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Tihomir Kit
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GoogleMaps.Net.Clustering
2 | C# library for clustering map points. This library is very suitable for WebAPI (and similar-type) projects that don't depend on ASP.Net MVC.
3 |
4 |
5 | 
6 |
7 | **Original Lib**
8 | This is a fork of [Google Maps Server-side Clustering with C#
9 | ](https://github.com/kunukn/Google-Maps-Clustering-CSharp) repo. The guys made a fast, working implementation of Google Maps clustering for C#. However, they tightly coupled it with MVC and WebForms where all I needed was a simple C# way to crunch a bunch of simple map points and convert them into cluster points.
10 |
11 | **Installation**
12 | You can download the [GoogleMaps.Net.Clustering](https://www.nuget.org/packages/GoogleMaps.Net.Clustering/) package to install the latest version of GoogleMaps.Net.Clustering Lib.
13 |
14 | Sponsored by [Dovetail Technologies](http://www.dovetail.ie/).
15 |
16 | ## Usage
17 |
18 | This is an example of how to used cached clustering.
19 |
20 | ```cs
21 | public IList GetClusters(YourFilterObj filter)
22 | {
23 | var clusterPointsCacheKey = "somecachekey";
24 | var points = GetClusterPointCollection(clusterPointsCacheKey);
25 |
26 | var mapService = new ClusterService(points);
27 | var input = new GetMarkersParams()
28 | {
29 | NorthEastLatitude = filter.NorthEastLatitude,
30 | NorthEastLongitude = filter.NorthEastLongitude,
31 | SouthWestLatitude = filter.SouthWestLatitude,
32 | SouthWestLongitude = filter.SouthWestLongitude,
33 | ZoomLevel = filter.ZoomLevel,
34 | PointType = clusterPointsCacheKey
35 | };
36 |
37 | var markers = mapService.GetClusterMarkers(input);
38 |
39 | return markers.Markers;
40 | }
41 |
42 | private PointCollection GetClusterPointCollection(string clusterPointsCacheKey)
43 | {
44 | var points = new PointCollection();
45 | if (points.Exists(clusterPointsCacheKey))
46 | return points;
47 |
48 | var dbPoints = GetPoints(); // Get your points here
49 | var mapPoints = dbPoints.Select(p => new MapPoint() { X = p.X, Y = p.Y }).ToList();
50 | var cacheDuration = TimeSpan.FromHours(6);
51 | points.Set(mapPoints, cacheDuration, clusterPointsCacheKey);
52 |
53 | return points;
54 | }
55 | ```
56 |
57 | You will also need to add the following section to your `.conf` file (add it before the `connectionString` and/or `appSettings` section).
58 |
59 | ```xml
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | ```
86 |
--------------------------------------------------------------------------------
/cluster-map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tihomir-kit/GoogleMaps.Net.Clustering/9ed96f748b792c584cd3691356dea553445288d6/cluster-map.png
--------------------------------------------------------------------------------
/nuget-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tihomir-kit/GoogleMaps.Net.Clustering/9ed96f748b792c584cd3691356dea553445288d6/nuget-icon.png
--------------------------------------------------------------------------------