diff options
Diffstat (limited to 'main/src/cgeo/geocaching/files/GPXParser.java')
| -rw-r--r-- | main/src/cgeo/geocaching/files/GPXParser.java | 795 |
1 files changed, 795 insertions, 0 deletions
diff --git a/main/src/cgeo/geocaching/files/GPXParser.java b/main/src/cgeo/geocaching/files/GPXParser.java new file mode 100644 index 0000000..a0da184 --- /dev/null +++ b/main/src/cgeo/geocaching/files/GPXParser.java @@ -0,0 +1,795 @@ +package cgeo.geocaching.files; + +import cgeo.geocaching.R; +import cgeo.geocaching.cgBase; +import cgeo.geocaching.cgCache; +import cgeo.geocaching.cgLog; +import cgeo.geocaching.cgSearch; +import cgeo.geocaching.cgSettings; +import cgeo.geocaching.cgTrackable; +import cgeo.geocaching.cgeoapplication; +import cgeo.geocaching.connector.ConnectorFactory; +import cgeo.geocaching.enumerations.CacheSize; +import cgeo.geocaching.geopoint.Geopoint; + +import org.apache.commons.lang3.StringUtils; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; + +import android.os.Handler; +import android.sax.Element; +import android.sax.EndElementListener; +import android.sax.EndTextElementListener; +import android.sax.RootElement; +import android.sax.StartElementListener; +import android.util.Log; +import android.util.Xml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class GPXParser extends FileParser { + + private static final SimpleDateFormat formatSimple = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); // 2010-04-20T07:00:00Z + private static final SimpleDateFormat formatTimezone = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'.000'Z"); // 2010-04-20T01:01:03.000-04:00 + + private static final Pattern patternGeocode = Pattern.compile("([A-Z]{2}[0-9A-Z]+)", Pattern.CASE_INSENSITIVE); + private static final Pattern patternGuid = Pattern.compile(".*" + Pattern.quote("guid=") + "([0-9a-z\\-]+)", Pattern.CASE_INSENSITIVE); + private static final String[] nsGCList = new String[] { + "http://www.groundspeak.com/cache/1/1", // PQ 1.1 + "http://www.groundspeak.com/cache/1/0/1", // PQ 1.0.1 + "http://www.groundspeak.com/cache/1/0", // PQ 1.0 + }; + + private static final String GSAK_NS = "http://www.gsak.net/xmlv1/5"; + + private static cgeoapplication app = null; + private int listId = 1; + private cgSearch search = null; + final protected String namespace; + final private String version; + private Handler handler = null; + + private cgCache cache = new cgCache(); + private cgTrackable trackable = new cgTrackable(); + private cgLog log = new cgLog(); + + private String type = null; + private String sym = null; + private String name = null; + private String cmt = null; + private String desc = null; + protected String[] userData = new String[5]; // take 5 cells, that makes indexing 1..4 easier + + private final class UserDataListener implements EndTextElementListener { + private int index; + + public UserDataListener(int index) { + this.index = index; + } + + @Override + public void end(String user) { + userData[index] = validate(user); + } + } + + private static final class CacheAttributeTranslator { + // List of cache attributes matching IDs used in GPX files. + // The ID is represented by the position of the String in the array. + // Strings are not used as text but as resource IDs of strings, just to be aware of changes + // made in strings.xml which then will lead to compile errors here and not to runtime errors. + private static final int[] CACHE_ATTRIBUTES = { + -1, // 0 + R.string.attribute_dogs_yes, // 1 + R.string.attribute_fee_yes, // 2 + R.string.attribute_rappelling_yes, // 3 + R.string.attribute_boat_yes, // 4 + R.string.attribute_scuba_yes, // 5 + R.string.attribute_kids_yes, // 6 + R.string.attribute_onehour_yes, // 7 + R.string.attribute_scenic_yes, // 8 + R.string.attribute_hiking_yes, // 9 + R.string.attribute_climbing_yes, // 10 + R.string.attribute_wading_yes, // 11 + R.string.attribute_swimming_yes, // 12 + R.string.attribute_available_yes, // 13 + R.string.attribute_night_yes, // 14 + R.string.attribute_winter_yes, // 15 + -1, // 16 + R.string.attribute_poisonoak_yes, // 17 + R.string.attribute_dangerousanimals_yes, // 18 + R.string.attribute_ticks_yes, // 19 + R.string.attribute_mine_yes, // 20 + R.string.attribute_cliff_yes, // 21 + R.string.attribute_hunting_yes, // 22 + R.string.attribute_danger_yes, // 23 + R.string.attribute_wheelchair_yes, // 24 + R.string.attribute_parking_yes, // 25 + R.string.attribute_public_yes, // 26 + R.string.attribute_water_yes, // 27 + R.string.attribute_restrooms_yes, // 28 + R.string.attribute_phone_yes, // 29 + R.string.attribute_picnic_yes, // 30 + R.string.attribute_camping_yes, // 31 + R.string.attribute_bicycles_yes, // 32 + R.string.attribute_motorcycles_yes, // 33 + R.string.attribute_quads_yes, // 34 + R.string.attribute_jeeps_yes, // 35 + R.string.attribute_snowmobiles_yes, // 36 + R.string.attribute_horses_yes, // 37 + R.string.attribute_campfires_yes, // 38 + R.string.attribute_thorn_yes, // 39 + R.string.attribute_stealth_yes, // 40 + R.string.attribute_stroller_yes, // 41 + R.string.attribute_firstaid_yes, // 42 + R.string.attribute_cow_yes, // 43 + R.string.attribute_flashlight_yes, // 44 + R.string.attribute_landf_yes, // 45 + R.string.attribute_rv_yes, // 46 + R.string.attribute_field_puzzle_yes, // 47 + R.string.attribute_uv_yes, // 48 + R.string.attribute_snowshoes_yes, // 49 + R.string.attribute_skiis_yes, // 50 + R.string.attribute_s_tool_yes, // 51 + R.string.attribute_nightcache_yes, // 52 + R.string.attribute_parkngrab_yes, // 53 + R.string.attribute_abandonedbuilding_yes, // 54 + R.string.attribute_hike_short_yes, // 55 + R.string.attribute_hike_med_yes, // 56 + R.string.attribute_hike_long_yes, // 57 + R.string.attribute_fuel_yes, // 58 + R.string.attribute_food_yes, // 59 + R.string.attribute_wirelessbeacon_yes, // 60 + R.string.attribute_partnership_yes, // 61 + R.string.attribute_seasonal_yes, // 62 + R.string.attribute_touristok_yes, // 63 + R.string.attribute_treeclimbing_yes, // 64 + R.string.attribute_frontyard_yes, // 65 + R.string.attribute_teamwork_yes, // 66 + }; + private static final String YES = "_yes"; + private static final String NO = "_no"; + private static final Pattern BASENAME_PATTERN = Pattern.compile("^.*attribute_(.*)(_yes|_no)"); + + // map GPX-Attribute-Id to baseName + public static String getBaseName(final int id) { + // get String out of array + if (CACHE_ATTRIBUTES.length <= id) { + return null; + } + final int stringId = CACHE_ATTRIBUTES[id]; + if (stringId == -1) { + return null; // id not found + } + // get text for string + String stringName = null; + try { + stringName = app.getResources().getResourceName(stringId); + } catch (NullPointerException e) { + return null; + } + if (stringName == null) { + return null; + } + // cut out baseName + final Matcher m = BASENAME_PATTERN.matcher(stringName); + if (!m.matches()) { + return null; + } + return m.group(1); + } + + // @return baseName + "_yes" or "_no" e.g. "food_no" or "uv_yes" + public static String getInternalId(final int attributeId, final boolean active) { + final String baseName = CacheAttributeTranslator.getBaseName(attributeId); + if (baseName == null) { + return null; + } + return baseName + (active ? YES : NO); + } + } + + protected GPXParser(cgeoapplication appIn, int listIdIn, cgSearch searchIn, String namespaceIn, String versionIn) { + app = appIn; + listId = listIdIn; + search = searchIn; + namespace = namespaceIn; + version = versionIn; + } + + private static Date parseDate(String inputUntrimmed) throws ParseException { + final String input = inputUntrimmed.trim(); + if (input.length() >= 3 && input.charAt(input.length() - 3) == ':') { + final String removeColon = input.substring(0, input.length() - 3) + input.substring(input.length() - 2); + return formatTimezone.parse(removeColon); + } + return formatSimple.parse(input); + } + + public UUID parse(final InputStream stream, Handler handlerIn) { + handler = handlerIn; + + final RootElement root = new RootElement(namespace, "gpx"); + final Element waypoint = root.getChild(namespace, "wpt"); + + // waypoint - attributes + waypoint.setStartElementListener(new StartElementListener() { + + @Override + public void start(Attributes attrs) { + try { + if (attrs.getIndex("lat") > -1 && attrs.getIndex("lon") > -1) { + cache.coords = new Geopoint(new Double(attrs.getValue("lat")), + new Double(attrs.getValue("lon"))); + } + } catch (Exception e) { + Log.w(cgSettings.tag, "Failed to parse waypoint's latitude and/or longitude."); + } + } + }); + + // waypoint + waypoint.setEndElementListener(new EndElementListener() { + + @Override + public void end() { + if (StringUtils.isBlank(cache.geocode)) { + // try to find geocode somewhere else + findGeoCode(name); + findGeoCode(desc); + findGeoCode(cmt); + } + + if (StringUtils.isNotBlank(cache.geocode) + && cache.coords != null + && ((type == null && sym == null) + || (type != null && type.indexOf("geocache") > -1) + || (sym != null && sym.indexOf("geocache") > -1))) { + fixCache(cache); + cache.reason = listId; + cache.detailed = true; + + if (StringUtils.isBlank(cache.personalNote)) { + StringBuilder buffer = new StringBuilder(); + for (int i = 0; i < userData.length; i++) { + if (StringUtils.isNotBlank(userData[i])) { + buffer.append(' ').append(userData[i]); + } + } + String note = buffer.toString().trim(); + if (StringUtils.isNotBlank(note)) { + cache.personalNote = note; + } + } + + app.addCacheToSearch(search, cache); + } + + showFinishedMessage(handler, search); + + resetCache(); + } + }); + + // waypoint.time + waypoint.getChild(namespace, "time").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + try { + cache.hidden = parseDate(body); + } catch (Exception e) { + Log.w(cgSettings.tag, "Failed to parse cache date: " + e.toString()); + } + } + }); + + // waypoint.name + waypoint.getChild(namespace, "name").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + name = body; + + final String content = body.trim(); + cache.name = content; + + findGeoCode(cache.name); + findGeoCode(cache.description); + } + }); + + // waypoint.desc + waypoint.getChild(namespace, "desc").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + desc = body; + + cache.shortdesc = validate(body); + } + }); + + // waypoint.cmt + waypoint.getChild(namespace, "cmt").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + cmt = body; + + cache.description = validate(body); + } + }); + + // waypoint.type + waypoint.getChild(namespace, "type").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + final String[] content = body.split("\\|"); + if (content.length > 0) { + type = content[0].toLowerCase().trim(); + } + } + }); + + // waypoint.sym + waypoint.getChild(namespace, "sym").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + body = body.toLowerCase(); + sym = body; + if (body.indexOf("geocache") != -1 && body.indexOf("found") != -1) { + cache.found = true; + } + } + }); + + // waypoint.url + waypoint.getChild(namespace, "url").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String url) { + final Matcher matcher = patternGuid.matcher(url); + if (matcher.matches()) { + String guid = matcher.group(1); + if (StringUtils.isNotBlank(guid)) { + cache.guid = guid; + } + } + } + }); + + // for GPX 1.0, cache info comes from waypoint node (so called private children, + // for GPX 1.1 from extensions node + final Element cacheParent = getCacheParent(waypoint); + + // GSAK extensions + final Element gsak = cacheParent.getChild(GSAK_NS, "wptExtension"); + gsak.getChild(GSAK_NS, "Watch").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String watchList) { + cache.onWatchlist = Boolean.valueOf(watchList.trim()); + } + }); + + gsak.getChild(GSAK_NS, "UserData").setEndTextElementListener(new UserDataListener(1)); + + for (int i = 2; i <= 4; i++) { + gsak.getChild(GSAK_NS, "User" + i).setEndTextElementListener(new UserDataListener(i)); + } + + // 3 different versions of the GC schema + for (String nsGC : nsGCList) { + // waypoints.cache + final Element gcCache = cacheParent.getChild(nsGC, "cache"); + + gcCache.setStartElementListener(new StartElementListener() { + + @Override + public void start(Attributes attrs) { + try { + if (attrs.getIndex("id") > -1) { + cache.cacheId = attrs.getValue("id"); + } + if (attrs.getIndex("archived") > -1) { + cache.archived = attrs.getValue("archived").equalsIgnoreCase("true"); + } + if (attrs.getIndex("available") > -1) { + cache.disabled = !attrs.getValue("available").equalsIgnoreCase("true"); + } + } catch (Exception e) { + Log.w(cgSettings.tag, "Failed to parse cache attributes."); + } + } + }); + + // waypoint.cache.name + gcCache.getChild(nsGC, "name").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String cacheName) { + cache.name = validate(cacheName); + } + }); + + // waypoint.cache.owner + gcCache.getChild(nsGC, "owner").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String cacheOwner) { + cache.owner = validate(cacheOwner); + } + }); + + // waypoint.cache.type + gcCache.getChild(nsGC, "type").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + setType(validate(body.toLowerCase())); + } + }); + + // waypoint.cache.container + gcCache.getChild(nsGC, "container").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + cache.size = CacheSize.FIND_BY_ID.get(validate(body.toLowerCase())); + } + }); + + // waypoint.cache.attributes + // @see issue #299 + + // <groundspeak:attributes> + // <groundspeak:attribute id="32" inc="1">Bicycles</groundspeak:attribute> + // <groundspeak:attribute id="13" inc="1">Available at all times</groundspeak:attribute> + // where inc = 0 => _no, inc = 1 => _yes + // IDs see array CACHE_ATTRIBUTES + final Element gcAttributes = gcCache.getChild(nsGC, "attributes"); + + // waypoint.cache.attribute + final Element gcAttribute = gcAttributes.getChild(nsGC, "attribute"); + + gcAttribute.setStartElementListener(new StartElementListener() { + @Override + public void start(Attributes attrs) { + try { + if (attrs.getIndex("id") > -1 && attrs.getIndex("inc") > -1) { + int attributeId = Integer.parseInt(attrs.getValue("id")); + boolean attributeActive = Integer.parseInt(attrs.getValue("inc")) != 0; + String internalId = CacheAttributeTranslator.getInternalId(attributeId, attributeActive); + if (internalId != null) { + if (cache.attributes == null) { + cache.attributes = new ArrayList<String>(); + } + cache.attributes.add(internalId); + } + } + } catch (NumberFormatException e) { + // nothing + } + } + }); + + // waypoint.cache.difficulty + gcCache.getChild(nsGC, "difficulty").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + try { + cache.difficulty = new Float(body); + } catch (Exception e) { + Log.w(cgSettings.tag, "Failed to parse difficulty: " + e.toString()); + } + } + }); + + // waypoint.cache.terrain + gcCache.getChild(nsGC, "terrain").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + try { + cache.terrain = new Float(body); + } catch (Exception e) { + Log.w(cgSettings.tag, "Failed to parse terrain: " + e.toString()); + } + } + }); + + // waypoint.cache.country + gcCache.getChild(nsGC, "country").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String country) { + if (StringUtils.isBlank(cache.location)) { + cache.location = validate(country); + } else { + cache.location = cache.location + ", " + country.trim(); + } + } + }); + + // waypoint.cache.state + gcCache.getChild(nsGC, "state").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String state) { + if (StringUtils.isBlank(cache.location)) { + cache.location = validate(state); + } else { + cache.location = state.trim() + ", " + cache.location; + } + } + }); + + // waypoint.cache.encoded_hints + gcCache.getChild(nsGC, "encoded_hints").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String encoded) { + cache.hint = validate(encoded); + } + }); + + gcCache.getChild(nsGC, "short_description").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String shortDesc) { + cache.shortdesc = validate(shortDesc); + } + }); + + gcCache.getChild(nsGC, "long_description").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String desc) { + cache.description = validate(desc); + } + }); + + // waypoint.cache.travelbugs + final Element gcTBs = gcCache.getChild(nsGC, "travelbugs"); + + // waypoint.cache.travelbugs.travelbug + gcTBs.getChild(nsGC, "travelbug").setStartElementListener(new StartElementListener() { + + @Override + public void start(Attributes attrs) { + trackable = new cgTrackable(); + + try { + if (attrs.getIndex("ref") > -1) { + trackable.geocode = attrs.getValue("ref").toUpperCase(); + } + } catch (Exception e) { + // nothing + } + } + }); + + // waypoint.cache.travelbug + final Element gcTB = gcTBs.getChild(nsGC, "travelbug"); + + gcTB.setEndElementListener(new EndElementListener() { + + @Override + public void end() { + if (StringUtils.isNotBlank(trackable.geocode) && StringUtils.isNotBlank(trackable.name)) { + if (cache.inventory == null) { + cache.inventory = new ArrayList<cgTrackable>(); + } + cache.inventory.add(trackable); + } + } + }); + + // waypoint.cache.travelbugs.travelbug.name + gcTB.getChild(nsGC, "name").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String tbName) { + trackable.name = validate(tbName); + } + }); + + // waypoint.cache.logs + final Element gcLogs = gcCache.getChild(nsGC, "logs"); + + // waypoint.cache.log + final Element gcLog = gcLogs.getChild(nsGC, "log"); + + gcLog.setStartElementListener(new StartElementListener() { + + @Override + public void start(Attributes attrs) { + log = new cgLog(); + + try { + if (attrs.getIndex("id") > -1) { + log.id = Integer.parseInt(attrs.getValue("id")); + } + } catch (Exception e) { + // nothing + } + } + }); + + gcLog.setEndElementListener(new EndElementListener() { + + @Override + public void end() { + if (StringUtils.isNotBlank(log.log)) { + if (cache.logs == null) { + cache.logs = new ArrayList<cgLog>(); + } + cache.logs.add(log); + } + } + }); + + // waypoint.cache.logs.log.date + gcLog.getChild(nsGC, "date").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + try { + log.date = parseDate(body).getTime(); + } catch (Exception e) { + Log.w(cgSettings.tag, "Failed to parse log date: " + e.toString()); + } + } + }); + + // waypoint.cache.logs.log.type + gcLog.getChild(nsGC, "type").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String body) { + final String logType = validate(body).toLowerCase(); + if (cgBase.logTypes0.containsKey(logType)) { + log.type = cgBase.logTypes0.get(logType); + } else { + log.type = cgBase.LOG_NOTE; + } + } + }); + + // waypoint.cache.logs.log.finder + gcLog.getChild(nsGC, "finder").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String finderName) { + log.author = validate(finderName); + } + }); + + // waypoint.cache.logs.log.text + gcLog.getChild(nsGC, "text").setEndTextElementListener(new EndTextElementListener() { + + @Override + public void end(String logText) { + log.log = validate(logText); + } + }); + } + boolean parsed = false; + try { + Xml.parse(stream, Xml.Encoding.UTF_8, root.getContentHandler()); + parsed = true; + } catch (IOException e) { + Log.e(cgSettings.tag, "Cannot parse .gpx file as GPX " + version + ": could not read file!"); + } catch (SAXException e) { + Log.e(cgSettings.tag, "Cannot parse .gpx file as GPX " + version + ": could not parse XML - " + e.toString()); + } + return parsed ? search.getCurrentId() : null; + } + + private UUID parse(final File file, final Handler handlerIn) { + if (file == null) { + return null; + } + + FileInputStream fis = null; + UUID result = null; + try { + fis = new FileInputStream(file); + result = parse(fis, handlerIn); + } catch (FileNotFoundException e) { + Log.e(cgSettings.tag, "Cannot parse .gpx file " + file.getAbsolutePath() + " as GPX " + version + ": file not found!"); + } + try { + if (fis != null) { + fis.close(); + } + } catch (IOException e) { + Log.e(cgSettings.tag, "Error after parsing .gpx file " + file.getAbsolutePath() + " as GPX " + version + ": could not close file!"); + } + return result; + } + + protected abstract Element getCacheParent(Element waypoint); + + protected static String validate(String input) { + if ("nil".equalsIgnoreCase(input)) { + return ""; + } + return input.trim(); + } + + private void setType(String parsedString) { + final String knownType = cgBase.cacheTypes.get(parsedString); + if (knownType != null) { + cache.type = knownType; + } + else { + if (StringUtils.isBlank(cache.type)) { + cache.type = "mystery"; // default for not recognized types + } + } + } + + private void findGeoCode(final String input) { + if (input == null || StringUtils.isNotBlank(cache.geocode)) { + return; + } + final Matcher matcherGeocode = patternGeocode.matcher(input); + if (matcherGeocode.find()) { + if (matcherGeocode.groupCount() > 0) { + final String geocode = matcherGeocode.group(1); + if (ConnectorFactory.canHandle(geocode)) { + cache.geocode = geocode; + } + } + } + } + + private void resetCache() { + type = null; + sym = null; + name = null; + desc = null; + cmt = null; + + cache = new cgCache(); + for (int i = 0; i < userData.length; i++) { + userData[i] = null; + } + } + + public static UUID parseGPX(cgeoapplication app, File file, int listId, Handler handler) { + final cgSearch search = new cgSearch(); + UUID searchId = null; + + try { + GPXParser parser = new GPX10Parser(app, listId, search); + searchId = parser.parse(file, handler); + if (searchId == null) { + parser = new GPX11Parser(app, listId, search); + searchId = parser.parse(file, handler); + } + } catch (Exception e) { + Log.e(cgSettings.tag, "cgBase.parseGPX: " + e.toString()); + } + + Log.i(cgSettings.tag, "Caches found in .gpx file: " + app.getCount(searchId)); + + return search.getCurrentId(); + } +} |
