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