package cgeo.geocaching.files; import cgeo.geocaching.enumerations.CacheSize; import cgeo.geocaching.enumerations.CacheType; import cgeo.geocaching.location.Geopoint; import cgeo.geocaching.models.Geocache; import cgeo.geocaching.utils.DisposableHandler; import cgeo.geocaching.utils.Charsets; import cgeo.geocaching.utils.Log; import org.apache.commons.lang3.StringUtils; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; public final class LocParser extends FileParser { @NonNull private static final String NAME_OWNER_SEPARATOR = " by "; @NonNull private static final CacheSize[] SIZES = { CacheSize.NOT_CHOSEN, // 1 CacheSize.MICRO, // 2 CacheSize.REGULAR, // 3 CacheSize.LARGE, // 4 CacheSize.VIRTUAL, // 5 CacheSize.OTHER, // 6 CacheSize.UNKNOWN, // 7 CacheSize.SMALL, // 8 }; // Used so that the initial value of the geocache is not null. Never filled. @NonNull private static final Geocache DUMMY_GEOCACHE = new Geocache(); private final int listId; public static void parseLoc(final String fileContent, final Set<Geocache> caches) { final Map<String, Geocache> cidCoords = parseLoc(fileContent); for (final Geocache cache : caches) { if (!cache.isReliableLatLon()) { final Geocache coord = cidCoords.get(cache.getGeocode()); // Archived caches will not have any coordinates if (coord != null) { copyCoordToCache(coord, cache); } } } } @NonNull private static Map<String, Geocache> parseLoc(final String content) { return parseLoc(new ByteArrayInputStream(content.getBytes(Charsets.UTF_8))); } @NonNull private static Map<String, Geocache> parseLoc(final InputStream content) { try { final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); final XmlPullParser xpp = factory.newPullParser(); xpp.setInput(content, Charsets.UTF_8.name()); final Map<String, Geocache> caches = new HashMap<>(); int eventType = xpp.getEventType(); Geocache currentCache = DUMMY_GEOCACHE; while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { switch (xpp.getName()) { case "waypoint": currentCache = new Geocache(); currentCache.setType(CacheType.UNKNOWN); // Type not present in .loc file break; case "name": currentCache.setGeocode(xpp.getAttributeValue(null, "id")); if (xpp.next() == XmlPullParser.TEXT) { final String nameOwner = xpp.getText(); currentCache.setName(StringUtils.trim(StringUtils.substringBeforeLast(nameOwner, NAME_OWNER_SEPARATOR))); currentCache.setOwnerUserId(StringUtils.trim(StringUtils.substringAfterLast(nameOwner, NAME_OWNER_SEPARATOR))); } break; case "coord": currentCache.setCoords(new Geopoint(Double.parseDouble(xpp.getAttributeValue(null, "lat")), Double.parseDouble(xpp.getAttributeValue(null, "lon")))); currentCache.setReliableLatLon(true); break; case "container": if (xpp.next() == XmlPullParser.TEXT) { currentCache.setSize(SIZES[Integer.parseInt(xpp.getText()) - 1]); } break; case "difficulty": if (xpp.next() == XmlPullParser.TEXT) { currentCache.setDifficulty(Float.parseFloat(xpp.getText())); } break; case "terrain": if (xpp.next() == XmlPullParser.TEXT) { currentCache.setTerrain(Float.parseFloat(xpp.getText())); } break; default: // Ignore } } else if (eventType == XmlPullParser.END_TAG && xpp.getName().equals("waypoint") && StringUtils.isNotBlank(currentCache.getGeocode())) { caches.put(currentCache.getGeocode(), currentCache); } eventType = xpp.next(); } Log.d("Coordinates found in .loc content: " + caches.size()); return caches; } catch (XmlPullParserException | IOException e) { Log.e("unable to parse .loc content", e); return Collections.emptyMap(); } } private static void copyCoordToCache(final Geocache coord, final Geocache cache) { cache.setCoords(coord.getCoords()); cache.setDifficulty(coord.getDifficulty()); cache.setTerrain(coord.getTerrain()); cache.setSize(coord.getSize()); cache.setGeocode(coord.getGeocode()); cache.setReliableLatLon(true); if (StringUtils.isBlank(cache.getName())) { cache.setName(coord.getName()); } cache.setOwnerUserId(coord.getOwnerUserId()); } @NonNull public static Geopoint parsePoint(final String latitude, final String longitude) { // the loc file contains the coordinates as plain floating point values, therefore avoid using the GeopointParser try { return new Geopoint(Double.parseDouble(latitude), Double.parseDouble(longitude)); } catch (final NumberFormatException e) { Log.e("LOC format has changed", e); } // fall back to parser, just in case the format changes return new Geopoint(latitude, longitude); } public LocParser(final int listId) { this.listId = listId; } @Override @NonNull public Collection<Geocache> parse(@NonNull final InputStream stream, @Nullable final DisposableHandler progressHandler) throws IOException, ParserException { final int maxSize = stream.available(); final Map<String, Geocache> coords = parseLoc(stream); final List<Geocache> caches = new ArrayList<>(); for (final Entry<String, Geocache> entry : coords.entrySet()) { final Geocache cache = entry.getValue(); if (StringUtils.isBlank(cache.getGeocode()) || StringUtils.isBlank(cache.getName())) { continue; } caches.add(cache); fixCache(cache); cache.setType(CacheType.UNKNOWN); // type is not given in the LOC file cache.getLists().add(listId); cache.setDetailed(true); cache.store(); if (progressHandler != null) { progressHandler.sendMessage(progressHandler.obtainMessage(0, maxSize * caches.size() / coords.size(), 0)); } } Log.i("Caches found in .loc file: " + caches.size()); return caches; } }