1 /*
2  *  This file is part of the "GKMap".
3  *  GKMap project borrowed from GMap.NET (by radioman).
4  *
5  *  Copyright (C) 2009-2018 by radioman (email@radioman.lt).
6  *  This program is licensed under the FLAT EARTH License.
7  */
8 
9 using System;
10 using System.Collections.Generic;
11 using System.Diagnostics;
12 using System.IO;
13 using System.Net;
14 using System.Net.NetworkInformation;
15 using System.Security.Cryptography;
16 using System.Text;
17 using System.Threading;
18 using GKMap.CacheProviders;
19 using GKMap.MapProviders;
20 
21 namespace GKMap
22 {
23     public enum CacheType
24     {
25         GeocoderCache,
26         PlacemarkCache,
27         UrlCache,
28     }
29 
30     /// <summary>
31     /// maps manager
32     /// </summary>
33     public class GMaps : Singleton<GMaps>
34     {
35         private volatile bool fAbortCacheLoop;
36         private Thread fCacheThread;
37         private bool fCacheExists = true;
38         private volatile bool fCacheOnIdleRead = true;
39         private string fImageCacheLocation;
40         private int fReadingCache;
41         private readonly Queue<CacheQueueItem> fTileCacheQueue = new Queue<CacheQueueItem>();
42 
43         internal readonly AutoResetEvent WaitForCache = new AutoResetEvent(false);
44         internal volatile bool NoMapInstances = false;
45 
46         private static bool fCacheDelay;
47         private static string fCacheLocation;
48 
49 
50         /// <summary>
51         /// primary cache provider, by default: ultra fast SQLite
52         /// </summary>
53         public IPureImageCache ImageCache { get; private set; }
54 
55         public string ImageCacheLocation
56         {
57             get {
58                 return fImageCacheLocation;
59             }
60             set {
61                 fImageCacheLocation = value;
62 
63                 var cache = ImageCache as SQLitePureImageCache;
64                 if (cache != null) {
65                     cache.CacheLocation = value;
66                 }
67 
68                 fCacheDelay = true;
69             }
70         }
71 
72         public static string CacheLocation
73         {
74             get {
75                 if (string.IsNullOrEmpty(fCacheLocation)) {
76                     ResetCacheLocation();
77                 }
78 
79                 return fCacheLocation;
80             }
81             set {
82                 if (string.IsNullOrEmpty(value)) { // setting to null resets to default
83                     ResetCacheLocation();
84                 } else {
85                     fCacheLocation = value;
86                 }
87 
88                 if (fCacheDelay) {
89                     Instance.ImageCacheLocation = fCacheLocation;
90                 }
91             }
92         }
93 
94         /// <summary>
95         /// MemoryCache provider
96         /// </summary>
97         public readonly MemoryCache MemoryCache = new MemoryCache();
98 
99         /// <summary>
100         /// internal proxy for image management
101         /// </summary>
102         internal static PureImageProxy TileImageProxy;
103 
104 
GMaps()105         public GMaps()
106         {
107             if (Instance != null) {
108                 throw (new Exception("You have tried to create a new singleton class where you should have instanced it. Replace your \"new class()\" with \"class.Instance\""));
109             }
110 
111             ServicePointManager.DefaultConnectionLimit = 5;
112             InitCache();
113         }
114 
InitCache()115         private void InitCache()
116         {
117             ImageCache = new SQLitePureImageCache();
118 
119             string newCache = CacheLocation;
120             string oldCache = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + Path.DirectorySeparatorChar + "GKMap" + Path.DirectorySeparatorChar;
121 
122             // move database to non-roaming user directory
123             if (Directory.Exists(oldCache)) {
124                 try {
125                     if (Directory.Exists(newCache)) {
126                         Directory.Delete(oldCache, true);
127                     } else {
128                         Directory.Move(oldCache, newCache);
129                     }
130                     ImageCacheLocation = newCache;
131                 } catch (Exception ex) {
132                     ImageCacheLocation = oldCache;
133                     Trace.WriteLine("SQLitePureImageCache, moving data: " + ex);
134                 }
135             } else {
136                 ImageCacheLocation = newCache;
137             }
138         }
139 
140         private static readonly SHA1CryptoServiceProvider HashProvider = new SHA1CryptoServiceProvider();
141 
ConvertToHash(ref string s)142         private static void ConvertToHash(ref string s)
143         {
144             s = BitConverter.ToString(HashProvider.ComputeHash(Encoding.Unicode.GetBytes(s)));
145         }
146 
SaveContent(string url, CacheType type, string content)147         public void SaveContent(string url, CacheType type, string content)
148         {
149             if (!fCacheExists)
150                 return;
151 
152             try {
153                 ConvertToHash(ref url);
154 
155                 string dir = Path.Combine(fImageCacheLocation, type.ToString()) + Path.DirectorySeparatorChar;
156 
157                 // recreate dir
158                 if (!Directory.Exists(dir)) {
159                     Directory.CreateDirectory(dir);
160                 }
161 
162                 string file = dir + url + ".txt";
163 
164                 using (StreamWriter writer = new StreamWriter(file, false, Encoding.UTF8)) {
165                     writer.Write(content);
166                 }
167             } catch (Exception ex) {
168                 Debug.WriteLine("SaveContent: " + ex);
169             }
170         }
171 
GetContent(string url, CacheType type, TimeSpan stayInCache)172         public string GetContent(string url, CacheType type, TimeSpan stayInCache)
173         {
174             if (!fCacheExists)
175                 return string.Empty;
176 
177             string ret = null;
178 
179             try {
180                 ConvertToHash(ref url);
181 
182                 string dir = Path.Combine(fImageCacheLocation, type.ToString()) + Path.DirectorySeparatorChar;
183                 string file = dir + url + ".txt";
184 
185                 if (File.Exists(file)) {
186                     var writeTime = File.GetLastWriteTime(file);
187                     if (DateTime.Now - writeTime < stayInCache) {
188                         using (StreamReader r = new StreamReader(file, Encoding.UTF8)) {
189                             ret = r.ReadToEnd();
190                         }
191                     } else {
192                         File.Delete(file);
193                     }
194                 }
195             } catch (Exception ex) {
196                 ret = null;
197                 Debug.WriteLine("GetContent: " + ex);
198             }
199 
200             return ret;
201         }
202 
GetContent(string url, CacheType type)203         public string GetContent(string url, CacheType type)
204         {
205             return GetContent(url, type, TimeSpan.FromDays(88));
206         }
207 
ResetCacheLocation()208         private static void ResetCacheLocation()
209         {
210             string appDataLocation = Stuff.GetApplicationDataFolderPath();
211 
212             if (string.IsNullOrEmpty(appDataLocation)) {
213                 Instance.fCacheExists = false;
214             } else {
215                 CacheLocation = appDataLocation;
216             }
217         }
218 
219         /// <summary>
220         /// </summary>
Initialize(PureImageProxy imageProxy)221         public static void Initialize(PureImageProxy imageProxy)
222         {
223             TileImageProxy = imageProxy;
224 
225             // triggers dynamic SQLite loading, call this before you use SQLite for other reasons than caching maps
226             SQLitePureImageCache.Ping();
227         }
228 
229         /// <summary>
230         /// enqueue tile to cache
231         /// </summary>
232         /// <param name="task"></param>
EnqueueCacheTask(CacheQueueItem task)233         private void EnqueueCacheTask(CacheQueueItem task)
234         {
235             lock (fTileCacheQueue) {
236                 if (!fTileCacheQueue.Contains(task)) {
237                     Debug.WriteLine("EnqueueCacheTask: " + task);
238 
239                     fTileCacheQueue.Enqueue(task);
240 
241                     if (fCacheThread != null && fCacheThread.IsAlive) {
242                         WaitForCache.Set();
243                     } else if (fCacheThread == null || fCacheThread.ThreadState == System.Threading.ThreadState.Stopped || fCacheThread.ThreadState == System.Threading.ThreadState.Unstarted) {
244                         fCacheThread = null;
245                         fCacheThread = new Thread(CacheThreadLoop);
246                         fCacheThread.Name = "CacheEngine";
247                         fCacheThread.IsBackground = false;
248                         fCacheThread.Priority = ThreadPriority.Lowest;
249 
250                         fAbortCacheLoop = false;
251                         fCacheThread.Start();
252                     }
253                 }
254             }
255         }
256 
257         /// <summary>
258         /// immediately stops background tile caching, call it if you want fast exit the process
259         /// </summary>
CancelTileCaching()260         public void CancelTileCaching()
261         {
262             Debug.WriteLine("CancelTileCaching...");
263 
264             fAbortCacheLoop = true;
265             lock (fTileCacheQueue) {
266                 fTileCacheQueue.Clear();
267                 WaitForCache.Set();
268             }
269         }
270 
271         /// <summary>
272         /// live for cache
273         /// </summary>
CacheThreadLoop()274         private void CacheThreadLoop()
275         {
276             Debug.WriteLine("CacheEngine: start");
277 
278             bool startEvent = false;
279 
280             while (!fAbortCacheLoop) {
281                 try {
282                     CacheQueueItem? task = null;
283 
284                     int left;
285                     lock (fTileCacheQueue) {
286                         left = fTileCacheQueue.Count;
287                         if (left > 0) {
288                             task = fTileCacheQueue.Dequeue();
289                         }
290                     }
291 
292                     if (task.HasValue) {
293                         if (startEvent) {
294                             startEvent = false;
295                         }
296 
297                         // check if stream wasn't disposed somehow
298                         var taskVal = task.Value;
299                         if (taskVal.Img != null) {
300                             Debug.WriteLine("CacheEngine[" + left + "]: storing tile " + taskVal + ", " + taskVal.Img.Length / 1024 + "kB...");
301 
302                             if (ImageCache != null) {
303                                 if (fCacheOnIdleRead) {
304                                     while (Interlocked.Decrement(ref fReadingCache) > 0) {
305                                         Thread.Sleep(1000);
306                                     }
307                                 }
308                                 ImageCache.PutImageToCache(taskVal.Img, taskVal.Tile.Type, taskVal.Tile.Pos, taskVal.Tile.Zoom);
309                             }
310 
311                             taskVal.Clear();
312 
313                             // boost cache engine
314                             Thread.Sleep(333);
315                         } else {
316                             Debug.WriteLine("CacheEngineLoop: skip, tile disposed to early -> " + taskVal);
317                         }
318                     } else {
319                         if (!startEvent) {
320                             startEvent = true;
321                         }
322 
323                         if (fAbortCacheLoop || NoMapInstances || !WaitForCache.WaitOne(33333, false)) {
324                             break;
325                         }
326                     }
327                 } catch (AbandonedMutexException) {
328                     break;
329                 } catch (Exception ex) {
330                     Debug.WriteLine("CacheEngineLoop: " + ex);
331                 }
332             }
333             Debug.WriteLine("CacheEngine: stop");
334         }
335 
336         /// <summary>
337         /// gets image from tile server
338         /// </summary>
339         /// <param name="provider"></param>
340         /// <param name="pos"></param>
341         /// <param name="zoom"></param>
342         /// <returns></returns>
GetImageFrom(GMapProvider provider, GPoint pos, int zoom, out Exception result)343         public PureImage GetImageFrom(GMapProvider provider, GPoint pos, int zoom, out Exception result)
344         {
345             PureImage ret = null;
346             result = null;
347 
348             try {
349                 var rawTile = new RawTile(provider.DbId, pos, zoom);
350 
351                 // let't check memory first
352                 var m = MemoryCache.GetTileFromMemoryCache(rawTile);
353                 if (m != null && TileImageProxy != null) {
354                     ret = TileImageProxy.FromArray(m);
355                 }
356 
357                 if (ret == null) {
358                     if (ImageCache != null) {
359                         // hold writer for 5s
360                         if (fCacheOnIdleRead) {
361                             Interlocked.Exchange(ref fReadingCache, 5);
362                         }
363 
364                         ret = ImageCache.GetImageFromCache(provider.DbId, pos, zoom);
365                         if (ret != null) {
366                             MemoryCache.AddTileToMemoryCache(rawTile, ret.Data.GetBuffer());
367                             return ret;
368                         }
369                     }
370 
371                     ret = provider.GetTileImage(pos, zoom);
372                     // Enqueue Cache
373                     if (ret != null) {
374                         MemoryCache.AddTileToMemoryCache(rawTile, ret.Data.GetBuffer());
375 
376                         EnqueueCacheTask(new CacheQueueItem(rawTile, ret.Data.GetBuffer()));
377                     }
378                 }
379             } catch (Exception ex) {
380                 result = ex;
381                 ret = null;
382                 Debug.WriteLine("GetImageFrom: " + ex);
383             }
384 
385             return ret;
386         }
387 
PingNetwork(string hostNameOrAddress)388         public static bool PingNetwork(string hostNameOrAddress)
389         {
390             bool pingStatus;
391 
392             using (Ping p = new Ping()) {
393                 byte[] buffer = Encoding.ASCII.GetBytes("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
394                 int timeout = 4444; // 4s
395 
396                 try {
397                     PingReply reply = p.Send(hostNameOrAddress, timeout, buffer);
398                     pingStatus = (reply != null && reply.Status == IPStatus.Success);
399                 } catch (Exception) {
400                     pingStatus = false;
401                 }
402             }
403 
404             return pingStatus;
405         }
406     }
407 }
408