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 }