001 // License: GPL. For details, see LICENSE file.
002 package org.openstreetmap.josm.io.imagery;
003
004 import static org.openstreetmap.josm.tools.I18n.tr;
005 import static org.openstreetmap.josm.tools.Utils.equal;
006
007 import java.io.IOException;
008 import java.io.InputStream;
009 import java.util.ArrayList;
010 import java.util.Arrays;
011 import java.util.List;
012 import java.util.Stack;
013
014 import javax.xml.parsers.ParserConfigurationException;
015 import javax.xml.parsers.SAXParserFactory;
016
017 import org.openstreetmap.josm.data.imagery.ImageryInfo;
018 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
019 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
020 import org.openstreetmap.josm.data.imagery.Shape;
021 import org.openstreetmap.josm.io.MirroredInputStream;
022 import org.openstreetmap.josm.io.UTFInputStreamReader;
023 import org.xml.sax.Attributes;
024 import org.xml.sax.InputSource;
025 import org.xml.sax.SAXException;
026 import org.xml.sax.helpers.DefaultHandler;
027
028 public class ImageryReader {
029
030 private String source;
031
032 private enum State {
033 INIT, // initial state, should always be at the bottom of the stack
034 IMAGERY, // inside the imagery element
035 ENTRY, // inside an entry
036 ENTRY_ATTRIBUTE, // note we are inside an entry attribute to collect the character data
037 PROJECTIONS,
038 CODE,
039 BOUNDS,
040 SHAPE,
041 UNKNOWN, // element is not recognized in the current context
042 }
043
044 public ImageryReader(String source) {
045 this.source = source;
046 }
047
048 public List<ImageryInfo> parse() throws SAXException, IOException {
049 Parser parser = new Parser();
050 try {
051 SAXParserFactory factory = SAXParserFactory.newInstance();
052 factory.setNamespaceAware(true);
053 InputStream in = new MirroredInputStream(source);
054 InputSource is = new InputSource(UTFInputStreamReader.create(in, "UTF-8"));
055 factory.newSAXParser().parse(is, parser);
056 return parser.entries;
057 } catch (SAXException e) {
058 throw e;
059 } catch (ParserConfigurationException e) {
060 e.printStackTrace(); // broken SAXException chaining
061 throw new SAXException(e);
062 }
063 }
064
065 private static class Parser extends DefaultHandler {
066 private StringBuffer accumulator = new StringBuffer();
067
068 private Stack<State> states;
069
070 List<ImageryInfo> entries;
071
072 /**
073 * Skip the current entry because it has mandatory attributes
074 * that this version of JOSM cannot process.
075 */
076 boolean skipEntry;
077
078 ImageryInfo entry;
079 ImageryBounds bounds;
080 Shape shape;
081 List<String> projections;
082
083 @Override public void startDocument() {
084 accumulator = new StringBuffer();
085 skipEntry = false;
086 states = new Stack<State>();
087 states.push(State.INIT);
088 entries = new ArrayList<ImageryInfo>();
089 entry = null;
090 bounds = null;
091 projections = null;
092 }
093
094 @Override
095 public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException {
096 accumulator.setLength(0);
097 State newState = null;
098 switch (states.peek()) {
099 case INIT:
100 if (qName.equals("imagery")) {
101 newState = State.IMAGERY;
102 }
103 break;
104 case IMAGERY:
105 if (qName.equals("entry")) {
106 entry = new ImageryInfo();
107 skipEntry = false;
108 newState = State.ENTRY;
109 }
110 break;
111 case ENTRY:
112 if (Arrays.asList(new String[] {
113 "name",
114 "type",
115 "default",
116 "url",
117 "eula",
118 "min-zoom",
119 "max-zoom",
120 "attribution-text",
121 "attribution-url",
122 "logo-image",
123 "logo-url",
124 "terms-of-use-text",
125 "terms-of-use-url",
126 "country-code",
127 "icon",
128 }).contains(qName)) {
129 newState = State.ENTRY_ATTRIBUTE;
130 } else if (qName.equals("bounds")) {
131 try {
132 bounds = new ImageryBounds(
133 atts.getValue("min-lat") + "," +
134 atts.getValue("min-lon") + "," +
135 atts.getValue("max-lat") + "," +
136 atts.getValue("max-lon"), ",");
137 } catch (IllegalArgumentException e) {
138 break;
139 }
140 newState = State.BOUNDS;
141 } else if (qName.equals("projections")) {
142 projections = new ArrayList<String>();
143 newState = State.PROJECTIONS;
144 }
145 break;
146 case BOUNDS:
147 if (qName.equals("shape")) {
148 shape = new Shape();
149 newState = State.SHAPE;
150 }
151 break;
152 case SHAPE:
153 if (qName.equals("point")) {
154 try {
155 shape.addPoint(atts.getValue("lat"), atts.getValue("lon"));
156 } catch (IllegalArgumentException e) {
157 break;
158 }
159 }
160 break;
161 case PROJECTIONS:
162 if (qName.equals("code")) {
163 newState = State.CODE;
164 }
165 break;
166 }
167 /**
168 * Did not recognize the element, so the new state is UNKNOWN.
169 * This includes the case where we are already inside an unknown
170 * element, i.e. we do not try to understand the inner content
171 * of an unknown element, but wait till it's over.
172 */
173 if (newState == null) {
174 newState = State.UNKNOWN;
175 }
176 states.push(newState);
177 if (newState == State.UNKNOWN && equal(atts.getValue("mandatory"), "true")) {
178 skipEntry = true;
179 }
180 return;
181 }
182
183 @Override
184 public void characters(char[] ch, int start, int length) {
185 accumulator.append(ch, start, length);
186 }
187
188 @Override
189 public void endElement(String namespaceURI, String qName, String rqName) {
190 switch (states.pop()) {
191 case INIT:
192 throw new RuntimeException("parsing error: more closing than opening elements");
193 case ENTRY:
194 if (qName.equals("entry")) {
195 if (!skipEntry) {
196 entries.add(entry);
197 }
198 entry = null;
199 }
200 break;
201 case ENTRY_ATTRIBUTE:
202 if (qName.equals("name")) {
203 entry.setName(tr(accumulator.toString()));
204 } else if (qName.equals("type")) {
205 boolean found = false;
206 for (ImageryType type : ImageryType.values()) {
207 if (equal(accumulator.toString(), type.getUrlString())) {
208 entry.setImageryType(type);
209 found = true;
210 break;
211 }
212 }
213 if (!found) {
214 skipEntry = true;
215 }
216 } else if (qName.equals("default")) {
217 if (accumulator.toString().equals("true")) {
218 entry.setDefaultEntry(true);
219 } else if (accumulator.toString().equals("false")) {
220 entry.setDefaultEntry(false);
221 } else {
222 skipEntry = true;
223 }
224 } else if (qName.equals("url")) {
225 entry.setUrl(accumulator.toString());
226 } else if (qName.equals("eula")) {
227 entry.setEulaAcceptanceRequired(accumulator.toString());
228 } else if (qName.equals("min-zoom") || qName.equals("max-zoom")) {
229 Integer val = null;
230 try {
231 val = Integer.parseInt(accumulator.toString());
232 } catch(NumberFormatException e) {
233 val = null;
234 }
235 if (val == null) {
236 skipEntry = true;
237 } else {
238 if (qName.equals("min-zoom")) {
239 entry.setDefaultMinZoom(val);
240 } else {
241 entry.setDefaultMaxZoom(val);
242 }
243 }
244 } else if (qName.equals("attribution-text")) {
245 entry.setAttributionText(accumulator.toString());
246 } else if (qName.equals("attribution-url")) {
247 entry.setAttributionLinkURL(accumulator.toString());
248 } else if (qName.equals("logo-image")) {
249 entry.setAttributionImage(accumulator.toString());
250 } else if (qName.equals("logo-url")) {
251 entry.setAttributionImageURL(accumulator.toString());
252 } else if (qName.equals("terms-of-use-text")) {
253 entry.setTermsOfUseText(accumulator.toString());
254 } else if (qName.equals("terms-of-use-url")) {
255 entry.setTermsOfUseURL(accumulator.toString());
256 } else if (qName.equals("country-code")) {
257 entry.setCountryCode(accumulator.toString());
258 } else if (qName.equals("icon")) {
259 entry.setIcon(accumulator.toString());
260 } else {
261 }
262 break;
263 case BOUNDS:
264 entry.setBounds(bounds);
265 bounds = null;
266 break;
267 case SHAPE:
268 bounds.addShape(shape);
269 shape = null;
270 break;
271 case CODE:
272 projections.add(accumulator.toString());
273 break;
274 case PROJECTIONS:
275 entry.setServerProjections(projections);
276 projections = null;
277 break;
278 }
279 }
280 }
281 }