package cgeo.geocaching.utils; import cgeo.geocaching.R; import cgeo.geocaching.compatibility.Compatibility; import cgeo.geocaching.enumerations.CacheListType; import cgeo.geocaching.log.LogType; import cgeo.geocaching.models.Geocache; import cgeo.geocaching.models.Waypoint; import org.apache.commons.lang3.builder.HashCodeBuilder; import android.support.annotation.NonNull; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.support.annotation.Nullable; import android.util.SparseArray; import java.util.ArrayList; import java.util.List; public final class MapUtils { // data for overlays private static final int[][] INSET_RELIABLE = { { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 } }; // center, 25x30 / 33x40 / 45x51 / 60x68 / 90x102 / 120x136 private static final int[][] INSET_TYPE = { { 3, 6, 4, 7 }, { 5, 8, 6, 10 }, { 4, 4, 5, 11 }, { 5, 5, 6, 14 }, { 8, 8, 10, 22 }, { 10, 10, 13, 29 } }; private static final int[][] INSET_TYPE_LIST = { { 1, 1, 1, 1 }, { 2, 2, 2, 2 }, { 3, 3, 3, 3 }, { 4, 4, 4, 4 }, { 6, 6, 6, 6 }, { 8, 8, 8, 8 } }; private static final int[][] INSET_OWN = { { 15, 0, 0, 21 }, { 21, 0, 0, 28 }, { 27, 0, 0, 33 }, { 36, 0, 0, 44 }, { 54, 0, 0, 66 }, { 72, 0, 0, 88 } }; private static final int[][] INSET_OWN_LIST = { { 16, 0, 0, 16 }, { 22, 0, 0, 22 }, { 33, 0, 0, 33 }, { 44, 0, 0, 44 }, { 66, 0, 0, 66 }, { 88, 0, 0, 88 } }; private static final int[][] INSET_FOUND = { { 0, 0, 15, 21 }, { 0, 0, 21, 28 }, { 0, 0, 27, 33 }, { 0, 0, 36, 44 }, { 0, 0, 54, 66 }, { 0, 0, 72, 88 } }; private static final int[][] INSET_FOUND_LIST = { { 0, 0, 16, 16 }, { 0, 0, 22, 22 }, { 0, 0, 33, 33 }, { 0, 0, 44, 44 }, { 0, 0, 66, 66 }, { 0, 0, 88, 88 } }; private static final int[][] INSET_USERMODIFIEDCOORDS = { { 12, 17, 0, 0 }, { 16, 23, 0, 0 }, { 19, 25, 0, 0 }, { 26, 34, 0, 0 }, { 39, 51, 0, 0 }, { 52, 68, 0, 0 } }; private static final int[][] INSET_USERMODIFIEDCOORDS_LIST = { { 16, 14, 0, 2 }, { 22, 19, 0, 3 }, { 33, 28, 0, 4 }, { 44, 38, 0, 6 }, { 66, 57, 0, 9 }, { 88, 76, 0, 12 } }; private static final int[][] INSET_PERSONALNOTE = { { 0, 17, 12, 0 }, { 0, 23, 16, 0 }, { 0, 25, 19, 0 }, { 0, 34, 26, 0 }, { 0, 51, 39, 0 }, { 0, 68, 52, 0 } }; private static final int[][] INSET_PERSONALNOTE_LIST = { { 0, 14, 16, 2 }, { 0, 19, 22, 3 }, { 0, 28, 33, 4 }, { 0, 38, 44, 6 }, { 0, 57, 66, 9 }, { 0, 76, 88, 12 } }; private static final SparseArray<LayerDrawable> overlaysCache = new SparseArray<>(); private MapUtils() { // Do not instantiate } /** * Obtain the drawable for a given cache, with background circle. * * @param res * the resources to use * @param cache * the cache to build the drawable for * @return * a drawable representing the current cache status */ @NonNull public static LayerDrawable getCacheMarker(final Resources res, final Geocache cache) { return getCacheMarker(res, cache, null); } /** * Obtain the drawable for a given cache. * Return a drawable from the cache, if a similar drawable was already generated. * * cacheListType should be Null if the requesting activity is Map. * * @param res * the resources to use * @param cache * the cache to build the drawable for * @param cacheListType * the current CacheListType or Null * @return * a drawable representing the current cache status */ @NonNull public static LayerDrawable getCacheMarker(final Resources res, final Geocache cache, @Nullable final CacheListType cacheListType) { final int hashcode = new HashCodeBuilder() .append(cache.isReliableLatLon()) .append(cache.getType().id) .append(cache.isDisabled() || cache.isArchived()) .append(cache.getMapMarkerId()) .append(cache.isOwner()) .append(cache.isFound()) .append(showUserModifiedCoords(cache)) .append(cache.getPersonalNote()) .append(cache.isLogOffline()) .append(!cache.getLists().isEmpty()) .append(cache.getOfflineLogType()) .append(showBackground(cacheListType)) .append(showFloppyOverlay(cacheListType)) .toHashCode(); synchronized (overlaysCache) { LayerDrawable drawable = overlaysCache.get(hashcode); if (drawable == null) { drawable = createCacheMarker(res, cache, cacheListType); overlaysCache.put(hashcode, drawable); } return drawable; } } /** * Obtain the drawable for a given waypoint. * Return a drawable from the cache, if a similar drawable was already generated. * * @param res * the resources to use * @param waypoint * the waypoint to build the drawable for * @return * a drawable representing the current waypoint status */ @NonNull public static LayerDrawable getWaypointMarker(final Resources res, final Waypoint waypoint) { final int hashcode = new HashCodeBuilder() .append(waypoint.isVisited()) .append(waypoint.getWaypointType().id) .toHashCode(); synchronized (overlaysCache) { LayerDrawable drawable = overlaysCache.get(hashcode); if (drawable == null) { drawable = createWaypointMarker(res, waypoint); overlaysCache.put(hashcode, drawable); } return drawable; } } /** * Build the drawable for a given waypoint. * * @param res * the resources to use * @param waypoint * the waypoint to build the drawable for * @return * a drawable representing the current waypoint status */ @NonNull private static LayerDrawable createWaypointMarker(final Resources res, final Waypoint waypoint) { final Drawable marker = Compatibility.getDrawable(res, !waypoint.isVisited() ? R.drawable.marker : R.drawable.marker_transparent); final Drawable[] layers = { marker, Compatibility.getDrawable(res, waypoint.getWaypointType().markerId) }; final LayerDrawable drawable = new LayerDrawable(layers); final int resolution = calculateResolution(marker); drawable.setLayerInset(1, INSET_TYPE[resolution][0], INSET_TYPE[resolution][1], INSET_TYPE[resolution][2], INSET_TYPE[resolution][3]); return drawable; } /** * Clear the cache of drawable items. */ public static void clearCachedItems() { synchronized (overlaysCache) { overlaysCache.clear(); } } /** * Build the drawable for a given cache. * * @param res * the resources to use * @param cache * the cache to build the drawable for * @param cacheListType * the current CacheListType or Null * @return * a drawable representing the current cache status */ @NonNull private static LayerDrawable createCacheMarker(final Resources res, final Geocache cache, @Nullable final CacheListType cacheListType) { // Set initial capacities to the maximum of layers and insets to avoid dynamic reallocation final List<Drawable> layers = new ArrayList<>(9); final List<int[]> insets = new ArrayList<>(8); // background: disabled or not final Drawable marker = Compatibility.getDrawable(res, cache.getMapMarkerId()); final int resolution = calculateResolution(marker); // Show the background circle only on map if (showBackground(cacheListType)) { layers.add(marker); insets.add(INSET_RELIABLE[resolution]); } // reliable or not if (!cache.isReliableLatLon() && showUnreliableLatLon(cacheListType)) { insets.add(INSET_RELIABLE[resolution]); layers.add(Compatibility.getDrawable(res, R.drawable.marker_notreliable)); } // cache type layers.add(Compatibility.getDrawable(res, cache.getType().markerId)); insets.add(getTypeInset(cacheListType)[resolution]); // own if (cache.isOwner()) { layers.add(Compatibility.getDrawable(res, R.drawable.marker_own)); insets.add(getOwnInset(cacheListType)[resolution]); // if not, checked if stored } else if (!cache.getLists().isEmpty() && showFloppyOverlay(cacheListType)) { layers.add(Compatibility.getDrawable(res, R.drawable.marker_stored)); insets.add(getOwnInset(cacheListType)[resolution]); } // found if (cache.isFound()) { layers.add(Compatibility.getDrawable(res, R.drawable.marker_found)); insets.add(getFoundInset(cacheListType)[resolution]); // if not, perhaps logged offline } else if (cache.isLogOffline()) { final LogType offlineLogType = cache.getOfflineLogType(); if (offlineLogType == null) { // Default, backward compatible layers.add(Compatibility.getDrawable(res, R.drawable.marker_found_offline)); } else { layers.add(Compatibility.getDrawable(res, offlineLogType.getOfflineLogOverlay())); } insets.add(getFoundInset(cacheListType)[resolution]); } // user modified coords if (showUserModifiedCoords(cache)) { layers.add(Compatibility.getDrawable(res, R.drawable.marker_usermodifiedcoords)); insets.add(getUMCInset(cacheListType)[resolution]); } // personal note if (cache.getPersonalNote() != null) { layers.add(Compatibility.getDrawable(res, R.drawable.marker_personalnote)); insets.add(getPNInset(cacheListType)[resolution]); } final LayerDrawable ld = new LayerDrawable(layers.toArray(new Drawable[layers.size()])); int index = 0; for (final int[] inset : insets) { ld.setLayerInset(index++, inset[0], inset[1], inset[2], inset[3]); } return ld; } /** * Get the resolution index used for positioning the overlays elements. * * @param marker * The Drawable reference * @return * an index for the overlays positions */ private static int calculateResolution(final Drawable marker) { return marker.getIntrinsicWidth() >= 30 ? (marker.getIntrinsicWidth() >= 40 ? (marker.getIntrinsicWidth() >= 50 ? (marker.getIntrinsicWidth() >= 70 ? (marker.getIntrinsicWidth() >= 100 ? 5 : 4) : 3) : 2) : 1) : 0; } /** * Conditional expression to choose if we need the background circle or not. * * @param cacheListType * The cache list currently used * @return * True if the background circle should be displayed */ private static boolean showBackground(@Nullable final CacheListType cacheListType) { return cacheListType == null; } /** * Conditional expression to choose if we need the orange circle or not. * The orange circle indicate an approximative cache position. * * @param cacheListType * The cache list currently used * @return * True if the background circle should be displayed */ private static boolean showUnreliableLatLon(@Nullable final CacheListType cacheListType) { // Show only on map return cacheListType == null; } /** * Conditional expression to choose if we need the UserModifiedCoords flag or not. * * @param cache * The cache currently used * @return * True if the UserModifiedCoords flag should be displayed */ private static boolean showUserModifiedCoords(final Geocache cache) { return cache.hasUserModifiedCoords() || cache.hasFinalDefined(); } /** * Conditional expression to choose if we need the floppy overlay or not. * * @param cacheListType * The cache list currently used * @return * True if the floppy overlay should be displayed */ private static boolean showFloppyOverlay(@Nullable final CacheListType cacheListType) { return cacheListType == null || cacheListType != CacheListType.OFFLINE; } private static int[][] getTypeInset(@Nullable final CacheListType cacheListType) { return cacheListType == null ? INSET_TYPE : INSET_TYPE_LIST; } private static int[][] getOwnInset(@Nullable final CacheListType cacheListType) { return cacheListType == null ? INSET_OWN : INSET_OWN_LIST; } private static int[][] getFoundInset(@Nullable final CacheListType cacheListType) { return cacheListType == null ? INSET_FOUND : INSET_FOUND_LIST; } private static int[][] getUMCInset(@Nullable final CacheListType cacheListType) { return cacheListType == null ? INSET_USERMODIFIEDCOORDS : INSET_USERMODIFIEDCOORDS_LIST; } private static int[][] getPNInset(@Nullable final CacheListType cacheListType) { return cacheListType == null ? INSET_PERSONALNOTE : INSET_PERSONALNOTE_LIST; } }