altoslib: Make sure AltosFlightSeries is filled in before use
[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_11;
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         private static String map_url(AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
52                 String format_string;
53                 int z = zoom;
54
55                 if (maptype == AltosMap.maptype_hybrid || maptype == AltosMap.maptype_satellite || maptype == AltosMap.maptype_terrain)
56                         format_string = "jpg";
57                 else
58                         format_string = "png32";
59
60                 for (int s = 1; s < scale; s <<= 1)
61                         z--;
62
63                 if (AltosVersion.has_google_maps_api_key())
64                         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",
65                                              center.lat, center.lon, z, px_size/scale, px_size/scale, scale, AltosMap.maptype_names[maptype], format_string, AltosVersion.google_maps_api_key);
66                 else
67                         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",
68                                              center.lat, center.lon, z, px_size/scale, px_size/scale, AltosMap.maptype_names[maptype], format_string);
69         }
70
71         public int status() {
72                 return status;
73         }
74
75         public synchronized void add_listener(AltosMapStoreListener listener) {
76                 if (!listeners.contains(listener))
77                         listeners.add(listener);
78                 listener.notify_store(this, status);
79         }
80
81         public synchronized void remove_listener(AltosMapStoreListener listener) {
82                 listeners.remove(listener);
83         }
84
85         private synchronized void notify_listeners(int status) {
86                 this.status = status;
87                 for (AltosMapStoreListener listener : listeners)
88                         listener.notify_store(this, status);
89         }
90
91         static Object   forbidden_lock = new Object();
92         static long     forbidden_time;
93         static boolean  forbidden_set;
94
95         private int fetch_url() {
96                 URL u;
97
98                 try {
99                         u = new URL(url);
100                 } catch (java.net.MalformedURLException e) {
101                         return AltosMapTile.bad_request;
102                 }
103
104                 byte[] data;
105                 URLConnection uc = null;
106                 try {
107                         uc = u.openConnection();
108                         String type = uc.getContentType();
109                         int contentLength = uc.getContentLength();
110                         if (uc instanceof HttpURLConnection) {
111                                 int response = ((HttpURLConnection) uc).getResponseCode();
112                                 switch (response) {
113                                 case HttpURLConnection.HTTP_FORBIDDEN:
114                                 case HttpURLConnection.HTTP_PAYMENT_REQUIRED:
115                                 case HttpURLConnection.HTTP_UNAUTHORIZED:
116                                         synchronized (forbidden_lock) {
117                                                 forbidden_time = System.nanoTime();
118                                                 forbidden_set = true;
119                                                 return AltosMapTile.forbidden;
120                                         }
121                                 }
122                         }
123                         InputStream in = new BufferedInputStream(uc.getInputStream());
124                         int bytesRead = 0;
125                         int offset = 0;
126                         data = new byte[contentLength];
127                         while (offset < contentLength) {
128                                 bytesRead = in.read(data, offset, data.length - offset);
129                                 if (bytesRead == -1)
130                                         break;
131                                 offset += bytesRead;
132                         }
133                         in.close();
134
135                         if (offset != contentLength)
136                                 return AltosMapTile.failed;
137
138                 } catch (IOException e) {
139                         return AltosMapTile.failed;
140                 }
141
142                 try {
143                         FileOutputStream out = new FileOutputStream(file);
144                         out.write(data);
145                         out.flush();
146                         out.close();
147                 } catch (FileNotFoundException e) {
148                         return AltosMapTile.bad_request;
149                 } catch (IOException e) {
150                         if (file.exists())
151                                 file.delete();
152                         return AltosMapTile.bad_request;
153                 }
154                 return AltosMapTile.fetched;
155         }
156
157         static Object   fetch_lock = new Object();
158
159         static final long       forbidden_interval = 60l * 1000l * 1000l * 1000l;
160         static final long       google_maps_ratelimit_ms = 1200;
161
162         static Object   fetcher_lock = new Object();
163
164         static LinkedList<AltosMapStore> waiting = new LinkedList<AltosMapStore>();
165         static LinkedList<AltosMapStore> running = new LinkedList<AltosMapStore>();
166
167         static final int concurrent_fetchers = 128;
168
169         static void start_fetchers() {
170                 while (!waiting.isEmpty() && running.size() < concurrent_fetchers) {
171                         AltosMapStore   s = waiting.remove();
172                         running.add(s);
173                         Thread lt = s.make_fetcher_thread();
174                         lt.start();
175                 }
176         }
177
178         void finish_fetcher() {
179                 synchronized(fetcher_lock) {
180                         running.remove(this);
181                         start_fetchers();
182                 }
183         }
184
185         void add_fetcher() {
186                 synchronized(fetcher_lock) {
187                         waiting.add(this);
188                         start_fetchers();
189                 }
190         }
191
192         class fetcher implements Runnable {
193
194                 public void run() {
195                         try {
196                                 if (file.exists()) {
197                                         notify_listeners(AltosMapTile.fetched);
198                                         return;
199                                 }
200
201                                 synchronized(forbidden_lock) {
202                                         if (forbidden_set && (System.nanoTime() - forbidden_time) < forbidden_interval) {
203                                                 notify_listeners(AltosMapTile.forbidden);
204                                                 return;
205                                         }
206                                 }
207
208                                 int new_status;
209
210                                 if (!AltosVersion.has_google_maps_api_key()) {
211                                         synchronized (fetch_lock) {
212                                                 long startTime = System.nanoTime();
213                                                 new_status = fetch_url();
214                                                 if (new_status == AltosMapTile.fetched) {
215                                                         long duration_ms = (System.nanoTime() - startTime) / 1000000;
216                                                         if (duration_ms < google_maps_ratelimit_ms) {
217                                                                 try {
218                                                                         Thread.sleep(google_maps_ratelimit_ms - duration_ms);
219                                                                 } catch (InterruptedException e) {
220                                                                         Thread.currentThread().interrupt();
221                                                                 }
222                                                         }
223                                                 }
224                                         }
225                                 } else {
226                                         new_status = fetch_url();
227                                 }
228                                 notify_listeners(new_status);
229                         } finally {
230                                 finish_fetcher();
231                         }
232                 }
233         }
234
235         private Thread make_fetcher_thread() {
236                 return new Thread(new fetcher());
237         }
238
239         private void fetch() {
240                 add_fetcher();
241         }
242
243         private AltosMapStore (String url, File file) {
244                 this.url = url;
245                 this.file = file;
246
247                 if (file.exists())
248                         status = AltosMapTile.fetched;
249                 else {
250                         status = AltosMapTile.fetching;
251                         fetch();
252                 }
253         }
254
255         public int hashCode() {
256                 return url.hashCode();
257         }
258
259         public boolean equals(Object o) {
260                 if (o == null)
261                         return false;
262
263                 if (!(o instanceof AltosMapStore))
264                         return false;
265
266                 AltosMapStore other = (AltosMapStore) o;
267                 return url.equals(other.url);
268         }
269
270         static HashMap<String,AltosMapStore> stores = new HashMap<String,AltosMapStore>();
271
272         public static AltosMapStore get(AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
273                 String url = map_url(center, zoom, maptype, px_size, scale);
274
275                 AltosMapStore   store;
276                 synchronized(stores) {
277                         if (stores.containsKey(url)) {
278                                 store = stores.get(url);
279                         } else {
280                                 store = new AltosMapStore(url, map_file(center, zoom, maptype, px_size, scale));
281                                 stores.put(url, store);
282                         }
283                 }
284                 return store;
285         }
286
287 }