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

Last change on this file since 13536 was 13536, checked in by stoecker, 6 years ago

add possibility to change map ids (see #14655), add overlay flag for imagery

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