source: josm/trunk/src/org/openstreetmap/josm/io/imagery/ImageryReader.java@ 11747

Last change on this file since 11747 was 11575, checked in by stoecker, 7 years ago

see #12313 - add best marking and show it in image preferences

  • Property svn:eol-style set to native
File size: 19.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io.imagery;
3
4import java.io.BufferedReader;
5import java.io.Closeable;
6import java.io.IOException;
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.HashMap;
10import java.util.List;
11import java.util.Map;
12import java.util.Objects;
13import java.util.Stack;
14
15import javax.xml.parsers.ParserConfigurationException;
16
17import org.openstreetmap.josm.Main;
18import org.openstreetmap.josm.data.imagery.ImageryInfo;
19import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
20import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
21import org.openstreetmap.josm.data.imagery.Shape;
22import org.openstreetmap.josm.io.CachedFile;
23import org.openstreetmap.josm.tools.HttpClient;
24import org.openstreetmap.josm.tools.JosmRuntimeException;
25import org.openstreetmap.josm.tools.LanguageInfo;
26import org.openstreetmap.josm.tools.MultiMap;
27import org.openstreetmap.josm.tools.Utils;
28import org.xml.sax.Attributes;
29import org.xml.sax.InputSource;
30import org.xml.sax.SAXException;
31import org.xml.sax.helpers.DefaultHandler;
32
33public class ImageryReader implements Closeable {
34
35 private final String source;
36 private CachedFile cachedFile;
37 private boolean fastFail;
38
39 private enum State {
40 INIT, // initial state, should always be at the bottom of the stack
41 IMAGERY, // inside the imagery element
42 ENTRY, // inside an entry
43 ENTRY_ATTRIBUTE, // note we are inside an entry attribute to collect the character data
44 PROJECTIONS, // inside projections block of an entry
45 MIRROR, // inside an mirror entry
46 MIRROR_ATTRIBUTE, // note we are inside an mirror attribute to collect the character data
47 MIRROR_PROJECTIONS, // inside projections block of an mirror entry
48 CODE,
49 BOUNDS,
50 SHAPE,
51 NO_TILE,
52 NO_TILESUM,
53 METADATA,
54 UNKNOWN, // element is not recognized in the current context
55 }
56
57 /**
58 * Constructs a {@code ImageryReader} from a given filename, URL or internal resource.
59 *
60 * @param source can be:<ul>
61 * <li>relative or absolute file name</li>
62 * <li>{@code file:///SOME/FILE} the same as above</li>
63 * <li>{@code http://...} a URL. It will be cached on disk.</li>
64 * <li>{@code resource://SOME/FILE} file from the classpath (usually in the current *.jar)</li>
65 * <li>{@code josmdir://SOME/FILE} file inside josm user data directory (since r7058)</li>
66 * <li>{@code josmplugindir://SOME/FILE} file inside josm plugin directory (since r7834)</li></ul>
67 */
68 public ImageryReader(String source) {
69 this.source = source;
70 }
71
72 /**
73 * Parses imagery source.
74 * @return list of imagery info
75 * @throws SAXException if any SAX error occurs
76 * @throws IOException if any I/O error occurs
77 */
78 public List<ImageryInfo> parse() throws SAXException, IOException {
79 Parser parser = new Parser();
80 try {
81 cachedFile = new CachedFile(source);
82 cachedFile.setFastFail(fastFail);
83 try (BufferedReader in = cachedFile
84 .setMaxAge(CachedFile.DAYS)
85 .setCachingStrategy(CachedFile.CachingStrategy.IfModifiedSince)
86 .getContentReader()) {
87 InputSource is = new InputSource(in);
88 Utils.parseSafeSAX(is, parser);
89 return parser.entries;
90 }
91 } catch (SAXException e) {
92 throw e;
93 } catch (ParserConfigurationException e) {
94 Main.error(e); // broken SAXException chaining
95 throw new SAXException(e);
96 }
97 }
98
99 private static class Parser extends DefaultHandler {
100 private StringBuilder accumulator = new StringBuilder();
101
102 private Stack<State> states;
103
104 private List<ImageryInfo> entries;
105
106 /**
107 * Skip the current entry because it has mandatory attributes
108 * that this version of JOSM cannot process.
109 */
110 private boolean skipEntry;
111
112 private ImageryInfo entry;
113 /** In case of mirror parsing this contains the mirror entry */
114 private ImageryInfo mirrorEntry;
115 private ImageryBounds bounds;
116 private Shape shape;
117 // language of last element, does only work for simple ENTRY_ATTRIBUTE's
118 private String lang;
119 private List<String> projections;
120 private MultiMap<String, String> noTileHeaders;
121 private MultiMap<String, String> noTileChecksums;
122 private Map<String, String> metadataHeaders;
123
124 @Override
125 public void startDocument() {
126 accumulator = new StringBuilder();
127 skipEntry = false;
128 states = new Stack<>();
129 states.push(State.INIT);
130 entries = new ArrayList<>();
131 entry = null;
132 bounds = null;
133 projections = null;
134 noTileHeaders = null;
135 noTileChecksums = null;
136 }
137
138 @Override
139 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
140 accumulator.setLength(0);
141 State newState = null;
142 switch (states.peek()) {
143 case INIT:
144 if ("imagery".equals(qName)) {
145 newState = State.IMAGERY;
146 }
147 break;
148 case IMAGERY:
149 if ("entry".equals(qName)) {
150 entry = new ImageryInfo();
151 skipEntry = false;
152 newState = State.ENTRY;
153 noTileHeaders = new MultiMap<>();
154 noTileChecksums = new MultiMap<>();
155 metadataHeaders = new HashMap<>();
156 String best = atts.getValue("eli-best");
157 if ("true".equals(best)) {
158 entry.setBestMarked(true);
159 }
160 }
161 break;
162 case MIRROR:
163 if (Arrays.asList(new String[] {
164 "type",
165 "url",
166 "min-zoom",
167 "max-zoom",
168 "tile-size",
169 }).contains(qName)) {
170 newState = State.MIRROR_ATTRIBUTE;
171 lang = atts.getValue("lang");
172 } else if ("projections".equals(qName)) {
173 projections = new ArrayList<>();
174 newState = State.MIRROR_PROJECTIONS;
175 }
176 break;
177 case ENTRY:
178 if (Arrays.asList(new String[] {
179 "name",
180 "id",
181 "type",
182 "description",
183 "default",
184 "url",
185 "eula",
186 "min-zoom",
187 "max-zoom",
188 "attribution-text",
189 "attribution-url",
190 "logo-image",
191 "logo-url",
192 "terms-of-use-text",
193 "terms-of-use-url",
194 "country-code",
195 "icon",
196 "date",
197 "tile-size",
198 "valid-georeference",
199 "epsg4326to3857Supported",
200 }).contains(qName)) {
201 newState = State.ENTRY_ATTRIBUTE;
202 lang = atts.getValue("lang");
203 } else if ("bounds".equals(qName)) {
204 try {
205 bounds = new ImageryBounds(
206 atts.getValue("min-lat") + ',' +
207 atts.getValue("min-lon") + ',' +
208 atts.getValue("max-lat") + ',' +
209 atts.getValue("max-lon"), ",");
210 } catch (IllegalArgumentException e) {
211 Main.trace(e);
212 break;
213 }
214 newState = State.BOUNDS;
215 } else if ("projections".equals(qName)) {
216 projections = new ArrayList<>();
217 newState = State.PROJECTIONS;
218 } else if ("mirror".equals(qName)) {
219 projections = new ArrayList<>();
220 newState = State.MIRROR;
221 mirrorEntry = new ImageryInfo();
222 } else if ("no-tile-header".equals(qName)) {
223 noTileHeaders.put(atts.getValue("name"), atts.getValue("value"));
224 newState = State.NO_TILE;
225 } else if ("no-tile-checksum".equals(qName)) {
226 noTileChecksums.put(atts.getValue("type"), atts.getValue("value"));
227 newState = State.NO_TILESUM;
228 } else if ("metadata-header".equals(qName)) {
229 metadataHeaders.put(atts.getValue("header-name"), atts.getValue("metadata-key"));
230 newState = State.METADATA;
231 }
232 break;
233 case BOUNDS:
234 if ("shape".equals(qName)) {
235 shape = new Shape();
236 newState = State.SHAPE;
237 }
238 break;
239 case SHAPE:
240 if ("point".equals(qName)) {
241 try {
242 shape.addPoint(atts.getValue("lat"), atts.getValue("lon"));
243 } catch (IllegalArgumentException e) {
244 Main.trace(e);
245 break;
246 }
247 }
248 break;
249 case PROJECTIONS:
250 case MIRROR_PROJECTIONS:
251 if ("code".equals(qName)) {
252 newState = State.CODE;
253 }
254 break;
255 default: // Do nothing
256 }
257 /**
258 * Did not recognize the element, so the new state is UNKNOWN.
259 * This includes the case where we are already inside an unknown
260 * element, i.e. we do not try to understand the inner content
261 * of an unknown element, but wait till it's over.
262 */
263 if (newState == null) {
264 newState = State.UNKNOWN;
265 }
266 states.push(newState);
267 if (newState == State.UNKNOWN && "true".equals(atts.getValue("mandatory"))) {
268 skipEntry = true;
269 }
270 }
271
272 @Override
273 public void characters(char[] ch, int start, int length) {
274 accumulator.append(ch, start, length);
275 }
276
277 @Override
278 public void endElement(String namespaceURI, String qName, String rqName) {
279 switch (states.pop()) {
280 case INIT:
281 throw new JosmRuntimeException("parsing error: more closing than opening elements");
282 case ENTRY:
283 if ("entry".equals(qName)) {
284 entry.setNoTileHeaders(noTileHeaders);
285 noTileHeaders = null;
286 entry.setNoTileChecksums(noTileChecksums);
287 noTileChecksums = null;
288 entry.setMetadataHeaders(metadataHeaders);
289 metadataHeaders = null;
290
291 if (!skipEntry) {
292 entries.add(entry);
293 }
294 entry = null;
295 }
296 break;
297 case MIRROR:
298 if (mirrorEntry != null && "mirror".equals(qName)) {
299 entry.addMirror(mirrorEntry);
300 mirrorEntry = null;
301 }
302 break;
303 case MIRROR_ATTRIBUTE:
304 if (mirrorEntry != null) {
305 switch(qName) {
306 case "type":
307 boolean found = false;
308 for (ImageryType type : ImageryType.values()) {
309 if (Objects.equals(accumulator.toString(), type.getTypeString())) {
310 mirrorEntry.setImageryType(type);
311 found = true;
312 break;
313 }
314 }
315 if (!found) {
316 mirrorEntry = null;
317 }
318 break;
319 case "url":
320 mirrorEntry.setUrl(accumulator.toString());
321 break;
322 case "min-zoom":
323 case "max-zoom":
324 Integer val = null;
325 try {
326 val = Integer.valueOf(accumulator.toString());
327 } catch (NumberFormatException e) {
328 val = null;
329 }
330 if (val == null) {
331 mirrorEntry = null;
332 } else {
333 if ("min-zoom".equals(qName)) {
334 mirrorEntry.setDefaultMinZoom(val);
335 } else {
336 mirrorEntry.setDefaultMaxZoom(val);
337 }
338 }
339 break;
340 case "tile-size":
341 Integer tileSize = null;
342 try {
343 tileSize = Integer.valueOf(accumulator.toString());
344 } catch (NumberFormatException e) {
345 tileSize = null;
346 }
347 if (tileSize == null) {
348 mirrorEntry = null;
349 } else {
350 entry.setTileSize(tileSize.intValue());
351 }
352 break;
353 default: // Do nothing
354 }
355 }
356 break;
357 case ENTRY_ATTRIBUTE:
358 switch(qName) {
359 case "name":
360 entry.setName(lang == null ? LanguageInfo.getJOSMLocaleCode(null) : lang, accumulator.toString());
361 break;
362 case "description":
363 entry.setDescription(lang, accumulator.toString());
364 break;
365 case "date":
366 entry.setDate(accumulator.toString());
367 break;
368 case "id":
369 entry.setId(accumulator.toString());
370 break;
371 case "type":
372 boolean found = false;
373 for (ImageryType type : ImageryType.values()) {
374 if (Objects.equals(accumulator.toString(), type.getTypeString())) {
375 entry.setImageryType(type);
376 found = true;
377 break;
378 }
379 }
380 if (!found) {
381 skipEntry = true;
382 }
383 break;
384 case "default":
385 switch (accumulator.toString()) {
386 case "true":
387 entry.setDefaultEntry(true);
388 break;
389 case "false":
390 entry.setDefaultEntry(false);
391 break;
392 default:
393 skipEntry = true;
394 }
395 break;
396 case "url":
397 entry.setUrl(accumulator.toString());
398 break;
399 case "eula":
400 entry.setEulaAcceptanceRequired(accumulator.toString());
401 break;
402 case "min-zoom":
403 case "max-zoom":
404 Integer val = null;
405 try {
406 val = Integer.valueOf(accumulator.toString());
407 } catch (NumberFormatException e) {
408 val = null;
409 }
410 if (val == null) {
411 skipEntry = true;
412 } else {
413 if ("min-zoom".equals(qName)) {
414 entry.setDefaultMinZoom(val);
415 } else {
416 entry.setDefaultMaxZoom(val);
417 }
418 }
419 break;
420 case "attribution-text":
421 entry.setAttributionText(accumulator.toString());
422 break;
423 case "attribution-url":
424 entry.setAttributionLinkURL(accumulator.toString());
425 break;
426 case "logo-image":
427 entry.setAttributionImage(accumulator.toString());
428 break;
429 case "logo-url":
430 entry.setAttributionImageURL(accumulator.toString());
431 break;
432 case "terms-of-use-text":
433 entry.setTermsOfUseText(accumulator.toString());
434 break;
435 case "terms-of-use-url":
436 entry.setTermsOfUseURL(accumulator.toString());
437 break;
438 case "country-code":
439 entry.setCountryCode(accumulator.toString());
440 break;
441 case "icon":
442 entry.setIcon(accumulator.toString());
443 break;
444 case "tile-size":
445 Integer tileSize = null;
446 try {
447 tileSize = Integer.valueOf(accumulator.toString());
448 } catch (NumberFormatException e) {
449 tileSize = null;
450 }
451 if (tileSize == null) {
452 skipEntry = true;
453 } else {
454 entry.setTileSize(tileSize.intValue());
455 }
456 break;
457 case "valid-georeference":
458 entry.setGeoreferenceValid(Boolean.parseBoolean(accumulator.toString()));
459 break;
460 case "epsg4326to3857Supported":
461 entry.setEpsg4326To3857Supported(Boolean.parseBoolean(accumulator.toString()));
462 break;
463 default: // Do nothing
464 }
465 break;
466 case BOUNDS:
467 entry.setBounds(bounds);
468 bounds = null;
469 break;
470 case SHAPE:
471 bounds.addShape(shape);
472 shape = null;
473 break;
474 case CODE:
475 projections.add(accumulator.toString());
476 break;
477 case PROJECTIONS:
478 entry.setServerProjections(projections);
479 projections = null;
480 break;
481 case MIRROR_PROJECTIONS:
482 mirrorEntry.setServerProjections(projections);
483 projections = null;
484 break;
485 case NO_TILE:
486 case NO_TILESUM:
487 case METADATA:
488 case UNKNOWN:
489 default:
490 // nothing to do for these or the unknown type
491 }
492 }
493 }
494
495 /**
496 * Sets whether opening HTTP connections should fail fast, i.e., whether a
497 * {@link HttpClient#setConnectTimeout(int) low connect timeout} should be used.
498 * @param fastFail whether opening HTTP connections should fail fast
499 * @see CachedFile#setFastFail(boolean)
500 */
501 public void setFastFail(boolean fastFail) {
502 this.fastFail = fastFail;
503 }
504
505 @Override
506 public void close() throws IOException {
507 Utils.close(cachedFile);
508 }
509}
Note: See TracBrowser for help on using the repository browser.