altosdroid: Fix offline map messages to match new meanings
[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_13;
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         static Object   forbidden_lock = new Object();
104         static long     forbidden_time;
105         static boolean  forbidden_set;
106         public static int forbidden_response;
107
108         private int fetch_url() {
109                 URL u;
110
111                 try {
112                         u = new URL(url);
113                 } catch (java.net.MalformedURLException e) {
114                         return AltosMapTile.bad_request;
115                 }
116
117                 byte[] data;
118                 URLConnection uc = null;
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                                         synchronized (forbidden_lock) {
130                                                 forbidden_time = System.nanoTime();
131                                                 forbidden_set = true;
132                                                 forbidden_response = response;
133                                                 return AltosMapTile.forbidden;
134                                         }
135                                 }
136                         }
137                         InputStream in = new BufferedInputStream(uc.getInputStream());
138                         int bytesRead = 0;
139                         int offset = 0;
140                         data = new byte[contentLength];
141                         while (offset < contentLength) {
142                                 bytesRead = in.read(data, offset, data.length - offset);
143                                 if (bytesRead == -1)
144                                         break;
145                                 offset += bytesRead;
146                         }
147                         in.close();
148
149                         if (offset != contentLength)
150                                 return AltosMapTile.failed;
151
152                 } catch (IOException e) {
153                         return AltosMapTile.failed;
154                 }
155
156                 try {
157                         FileOutputStream out = new FileOutputStream(file);
158                         out.write(data);
159                         out.flush();
160                         out.close();
161                 } catch (FileNotFoundException e) {
162                         return AltosMapTile.bad_request;
163                 } catch (IOException e) {
164                         if (file.exists())
165                                 file.delete();
166                         return AltosMapTile.bad_request;
167                 }
168                 return AltosMapTile.fetched;
169         }
170
171         static Object   fetch_lock = new Object();
172
173         static final long       forbidden_interval = 60l * 1000l * 1000l * 1000l;
174         static final long       google_maps_ratelimit_ms = 1200;
175
176         static Object   fetcher_lock = new Object();
177
178         static LinkedList<AltosMapStore> waiting = new LinkedList<AltosMapStore>();
179         static LinkedList<AltosMapStore> running = new LinkedList<AltosMapStore>();
180
181         static final int concurrent_fetchers = 128;
182
183         static void start_fetchers() {
184                 while (!waiting.isEmpty() && running.size() < concurrent_fetchers) {
185                         AltosMapStore   s = waiting.remove();
186                         running.add(s);
187                         Thread lt = s.make_fetcher_thread();
188                         lt.start();
189                 }
190         }
191
192         void finish_fetcher() {
193                 synchronized(fetcher_lock) {
194                         running.remove(this);
195                         start_fetchers();
196                 }
197         }
198
199         void add_fetcher() {
200                 synchronized(fetcher_lock) {
201                         waiting.add(this);
202                         start_fetchers();
203                 }
204         }
205
206         class fetcher implements Runnable {
207
208                 public void run() {
209                         try {
210                                 if (file.exists()) {
211                                         notify_listeners(AltosMapTile.fetched);
212                                         return;
213                                 }
214
215                                 synchronized(forbidden_lock) {
216                                         if (forbidden_set && (System.nanoTime() - forbidden_time) < forbidden_interval) {
217                                                 notify_listeners(AltosMapTile.forbidden);
218                                                 return;
219                                         }
220                                 }
221
222                                 int new_status;
223
224                                 new_status = fetch_url();
225
226                                 notify_listeners(new_status);
227                         } finally {
228                                 finish_fetcher();
229                         }
230                 }
231         }
232
233         private Thread make_fetcher_thread() {
234                 return new Thread(new fetcher());
235         }
236
237         private void fetch() {
238                 add_fetcher();
239         }
240
241         private AltosMapStore (String url, File file) {
242                 this.url = url;
243                 this.file = file;
244
245                 if (file.exists())
246                         status = AltosMapTile.fetched;
247                 else {
248                         status = AltosMapTile.fetching;
249                         fetch();
250                 }
251         }
252
253         public int hashCode() {
254                 return url.hashCode();
255         }
256
257         public boolean equals(Object o) {
258                 if (o == null)
259                         return false;
260
261                 if (!(o instanceof AltosMapStore))
262                         return false;
263
264                 AltosMapStore other = (AltosMapStore) o;
265                 return url.equals(other.url);
266         }
267
268         static HashMap<String,AltosMapStore> stores = new HashMap<String,AltosMapStore>();
269
270         public static AltosMapStore get(AltosLatLon center, int zoom, int maptype, int px_size, int scale) {
271                 String url = map_url(center, zoom, maptype, px_size, scale);
272
273                 AltosMapStore   store;
274                 synchronized(stores) {
275                         if (stores.containsKey(url)) {
276                                 store = stores.get(url);
277                         } else {
278                                 store = new AltosMapStore(url, map_file(center, zoom, maptype, px_size, scale));
279                                 stores.put(url, store);
280                         }
281                 }
282                 return store;
283         }
284
285 }