Version 1.9.4
[fw/altos] / altoslib / AltosMapStore.java
1 /*
2  * Copyright © 2014 Keith Packard <keithp@keithp.com>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
17  */
18
19 package org.altusmetrum.altoslib_14;
20
21 import java.io.*;
22 import java.net.*;
23 import java.util.*;
24
25 public class AltosMapStore {
26         String                                  url;
27         public File                             file;
28         LinkedList<AltosMapStoreListener>       listeners = new LinkedList<AltosMapStoreListener>();
29
30         int                                     status;
31
32         private static File map_file(AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
33                 double lat = center.lat;
34                 double lon = center.lon;
35                 char chlat = lat < 0 ? 'S' : 'N';
36                 char chlon = lon < 0 ? 'W' : 'E';
37
38                 if (lat < 0) lat = -lat;
39                 if (lon < 0) lon = -lon;
40                 String maptype_string = String.format("%s-", AltosMap.maptype_names[maptype]);
41                 String format_string;
42                 if (maptype == AltosMap.maptype_hybrid || maptype == AltosMap.maptype_satellite || maptype == AltosMap.maptype_terrain)
43                         format_string = "jpg";
44                 else
45                         format_string = "png";
46                 return new File(AltosPreferences.mapdir(),
47                                 String.format("map-%c%.6f,%c%.6f-%s%d%s.%s",
48                                               chlat, lat, chlon, lon, maptype_string, zoom, scale == 1 ? "" : String.format("-%d", scale), format_string));
49         }
50
51         public static String google_maps_api_key = null;
52
53         private static String google_map_url(AltosLatLon center, int zoom, int maptype, int px_size, int scale, String format_string) {
54                 return String.format("http://maps.google.com/maps/api/staticmap?center=%.6f,%.6f&zoom=%d&size=%dx%d&scale=%d&sensor=false&maptype=%s&format=%s&key=%s",
55                                      center.lat, center.lon, zoom, px_size, px_size, scale,
56                                      AltosMap.maptype_names[maptype], format_string, google_maps_api_key);
57         }
58
59         private static String altos_map_url(AltosLatLon center, int zoom, int maptype, int px_size, int scale, String format_string) {
60                 return String.format("https://maps.altusmetrum.org/cgi-bin/altos-map?lat=%.6f&lon=%.6f&zoom=%d",
61                                      center.lat, center.lon, zoom);
62         }
63
64         private static String map_url(AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
65                 String format_string;
66
67                 if (maptype == AltosMap.maptype_hybrid || maptype == AltosMap.maptype_satellite || maptype == AltosMap.maptype_terrain)
68                         format_string = "jpg";
69                 else
70                         format_string = "png32";
71
72                 for (int s = 1; s < scale; s <<= 1)
73                         zoom--;
74
75                 px_size /= scale;
76
77                 if (google_maps_api_key != null)
78                         return google_map_url(center, zoom, maptype, px_size, scale, format_string);
79                 else
80                         return altos_map_url(center, zoom, maptype, px_size, scale, format_string);
81         }
82
83         public synchronized int status() {
84                 return status;
85         }
86
87         public synchronized void add_listener(AltosMapStoreListener listener) {
88                 if (!listeners.contains(listener))
89                         listeners.add(listener);
90                 listener.notify_store(this, status);
91         }
92
93         public synchronized void remove_listener(AltosMapStoreListener listener) {
94                 listeners.remove(listener);
95         }
96
97         private synchronized void notify_listeners(int status) {
98                 this.status = status;
99                 for (AltosMapStoreListener listener : listeners)
100                         listener.notify_store(this, status);
101         }
102
103         private int fetch_url() {
104                 URL u;
105
106                 try {
107                         u = new URL(url);
108                 } catch (java.net.MalformedURLException e) {
109                         return AltosMapTile.bad_request;
110                 }
111
112                 byte[] data = null;
113                 URLConnection uc = null;
114
115                 int status = AltosMapTile.failed;
116                 int tries = 0;
117
118                 while (tries < 10 && status != AltosMapTile.fetched) {
119                         try {
120                                 uc = u.openConnection();
121                                 String type = uc.getContentType();
122                                 int contentLength = uc.getContentLength();
123                                 if (uc instanceof HttpURLConnection) {
124                                         int response = ((HttpURLConnection) uc).getResponseCode();
125                                         switch (response) {
126                                         case HttpURLConnection.HTTP_FORBIDDEN:
127                                         case HttpURLConnection.HTTP_PAYMENT_REQUIRED:
128                                         case HttpURLConnection.HTTP_UNAUTHORIZED:
129                                                 return AltosMapTile.forbidden;
130                                         }
131                                 }
132                                 InputStream in = new BufferedInputStream(uc.getInputStream());
133                                 int bytesRead = 0;
134                                 int offset = 0;
135                                 data = new byte[contentLength];
136                                 while (offset < contentLength) {
137                                         bytesRead = in.read(data, offset, data.length - offset);
138                                         if (bytesRead == -1)
139                                                 break;
140                                         offset += bytesRead;
141                                 }
142                                 in.close();
143
144                                 if (offset == contentLength)
145                                         status = AltosMapTile.fetched;
146                                 else
147                                         status = AltosMapTile.failed;
148
149                         } catch (IOException e) {
150                                 status = AltosMapTile.failed;
151                         }
152
153                         if (status != AltosMapTile.fetched) {
154                                 try {
155                                         Thread.sleep(100);
156                                 } catch (InterruptedException ie) {
157                                 }
158                                 tries++;
159                                 System.out.printf("Fetch failed, retrying %d\n", tries);
160                         }
161                 }
162
163                 if (status != AltosMapTile.fetched)
164                         return status;
165
166                 try {
167                         FileOutputStream out = new FileOutputStream(file);
168                         if (data != null)
169                                 out.write(data);
170                         out.flush();
171                         out.close();
172                 } catch (FileNotFoundException e) {
173                         return AltosMapTile.bad_request;
174                 } catch (IOException e) {
175                         if (file.exists())
176                                 file.delete();
177                         return AltosMapTile.bad_request;
178                 }
179                 return AltosMapTile.fetched;
180         }
181
182         static Object   fetch_lock = new Object();
183
184         static Object   fetcher_lock = new Object();
185
186         static LinkedList<AltosMapStore> waiting = new LinkedList<AltosMapStore>();
187         static LinkedList<AltosMapStore> running = new LinkedList<AltosMapStore>();
188
189         static int concurrent_fetchers() {
190                 if (google_maps_api_key == null)
191                         return 16;
192                 return 128;
193         }
194
195         static void start_fetchers() {
196                 while (!waiting.isEmpty() && running.size() < concurrent_fetchers()) {
197                         AltosMapStore   s = waiting.remove();
198                         running.add(s);
199                         Thread lt = s.make_fetcher_thread();
200                         lt.start();
201                 }
202         }
203
204         void finish_fetcher() {
205                 synchronized(fetcher_lock) {
206                         running.remove(this);
207                         start_fetchers();
208                 }
209         }
210
211         void add_fetcher() {
212                 synchronized(fetcher_lock) {
213                         waiting.add(this);
214                         start_fetchers();
215                 }
216         }
217
218         class fetcher implements Runnable {
219
220                 public void run() {
221                         try {
222                                 if (file.exists()) {
223                                         notify_listeners(AltosMapTile.fetched);
224                                         return;
225                                 }
226
227                                 int new_status;
228
229                                 new_status = fetch_url();
230
231                                 notify_listeners(new_status);
232                         } finally {
233                                 finish_fetcher();
234                         }
235                 }
236         }
237
238         private Thread make_fetcher_thread() {
239                 return new Thread(new fetcher());
240         }
241
242         private void fetch() {
243                 add_fetcher();
244         }
245
246         private AltosMapStore (String url, File file) {
247                 this.url = url;
248                 this.file = file;
249
250                 if (file.exists())
251                         status = AltosMapTile.fetched;
252                 else {
253                         status = AltosMapTile.fetching;
254                         fetch();
255                 }
256         }
257
258         public int hashCode() {
259                 return url.hashCode();
260         }
261
262         public boolean equals(Object o) {
263                 if (o == null)
264                         return false;
265
266                 if (!(o instanceof AltosMapStore))
267                         return false;
268
269                 AltosMapStore other = (AltosMapStore) o;
270                 return url.equals(other.url);
271         }
272
273         static HashMap<String,AltosMapStore> stores = new HashMap<String,AltosMapStore>();
274
275         public static AltosMapStore get(AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
276                 String url = map_url(center, zoom, maptype, px_size, scale);
277
278                 AltosMapStore   store;
279                 synchronized(stores) {
280                         if (stores.containsKey(url)) {
281                                 store = stores.get(url);
282                         } else {
283                                 store = new AltosMapStore(url, map_file(center, zoom, maptype, px_size, scale));
284                                 stores.put(url, store);
285                         }
286                 }
287                 return store;
288         }
289
290 }