diff options
Diffstat (limited to 'main/src/cgeo/geocaching/Geocache.java')
-rw-r--r-- | main/src/cgeo/geocaching/Geocache.java | 1647 |
1 files changed, 1647 insertions, 0 deletions
diff --git a/main/src/cgeo/geocaching/Geocache.java b/main/src/cgeo/geocaching/Geocache.java new file mode 100644 index 0000000..2b8e03b --- /dev/null +++ b/main/src/cgeo/geocaching/Geocache.java @@ -0,0 +1,1647 @@ +package cgeo.geocaching; + +import cgeo.geocaching.activity.ActivityMixin; +import cgeo.geocaching.activity.IAbstractActivity; +import cgeo.geocaching.cgData.StorageLocation; +import cgeo.geocaching.connector.ConnectorFactory; +import cgeo.geocaching.connector.IConnector; +import cgeo.geocaching.connector.capability.ISearchByCenter; +import cgeo.geocaching.connector.capability.ISearchByGeocode; +import cgeo.geocaching.connector.gc.GCConnector; +import cgeo.geocaching.connector.gc.GCConstants; +import cgeo.geocaching.connector.gc.Tile; +import cgeo.geocaching.enumerations.CacheAttribute; +import cgeo.geocaching.enumerations.CacheRealm; +import cgeo.geocaching.enumerations.CacheSize; +import cgeo.geocaching.enumerations.CacheType; +import cgeo.geocaching.enumerations.LoadFlags; +import cgeo.geocaching.enumerations.LoadFlags.LoadFlag; +import cgeo.geocaching.enumerations.LoadFlags.RemoveFlag; +import cgeo.geocaching.enumerations.LoadFlags.SaveFlag; +import cgeo.geocaching.enumerations.LogType; +import cgeo.geocaching.enumerations.WaypointType; +import cgeo.geocaching.files.GPXParser; +import cgeo.geocaching.geopoint.Geopoint; +import cgeo.geocaching.network.HtmlImage; +import cgeo.geocaching.utils.CancellableHandler; +import cgeo.geocaching.utils.LazyInitializedList; +import cgeo.geocaching.utils.Log; +import cgeo.geocaching.utils.LogTemplateProvider; +import cgeo.geocaching.utils.LogTemplateProvider.LogContext; +import cgeo.geocaching.utils.MatcherWrapper; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.text.Html; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Internal c:geo representation of a "cache" + */ +public class Geocache implements ICache, IWaypoint { + + private long updated = 0; + private long detailedUpdate = 0; + private long visitedDate = 0; + private int listId = StoredList.TEMPORARY_LIST_ID; + private boolean detailed = false; + private String geocode = ""; + private String cacheId = ""; + private String guid = ""; + private CacheType cacheType = CacheType.UNKNOWN; + private String name = ""; + private String ownerDisplayName = ""; + private String ownerUserId = ""; + private Date hidden = null; + private String hint = ""; + private CacheSize size = CacheSize.UNKNOWN; + private float difficulty = 0; + private float terrain = 0; + private Float direction = null; + private Float distance = null; + private String latlon = ""; + private String location = ""; + private Geopoint coords = null; + private boolean reliableLatLon = false; + private Double elevation = null; + private String personalNote = null; + private String shortdesc = ""; + private String description = null; + private boolean disabled = false; + private boolean archived = false; + private boolean premiumMembersOnly = false; + private boolean found = false; + private boolean favorite = false; + private int favoritePoints = 0; + private float rating = 0; // valid ratings are larger than zero + private int votes = 0; + private float myVote = 0; // valid ratings are larger than zero + private int inventoryItems = 0; + private boolean onWatchlist = false; + private LazyInitializedList<String> attributes = new LazyInitializedList<String>() { + @Override + protected List<String> loadFromDatabase() { + return cgData.loadAttributes(geocode); + } + }; + private LazyInitializedList<Waypoint> waypoints = new LazyInitializedList<Waypoint>() { + @Override + protected List<Waypoint> loadFromDatabase() { + return cgData.loadWaypoints(geocode); + } + }; + private List<Image> spoilers = null; + private LazyInitializedList<LogEntry> logs = new LazyInitializedList<LogEntry>() { + @Override + protected List<LogEntry> loadFromDatabase() { + return cgData.loadLogs(geocode); + } + }; + private List<Trackable> inventory = null; + private Map<LogType, Integer> logCounts = new HashMap<LogType, Integer>(); + private boolean logOffline = false; + private boolean userModifiedCoords = false; + // temporary values + private boolean statusChecked = false; + private String directionImg = ""; + private String nameForSorting; + private final EnumSet<StorageLocation> storageLocation = EnumSet.of(StorageLocation.HEAP); + private boolean finalDefined = false; + private int zoomlevel = Tile.ZOOMLEVEL_MAX + 1; + + private static final Pattern NUMBER_PATTERN = Pattern.compile("\\d+"); + + private Handler changeNotificationHandler = null; + + /** + * Create a new cache. To be used everywhere except for the GPX parser + */ + public Geocache() { + // empty + } + + /** + * Cache constructor to be used by the GPX parser only. This constructor explicitly sets several members to empty + * lists. + * + * @param gpxParser + */ + public Geocache(GPXParser gpxParser) { + setReliableLatLon(true); + setAttributes(Collections.<String> emptyList()); + setWaypoints(Collections.<Waypoint> emptyList(), false); + setLogs(Collections.<LogEntry> emptyList()); + } + + public void setChangeNotificationHandler(Handler newNotificationHandler) { + changeNotificationHandler = newNotificationHandler; + } + + /** + * Sends a change notification to interested parties + */ + private void notifyChange() { + if (changeNotificationHandler != null) { + changeNotificationHandler.sendEmptyMessage(0); + } + } + + /** + * Gather missing information from another cache object. + * + * @param other + * the other version, or null if non-existent + * @return true if this cache is "equal" to the other version + */ + public boolean gatherMissingFrom(final Geocache other) { + if (other == null) { + return false; + } + + updated = System.currentTimeMillis(); + if (!detailed && (other.detailed || zoomlevel < other.zoomlevel)) { + detailed = other.detailed; + detailedUpdate = other.detailedUpdate; + coords = other.coords; + cacheType = other.cacheType; + zoomlevel = other.zoomlevel; + // boolean values must be enumerated here. Other types are assigned outside this if-statement + premiumMembersOnly = other.premiumMembersOnly; + reliableLatLon = other.reliableLatLon; + archived = other.archived; + found = other.found; + disabled = other.disabled; + favorite = other.favorite; + onWatchlist = other.onWatchlist; + logOffline = other.logOffline; + finalDefined = other.finalDefined; + } + + /* + * No gathering for boolean members if other cache is not-detailed + * and does not have information with higher reliability (denoted by zoomlevel) + * - found + * - own + * - disabled + * - favorite + * - onWatchlist + * - logOffline + */ + if (visitedDate == 0) { + visitedDate = other.visitedDate; + } + if (listId == StoredList.TEMPORARY_LIST_ID) { + listId = other.listId; + } + if (StringUtils.isBlank(geocode)) { + geocode = other.geocode; + } + if (StringUtils.isBlank(cacheId)) { + cacheId = other.cacheId; + } + if (StringUtils.isBlank(guid)) { + guid = other.guid; + } + if (null == cacheType || CacheType.UNKNOWN == cacheType) { + cacheType = other.cacheType; + } + if (StringUtils.isBlank(name)) { + name = other.name; + } + if (StringUtils.isBlank(ownerDisplayName)) { + ownerDisplayName = other.ownerDisplayName; + } + if (StringUtils.isBlank(ownerUserId)) { + ownerUserId = other.ownerUserId; + } + if (hidden == null) { + hidden = other.hidden; + } + if (StringUtils.isBlank(hint)) { + hint = other.hint; + } + if (size == null || CacheSize.UNKNOWN == size) { + size = other.size; + } + if (difficulty == 0) { + difficulty = other.difficulty; + } + if (terrain == 0) { + terrain = other.terrain; + } + if (direction == null) { + direction = other.direction; + } + if (distance == null) { + distance = other.distance; + } + if (StringUtils.isBlank(latlon)) { + latlon = other.latlon; + } + if (StringUtils.isBlank(location)) { + location = other.location; + } + if (coords == null) { + coords = other.coords; + } + if (elevation == null) { + elevation = other.elevation; + } + if (personalNote == null) { // don't use StringUtils.isBlank here. Otherwise we cannot recognize a note which was deleted on GC + personalNote = other.personalNote; + } + if (StringUtils.isBlank(shortdesc)) { + shortdesc = other.shortdesc; + } + if (StringUtils.isBlank(description)) { + description = other.description; + } + // FIXME: this makes no sense to favor this over the other. 0 should not be a special case here as it is + // in the range of acceptable values. This is probably the case at other places (rating, votes, etc.) too. + if (favoritePoints == 0) { + favoritePoints = other.favoritePoints; + } + if (rating == 0) { + rating = other.rating; + } + if (votes == 0) { + votes = other.votes; + } + if (myVote == 0) { + myVote = other.myVote; + } + if (attributes.isEmpty()) { + attributes.set(other.attributes); + } + if (waypoints.isEmpty()) { + this.setWaypoints(other.waypoints.asList(), false); + } + else { + ArrayList<Waypoint> newPoints = new ArrayList<Waypoint>(waypoints.asList()); + Waypoint.mergeWayPoints(newPoints, other.waypoints.asList(), false); + this.setWaypoints(newPoints, false); + } + if (spoilers == null) { + spoilers = other.spoilers; + } + if (inventory == null) { + // If inventoryItems is 0, it can mean both + // "don't know" or "0 items". Since we cannot distinguish + // them here, only populate inventoryItems from + // old data when we have to do it for inventory. + inventory = other.inventory; + inventoryItems = other.inventoryItems; + } + if (logs.isEmpty()) { // keep last known logs if none + logs.set(other.logs); + } + if (logCounts.isEmpty()) { + logCounts = other.logCounts; + } + + // if cache has ORIGINAL type waypoint ... it is considered that it has modified coordinates, otherwise not + userModifiedCoords = false; + if (waypoints != null) { + for (Waypoint wpt : waypoints) { + if (wpt.getWaypointType() == WaypointType.ORIGINAL) { + userModifiedCoords = true; + break; + } + } + } + + if (!reliableLatLon) { + reliableLatLon = other.reliableLatLon; + } + if (zoomlevel == -1) { + zoomlevel = other.zoomlevel; + } + + return isEqualTo(other); + } + + /** + * Compare two caches quickly. For map and list fields only the references are compared ! + * + * @param other + * the other cache to compare this one to + * @return true if both caches have the same content + */ + private boolean isEqualTo(final Geocache other) { + return detailed == other.detailed && + StringUtils.equalsIgnoreCase(geocode, other.geocode) && + StringUtils.equalsIgnoreCase(name, other.name) && + cacheType == other.cacheType && + size == other.size && + found == other.found && + premiumMembersOnly == other.premiumMembersOnly && + difficulty == other.difficulty && + terrain == other.terrain && + (coords != null ? coords.equals(other.coords) : null == other.coords) && + reliableLatLon == other.reliableLatLon && + disabled == other.disabled && + archived == other.archived && + listId == other.listId && + StringUtils.equalsIgnoreCase(ownerDisplayName, other.ownerDisplayName) && + StringUtils.equalsIgnoreCase(ownerUserId, other.ownerUserId) && + StringUtils.equalsIgnoreCase(description, other.description) && + StringUtils.equalsIgnoreCase(personalNote, other.personalNote) && + StringUtils.equalsIgnoreCase(shortdesc, other.shortdesc) && + StringUtils.equalsIgnoreCase(latlon, other.latlon) && + StringUtils.equalsIgnoreCase(location, other.location) && + favorite == other.favorite && + favoritePoints == other.favoritePoints && + onWatchlist == other.onWatchlist && + (hidden != null ? hidden.equals(other.hidden) : null == other.hidden) && + StringUtils.equalsIgnoreCase(guid, other.guid) && + StringUtils.equalsIgnoreCase(hint, other.hint) && + StringUtils.equalsIgnoreCase(cacheId, other.cacheId) && + (direction != null ? direction.equals(other.direction) : null == other.direction) && + (distance != null ? distance.equals(other.distance) : null == other.distance) && + (elevation != null ? elevation.equals(other.elevation) : null == other.elevation) && + rating == other.rating && + votes == other.votes && + myVote == other.myVote && + inventoryItems == other.inventoryItems && + attributes == other.attributes && + waypoints == other.waypoints && + spoilers == other.spoilers && + logs == other.logs && + inventory == other.inventory && + logCounts == other.logCounts && + logOffline == other.logOffline && + finalDefined == other.finalDefined; + } + + public boolean hasTrackables() { + return inventoryItems > 0; + } + + public boolean canBeAddedToCalendar() { + // is event type? + if (!isEventCache()) { + return false; + } + // has event date set? + if (hidden == null) { + return false; + } + // is not in the past? + final Calendar cal = Calendar.getInstance(); + cal.setTime(new Date()); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + return hidden.compareTo(cal.getTime()) >= 0; + } + + /** + * Checks if a page contains the guid of a cache + * + * @param page + * the page to search in, may be null + * @return true if the page contains the guid of the cache, false otherwise + */ + public boolean isGuidContainedInPage(final String page) { + if (StringUtils.isBlank(page) || StringUtils.isBlank(guid)) { + return false; + } + final Boolean found = Pattern.compile(guid, Pattern.CASE_INSENSITIVE).matcher(page).find(); + Log.i("Geocache.isGuidContainedInPage: guid '" + guid + "' " + (found ? "" : "not ") + "found"); + return found; + } + + public boolean isEventCache() { + return cacheType.isEvent(); + } + + public void logVisit(final IAbstractActivity fromActivity) { + if (StringUtils.isBlank(cacheId)) { + fromActivity.showToast(((Activity) fromActivity).getResources().getString(R.string.err_cannot_log_visit)); + return; + } + Intent logVisitIntent = new Intent((Activity) fromActivity, VisitCacheActivity.class); + logVisitIntent.putExtra(VisitCacheActivity.EXTRAS_ID, cacheId); + logVisitIntent.putExtra(VisitCacheActivity.EXTRAS_GEOCODE, geocode); + + ((Activity) fromActivity).startActivity(logVisitIntent); + } + + public void logOffline(final Activity fromActivity, final LogType logType) { + final boolean mustIncludeSignature = StringUtils.isNotBlank(Settings.getSignature()) && Settings.isAutoInsertSignature(); + final String initial = mustIncludeSignature ? LogTemplateProvider.applyTemplates(Settings.getSignature(), new LogContext(this, true)) : ""; + logOffline(fromActivity, initial, Calendar.getInstance(), logType); + } + + void logOffline(final Activity fromActivity, final String log, Calendar date, final LogType logType) { + if (logType == LogType.UNKNOWN) { + return; + } + final boolean status = cgData.saveLogOffline(geocode, date.getTime(), logType, log); + + Resources res = fromActivity.getResources(); + if (status) { + ActivityMixin.showToast(fromActivity, res.getString(R.string.info_log_saved)); + cgData.saveVisitDate(geocode); + logOffline = true; + + notifyChange(); + } else { + ActivityMixin.showToast(fromActivity, res.getString(R.string.err_log_post_failed)); + } + } + + public List<LogType> getPossibleLogTypes() { + final List<LogType> logTypes = new LinkedList<LogType>(); + if (isEventCache()) { + logTypes.add(LogType.WILL_ATTEND); + logTypes.add(LogType.NOTE); + logTypes.add(LogType.ATTENDED); + logTypes.add(LogType.NEEDS_ARCHIVE); + if (isOwner()) { + logTypes.add(LogType.ANNOUNCEMENT); + } + } else if (CacheType.WEBCAM == cacheType) { + logTypes.add(LogType.WEBCAM_PHOTO_TAKEN); + logTypes.add(LogType.DIDNT_FIND_IT); + logTypes.add(LogType.NOTE); + logTypes.add(LogType.NEEDS_ARCHIVE); + logTypes.add(LogType.NEEDS_MAINTENANCE); + } else { + logTypes.add(LogType.FOUND_IT); + logTypes.add(LogType.DIDNT_FIND_IT); + logTypes.add(LogType.NOTE); + logTypes.add(LogType.NEEDS_ARCHIVE); + logTypes.add(LogType.NEEDS_MAINTENANCE); + } + if (isOwner()) { + logTypes.add(LogType.OWNER_MAINTENANCE); + logTypes.add(LogType.TEMP_DISABLE_LISTING); + logTypes.add(LogType.ENABLE_LISTING); + logTypes.add(LogType.ARCHIVE); + logTypes.remove(LogType.UPDATE_COORDINATES); + } + return logTypes; + } + + public void openInBrowser(Activity fromActivity) { + fromActivity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getCacheUrl()))); + } + + private String getCacheUrl() { + return getConnector().getCacheUrl(this); + } + + private IConnector getConnector() { + return ConnectorFactory.getConnector(this); + } + + public boolean canOpenInBrowser() { + return getCacheUrl() != null; + } + + public boolean supportsRefresh() { + return getConnector() instanceof ISearchByGeocode; + } + + public boolean supportsWatchList() { + return getConnector().supportsWatchList(); + } + + public boolean supportsFavoritePoints() { + return getConnector().supportsFavoritePoints(); + } + + public boolean supportsLogging() { + return getConnector().supportsLogging(); + } + + public boolean supportsOwnCoordinates() { + return getConnector().supportsOwnCoordinates(); + } + + public CacheRealm getCacheRealm() { + return getConnector().getCacheRealm(); + } + + @Override + public float getDifficulty() { + return difficulty; + } + + @Override + public String getGeocode() { + return geocode; + } + + @Override + public String getOwnerDisplayName() { + return ownerDisplayName; + } + + @Override + public CacheSize getSize() { + if (size == null) { + return CacheSize.UNKNOWN; + } + return size; + } + + @Override + public float getTerrain() { + return terrain; + } + + @Override + public boolean isArchived() { + return archived; + } + + @Override + public boolean isDisabled() { + return disabled; + } + + @Override + public boolean isPremiumMembersOnly() { + return premiumMembersOnly; + } + + public void setPremiumMembersOnly(boolean members) { + this.premiumMembersOnly = members; + } + + @Override + public boolean isOwner() { + return getConnector().isOwner(this); + } + + @Override + public String getOwnerUserId() { + return ownerUserId; + } + + @Override + public String getHint() { + return hint; + } + + @Override + public String getDescription() { + if (description == null) { + description = StringUtils.defaultString(cgData.getCacheDescription(geocode)); + } + return description; + } + + @Override + public String getShortDescription() { + return shortdesc; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getCacheId() { + if (StringUtils.isBlank(cacheId) && getConnector().equals(GCConnector.getInstance())) { + return String.valueOf(GCConstants.gccodeToGCId(geocode)); + } + + return cacheId; + } + + @Override + public String getGuid() { + return guid; + } + + @Override + public String getLocation() { + return location; + } + + @Override + public String getPersonalNote() { + // non premium members have no personal notes, premium members have an empty string by default. + // map both to null, so other code doesn't need to differentiate + if (StringUtils.isBlank(personalNote)) { + return null; + } + return personalNote; + } + + public boolean supportsUserActions() { + return getConnector().supportsUserActions(); + } + + public boolean supportsCachesAround() { + return getConnector() instanceof ISearchByCenter; + } + + public void shareCache(Activity fromActivity, Resources res) { + if (geocode == null) { + return; + } + + StringBuilder subject = new StringBuilder("Geocache "); + subject.append(geocode); + if (StringUtils.isNotBlank(name)) { + subject.append(" - ").append(name); + } + + final Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_SUBJECT, subject.toString()); + intent.putExtra(Intent.EXTRA_TEXT, getUrl()); + + fromActivity.startActivity(Intent.createChooser(intent, res.getText(R.string.action_bar_share_title))); + } + + public String getUrl() { + return getConnector().getCacheUrl(this); + } + + public boolean supportsGCVote() { + return StringUtils.startsWithIgnoreCase(geocode, "GC"); + } + + public void setDescription(final String description) { + this.description = description; + } + + @Override + public boolean isFound() { + return found; + } + + @Override + public boolean isFavorite() { + return favorite; + } + + public void setFavorite(boolean favourite) { + this.favorite = favourite; + } + + @Override + public boolean isWatchlist() { + return onWatchlist; + } + + @Override + public Date getHiddenDate() { + return hidden; + } + + @Override + public LazyInitializedList<String> getAttributes() { + return attributes; + } + + @Override + public List<Trackable> getInventory() { + return inventory; + } + + public void addSpoiler(final Image spoiler) { + if (spoilers == null) { + spoilers = new ArrayList<Image>(); + } + spoilers.add(spoiler); + } + + @Override + public List<Image> getSpoilers() { + if (spoilers == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(spoilers); + } + + @Override + public Map<LogType, Integer> getLogCounts() { + return logCounts; + } + + @Override + public int getFavoritePoints() { + return favoritePoints; + } + + @Override + public String getNameForSorting() { + if (null == nameForSorting) { + final MatcherWrapper matcher = new MatcherWrapper(NUMBER_PATTERN, name); + if (matcher.find()) { + nameForSorting = name.replace(matcher.group(), StringUtils.leftPad(matcher.group(), 6, '0')); + } + else { + nameForSorting = name; + } + } + return nameForSorting; + } + + public boolean isVirtual() { + return CacheType.VIRTUAL == cacheType || CacheType.WEBCAM == cacheType + || CacheType.EARTH == cacheType; + } + + public boolean showSize() { + return !((isEventCache() || isVirtual()) && size == CacheSize.NOT_CHOSEN); + } + + public long getUpdated() { + return updated; + } + + public void setUpdated(long updated) { + this.updated = updated; + } + + public long getDetailedUpdate() { + return detailedUpdate; + } + + public void setDetailedUpdate(long detailedUpdate) { + this.detailedUpdate = detailedUpdate; + } + + public long getVisitedDate() { + return visitedDate; + } + + public void setVisitedDate(long visitedDate) { + this.visitedDate = visitedDate; + } + + public int getListId() { + return listId; + } + + public void setListId(int listId) { + this.listId = listId; + } + + public boolean isDetailed() { + return detailed; + } + + public void setDetailed(boolean detailed) { + this.detailed = detailed; + } + + public void setHidden(final Date hidden) { + if (hidden == null) { + this.hidden = null; + } + else { + this.hidden = new Date(hidden.getTime()); // avoid storing the external reference in this object + } + } + + public Float getDirection() { + return direction; + } + + public void setDirection(Float direction) { + this.direction = direction; + } + + public Float getDistance() { + return distance; + } + + public void setDistance(Float distance) { + this.distance = distance; + } + + public String getLatlon() { + return latlon; + } + + public void setLatlon(String latlon) { + this.latlon = latlon; + } + + @Override + public Geopoint getCoords() { + return coords; + } + + public void setCoords(Geopoint coords) { + this.coords = coords; + } + + /** + * @return true if the coords are from the cache details page and the user has been logged in + */ + public boolean isReliableLatLon() { + return getConnector().isReliableLatLon(reliableLatLon); + } + + public void setReliableLatLon(boolean reliableLatLon) { + this.reliableLatLon = reliableLatLon; + } + + public Double getElevation() { + return elevation; + } + + public void setElevation(Double elevation) { + this.elevation = elevation; + } + + public String getShortdesc() { + return shortdesc; + } + + public void setShortdesc(String shortdesc) { + this.shortdesc = shortdesc; + } + + public void setFavoritePoints(int favoriteCnt) { + this.favoritePoints = favoriteCnt; + } + + public float getRating() { + return rating; + } + + public void setRating(float rating) { + this.rating = rating; + } + + public int getVotes() { + return votes; + } + + public void setVotes(int votes) { + this.votes = votes; + } + + public float getMyVote() { + return myVote; + } + + public void setMyVote(float myVote) { + this.myVote = myVote; + } + + public int getInventoryItems() { + return inventoryItems; + } + + public void setInventoryItems(int inventoryItems) { + this.inventoryItems = inventoryItems; + } + + public boolean isOnWatchlist() { + return onWatchlist; + } + + public void setOnWatchlist(boolean onWatchlist) { + this.onWatchlist = onWatchlist; + } + + /** + * return an immutable list of waypoints. + * + * @return always non <code>null</code> + */ + public List<Waypoint> getWaypoints() { + return waypoints.asList(); + } + + /** + * @param waypoints + * List of waypoints to set for cache + * @param saveToDatabase + * Indicates whether to add the waypoints to the database. Should be false if + * called while loading or building a cache + * @return <code>true</code> if waypoints successfully added to waypoint database + */ + public boolean setWaypoints(List<Waypoint> waypoints, boolean saveToDatabase) { + this.waypoints.set(waypoints); + finalDefined = false; + if (waypoints != null) { + for (Waypoint waypoint : waypoints) { + waypoint.setGeocode(geocode); + if (waypoint.isFinalWithCoords()) { + finalDefined = true; + } + } + } + return saveToDatabase && cgData.saveWaypoints(this); + } + + /** + * @return never <code>null</code> + */ + public LazyInitializedList<LogEntry> getLogs() { + return logs; + } + + /** + * @return only the logs of friends, never <code>null</code> + */ + public List<LogEntry> getFriendsLogs() { + ArrayList<LogEntry> friendLogs = new ArrayList<LogEntry>(); + for (LogEntry log : logs) { + if (log.friend) { + friendLogs.add(log); + } + } + return Collections.unmodifiableList(friendLogs); + } + + /** + * @param logs + * the log entries + */ + public void setLogs(List<LogEntry> logs) { + this.logs.set(logs); + } + + public boolean isLogOffline() { + return logOffline; + } + + public void setLogOffline(boolean logOffline) { + this.logOffline = logOffline; + } + + public boolean isStatusChecked() { + return statusChecked; + } + + public void setStatusChecked(boolean statusChecked) { + this.statusChecked = statusChecked; + } + + public String getDirectionImg() { + return directionImg; + } + + public void setDirectionImg(String directionImg) { + this.directionImg = directionImg; + } + + public void setGeocode(String geocode) { + this.geocode = StringUtils.upperCase(geocode); + } + + public void setCacheId(String cacheId) { + this.cacheId = cacheId; + } + + public void setGuid(String guid) { + this.guid = guid; + } + + public void setName(String name) { + this.name = name; + } + + public void setOwnerDisplayName(String ownerDisplayName) { + this.ownerDisplayName = ownerDisplayName; + } + + public void setOwnerUserId(String ownerUserId) { + this.ownerUserId = ownerUserId; + } + + public void setHint(String hint) { + this.hint = hint; + } + + public void setSize(CacheSize size) { + if (size == null) { + this.size = CacheSize.UNKNOWN; + } + else { + this.size = size; + } + } + + public void setDifficulty(float difficulty) { + this.difficulty = difficulty; + } + + public void setTerrain(float terrain) { + this.terrain = terrain; + } + + public void setLocation(String location) { + this.location = location; + } + + public void setPersonalNote(String personalNote) { + this.personalNote = personalNote; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + public void setArchived(boolean archived) { + this.archived = archived; + } + + public void setFound(boolean found) { + this.found = found; + } + + public void setAttributes(List<String> attributes) { + this.attributes.set(attributes); + } + + public void setSpoilers(List<Image> spoilers) { + this.spoilers = spoilers; + } + + public void setInventory(List<Trackable> inventory) { + this.inventory = inventory; + } + + public void setLogCounts(Map<LogType, Integer> logCounts) { + this.logCounts = logCounts; + } + + /* + * (non-Javadoc) + * + * @see cgeo.geocaching.IBasicCache#getType() + * + * @returns Never null + */ + @Override + public CacheType getType() { + return cacheType; + } + + public void setType(CacheType cacheType) { + if (cacheType == null || CacheType.ALL == cacheType) { + throw new IllegalArgumentException("Illegal cache type"); + } + this.cacheType = cacheType; + } + + public boolean hasDifficulty() { + return difficulty > 0f; + } + + public boolean hasTerrain() { + return terrain > 0f; + } + + /** + * @return the storageLocation + */ + public EnumSet<StorageLocation> getStorageLocation() { + return storageLocation; + } + + /** + * @param storageLocation + * the storageLocation to set + */ + public void addStorageLocation(final StorageLocation storageLocation) { + this.storageLocation.add(storageLocation); + } + + /** + * @param waypoint + * Waypoint to add to the cache + * @param saveToDatabase + * Indicates whether to add the waypoint to the database. Should be false if + * called while loading or building a cache + * @return <code>true</code> if waypoint successfully added to waypoint database + */ + public boolean addOrChangeWaypoint(final Waypoint waypoint, boolean saveToDatabase) { + waypoint.setGeocode(geocode); + + if (waypoint.getId() < 0) { // this is a new waypoint + waypoints.add(waypoint); + if (waypoint.isFinalWithCoords()) { + finalDefined = true; + } + } else { // this is a waypoint being edited + final int index = getWaypointIndex(waypoint); + if (index >= 0) { + waypoints.remove(index); + } + waypoints.add(waypoint); + // when waypoint was edited, finalDefined may have changed + resetFinalDefined(); + } + return saveToDatabase && cgData.saveWaypoint(waypoint.getId(), geocode, waypoint); + } + + public boolean hasWaypoints() { + return !waypoints.isEmpty(); + } + + public boolean hasFinalDefined() { + return finalDefined; + } + + // Only for loading + public void setFinalDefined(boolean finalDefined) { + this.finalDefined = finalDefined; + } + + /** + * Reset <code>finalDefined</code> based on current list of stored waypoints + */ + private void resetFinalDefined() { + finalDefined = false; + for (Waypoint wp : waypoints) { + if (wp.isFinalWithCoords()) { + finalDefined = true; + break; + } + } + } + + public boolean hasUserModifiedCoords() { + return userModifiedCoords; + } + + public void setUserModifiedCoords(boolean coordsChanged) { + userModifiedCoords = coordsChanged; + } + + /** + * Duplicate a waypoint. + * + * @param original + * the waypoint to duplicate + * @return <code>true</code> if the waypoint was duplicated, <code>false</code> otherwise (invalid index) + */ + public boolean duplicateWaypoint(final Waypoint original) { + if (original == null) { + return false; + } + final int index = getWaypointIndex(original); + final Waypoint copy = new Waypoint(original); + copy.setUserDefined(); + copy.setName(cgeoapplication.getInstance().getString(R.string.waypoint_copy_of) + " " + copy.getName()); + waypoints.add(index + 1, copy); + return cgData.saveWaypoint(-1, geocode, copy); + } + + /** + * delete a user defined waypoint + * + * @param waypoint + * to be removed from cache + * @return <code>true</code>, if the waypoint was deleted + */ + public boolean deleteWaypoint(final Waypoint waypoint) { + if (waypoint == null) { + return false; + } + if (waypoint.getId() < 0) { + return false; + } + if (waypoint.isUserDefined()) { + final int index = getWaypointIndex(waypoint); + waypoints.remove(index); + cgData.deleteWaypoint(waypoint.getId()); + cgData.removeCache(geocode, EnumSet.of(RemoveFlag.REMOVE_CACHE)); + // Check status if Final is defined + if (waypoint.isFinalWithCoords()) { + resetFinalDefined(); + } + return true; + } + return false; + } + + /** + * deletes any waypoint + * + * @param waypoint + */ + + public void deleteWaypointForce(Waypoint waypoint) { + final int index = getWaypointIndex(waypoint); + waypoints.remove(index); + cgData.deleteWaypoint(waypoint.getId()); + cgData.removeCache(geocode, EnumSet.of(RemoveFlag.REMOVE_CACHE)); + resetFinalDefined(); + } + + /** + * Find index of given <code>waypoint</code> in cache's <code>waypoints</code> list + * + * @param waypoint + * to find index for + * @return index in <code>waypoints</code> if found, -1 otherwise + */ + private int getWaypointIndex(final Waypoint waypoint) { + final int id = waypoint.getId(); + for (int index = 0; index < waypoints.size(); index++) { + if (waypoints.get(index).getId() == id) { + return index; + } + } + return -1; + } + + /** + * Retrieve a given waypoint. + * + * @param index + * the index of the waypoint + * @return waypoint or <code>null</code> if index is out of range + */ + public Waypoint getWaypoint(final int index) { + return index >= 0 && index < waypoints.size() ? waypoints.get(index) : null; + } + + /** + * Lookup a waypoint by its id. + * + * @param id + * the id of the waypoint to look for + * @return waypoint or <code>null</code> + */ + public Waypoint getWaypointById(final int id) { + for (final Waypoint waypoint : waypoints) { + if (waypoint.getId() == id) { + return waypoint; + } + } + return null; + } + + public void parseWaypointsFromNote() { + try { + if (StringUtils.isBlank(getPersonalNote())) { + return; + } + final Pattern coordPattern = Pattern.compile("\\b[nNsS]{1}\\s*\\d"); // begin of coordinates + int count = 1; + String note = getPersonalNote(); + MatcherWrapper matcher = new MatcherWrapper(coordPattern, note); + while (matcher.find()) { + try { + final Geopoint point = new Geopoint(note.substring(matcher.start())); + // coords must have non zero latitude and longitude and at least one part shall have fractional degrees + if (point.getLatitudeE6() != 0 && point.getLongitudeE6() != 0 && ((point.getLatitudeE6() % 1000) != 0 || (point.getLongitudeE6() % 1000) != 0)) { + final String name = cgeoapplication.getInstance().getString(R.string.cache_personal_note) + " " + count; + final Waypoint waypoint = new Waypoint(name, WaypointType.WAYPOINT, false); + waypoint.setCoords(point); + addOrChangeWaypoint(waypoint, false); + count++; + } + } catch (Geopoint.ParseException e) { + // ignore + } + + note = note.substring(matcher.start() + 1); + matcher = new MatcherWrapper(coordPattern, note); + } + } catch (Exception e) { + Log.e("Geocache.parseWaypointsFromNote", e); + } + } + + /* + * For working in the debugger + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return this.geocode + " " + this.name; + } + + @Override + public int hashCode() { + return geocode.hashCode() * name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Geocache)) { + return false; + } + // just compare the geocode even if that is not what "equals" normally does + return StringUtils.isNotBlank(geocode) && geocode.equals(((Geocache) obj).geocode); + } + + public void store(CancellableHandler handler) { + final int listId = Math.max(getListId(), StoredList.STANDARD_LIST_ID); + storeCache(this, null, listId, false, handler); + } + + public void setZoomlevel(int zoomlevel) { + this.zoomlevel = zoomlevel; + } + + @Override + public int getId() { + return 0; + } + + @Override + public WaypointType getWaypointType() { + return null; + } + + @Override + public String getCoordType() { + return "cache"; + } + + public void drop(Handler handler) { + try { + cgData.markDropped(Collections.singletonList(this)); + cgData.removeCache(getGeocode(), EnumSet.of(RemoveFlag.REMOVE_CACHE)); + + handler.sendMessage(Message.obtain()); + } catch (Exception e) { + Log.e("cache.drop: ", e); + } + } + + public void checkFields() { + if (StringUtils.isBlank(getGeocode())) { + Log.w("geo code not parsed correctly for " + geocode); + } + if (StringUtils.isBlank(getName())) { + Log.w("name not parsed correctly for " + geocode); + } + if (StringUtils.isBlank(getGuid())) { + Log.w("guid not parsed correctly for " + geocode); + } + if (getTerrain() == 0.0) { + Log.w("terrain not parsed correctly for " + geocode); + } + if (getDifficulty() == 0.0) { + Log.w("difficulty not parsed correctly for " + geocode); + } + if (StringUtils.isBlank(getOwnerDisplayName())) { + Log.w("owner display name not parsed correctly for " + geocode); + } + if (StringUtils.isBlank(getOwnerUserId())) { + Log.w("owner user id real not parsed correctly for " + geocode); + } + if (getHiddenDate() == null) { + Log.w("hidden not parsed correctly for " + geocode); + } + if (getFavoritePoints() < 0) { + Log.w("favoriteCount not parsed correctly for " + geocode); + } + if (getSize() == null) { + Log.w("size not parsed correctly for " + geocode); + } + if (getType() == null || getType() == CacheType.UNKNOWN) { + Log.w("type not parsed correctly for " + geocode); + } + if (getCoords() == null) { + Log.w("coordinates not parsed correctly for " + geocode); + } + if (StringUtils.isBlank(getLocation())) { + Log.w("location not parsed correctly for " + geocode); + } + } + + public void refresh(int newListId, CancellableHandler handler) { + cgData.removeCache(geocode, EnumSet.of(RemoveFlag.REMOVE_CACHE)); + storeCache(null, geocode, newListId, true, handler); + } + + public static void storeCache(Geocache origCache, String geocode, int listId, boolean forceRedownload, CancellableHandler handler) { + try { + Geocache cache; + // get cache details, they may not yet be complete + if (origCache != null) { + // only reload the cache if it was already stored or doesn't have full details (by checking the description) + if (origCache.isOffline() || StringUtils.isBlank(origCache.getDescription())) { + final SearchResult search = searchByGeocode(origCache.getGeocode(), null, listId, false, handler); + cache = search.getFirstCacheFromResult(LoadFlags.LOAD_CACHE_OR_DB); + } else { + cache = origCache; + } + } else if (StringUtils.isNotBlank(geocode)) { + final SearchResult search = searchByGeocode(geocode, null, listId, forceRedownload, handler); + cache = search.getFirstCacheFromResult(LoadFlags.LOAD_CACHE_OR_DB); + } else { + cache = null; + } + + if (cache == null) { + if (handler != null) { + handler.sendMessage(Message.obtain()); + } + + return; + } + + if (CancellableHandler.isCancelled(handler)) { + return; + } + + final HtmlImage imgGetter = new HtmlImage(cache.getGeocode(), false, listId, true); + + // store images from description + if (StringUtils.isNotBlank(cache.getDescription())) { + Html.fromHtml(cache.getDescription(), imgGetter, null); + } + + if (CancellableHandler.isCancelled(handler)) { + return; + } + + // store spoilers + if (CollectionUtils.isNotEmpty(cache.getSpoilers())) { + for (Image oneSpoiler : cache.getSpoilers()) { + imgGetter.getDrawable(oneSpoiler.getUrl()); + } + } + + if (CancellableHandler.isCancelled(handler)) { + return; + } + + // store images from logs + if (Settings.isStoreLogImages()) { + for (LogEntry log : cache.getLogs()) { + if (log.hasLogImages()) { + for (Image oneLogImg : log.getLogImages()) { + imgGetter.getDrawable(oneLogImg.getUrl()); + } + } + } + } + + if (CancellableHandler.isCancelled(handler)) { + return; + } + + cache.setListId(listId); + cgData.saveCache(cache, EnumSet.of(SaveFlag.SAVE_DB)); + + if (CancellableHandler.isCancelled(handler)) { + return; + } + + StaticMapsProvider.downloadMaps(cache); + + if (handler != null) { + handler.sendMessage(Message.obtain()); + } + } catch (Exception e) { + Log.e("cgBase.storeCache"); + } + } + + public static SearchResult searchByGeocode(final String geocode, final String guid, final int listId, final boolean forceReload, final CancellableHandler handler) { + if (StringUtils.isBlank(geocode) && StringUtils.isBlank(guid)) { + Log.e("Geocache.searchByGeocode: No geocode nor guid given"); + return null; + } + + if (!forceReload && listId == StoredList.TEMPORARY_LIST_ID && (cgData.isOffline(geocode, guid) || cgData.isThere(geocode, guid, true, true))) { + final SearchResult search = new SearchResult(); + final String realGeocode = StringUtils.isNotBlank(geocode) ? geocode : cgData.getGeocodeForGuid(guid); + search.addGeocode(realGeocode); + return search; + } + + // if we have no geocode, we can't dynamically select the handler, but must explicitly use GC + if (geocode == null && guid != null) { + return GCConnector.getInstance().searchByGeocode(null, guid, handler); + } + + final IConnector connector = ConnectorFactory.getConnector(geocode); + if (connector instanceof ISearchByGeocode) { + return ((ISearchByGeocode) connector).searchByGeocode(geocode, guid, handler); + } + return null; + } + + public boolean isOffline() { + return listId >= StoredList.STANDARD_LIST_ID; + } + + /** + * guess an event start time from the description + * + * @return start time in minutes after midnight + */ + public String guessEventTimeMinutes() { + if (!isEventCache()) { + return null; + } + // 12:34 + final Pattern time = Pattern.compile("\\b(\\d{1,2})\\:(\\d\\d)\\b"); + final MatcherWrapper matcher = new MatcherWrapper(time, getDescription()); + while (matcher.find()) { + try { + final int hours = Integer.valueOf(matcher.group(1)); + final int minutes = Integer.valueOf(matcher.group(2)); + if (hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60) { + return String.valueOf(hours * 60 + minutes); + } + } catch (NumberFormatException e) { + // cannot happen, but static code analysis doesn't know + } + } + // 12 o'clock + final String hourLocalized = cgeoapplication.getInstance().getString(R.string.cache_time_full_hours); + if (StringUtils.isNotBlank(hourLocalized)) { + final Pattern fullHours = Pattern.compile("\\b(\\d{1,2})\\s+" + Pattern.quote(hourLocalized), Pattern.CASE_INSENSITIVE); + final MatcherWrapper matcherHours = new MatcherWrapper(fullHours, getDescription()); + if (matcherHours.find()) { + try { + final int hours = Integer.valueOf(matcherHours.group(1)); + if (hours >= 0 && hours < 24) { + return String.valueOf(hours * 60); + } + } catch (NumberFormatException e) { + // cannot happen, but static code analysis doesn't know + } + } + } + return null; + } + + /** + * check whether the cache has a given attribute + * + * @param attribute + * @param yes + * true if we are looking for the attribute_yes version, false for the attribute_no version + * @return + */ + public boolean hasAttribute(CacheAttribute attribute, boolean yes) { + Geocache fullCache = cgData.loadCache(getGeocode(), EnumSet.of(LoadFlag.LOAD_ATTRIBUTES)); + if (fullCache == null) { + fullCache = this; + } + return fullCache.getAttributes().contains(attribute.getAttributeName(yes)); + } + + public boolean hasStaticMap() { + return StaticMapsProvider.hasStaticMap(this); + } + + public List<Image> getImages() { + List<Image> result = new ArrayList<Image>(); + result.addAll(getSpoilers()); + for (LogEntry log : getLogs()) { + result.addAll(log.getLogImages()); + } + return result; + } + + public void setDetailedUpdatedNow() { + final long now = System.currentTimeMillis(); + setUpdated(now); + setDetailedUpdate(now); + setDetailed(true); + } + + /** + * Gets whether the user has logged the specific log type for this cache. Only checks the currently stored logs of + * the cache, so the result might be wrong. + */ + public boolean hasOwnLog(LogType logType) { + for (LogEntry logEntry : getLogs()) { + if (logEntry.type == logType && logEntry.isOwn()) { + return true; + } + } + return false; + } +} |