/** * Copyright (C) 2016 eBusiness Information * * This file is part of OSM Contributor. * * OSM Contributor is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * OSM Contributor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OSM Contributor. If not, see <http://www.gnu.org/licenses/>. */ package io.jawg.osmcontributor.ui.utils; import android.app.Application; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.support.v4.content.ContextCompat; import android.support.v4.util.LruCache; import java.util.HashMap; import java.util.Map; import javax.inject.Inject; import javax.inject.Singleton; import io.jawg.osmcontributor.R; import io.jawg.osmcontributor.model.entities.Note; import io.jawg.osmcontributor.model.entities.Poi; import io.jawg.osmcontributor.model.entities.PoiNodeRef; import io.jawg.osmcontributor.model.entities.PoiType; /** * Handle the bitmaps and cache them into the memory for future uses. */ @Singleton public class BitmapHandler { public static final String BITMAP_NOTE_ID = "BITMAP_NOTE_ID"; public static final String BITMAP_POI_NODE_REF_ID = "BITMAP_NODE_ID"; private LruCache<String, Bitmap> cache; private final Map<String, Integer> icons = new HashMap<>(); private final Context context; @Inject public BitmapHandler(Application osmTemplateApplication) { context = osmTemplateApplication.getApplicationContext(); final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // Use 1/5th of the available memory for this memory cache. final int cacheSize = maxMemory / 5; cache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // The cache size will be measured in kilobytes rather than items count return bitmap.getByteCount() / 1024; } }; icons.put("administrative", R.drawable.administrative); icons.put("animal_shelter", R.drawable.pet2); icons.put("antique", R.drawable.archaeological); icons.put("gallery", R.drawable.art_gallery2); icons.put("boat_sharing", R.drawable.sailing); icons.put("books", R.drawable.library); icons.put("boutique", R.drawable.convenience); icons.put("brothel", R.drawable.hotel2); icons.put("bureau_de_change", R.drawable.currency_exchange); icons.put("camp_site", R.drawable.camping); icons.put("car_wash", R.drawable.ford); icons.put("car_sharing", R.drawable.car_share); icons.put("clinic", R.drawable.doctors2); icons.put("arts_centre", R.drawable.art_gallery2); icons.put("dry_cleaning", R.drawable.laundrette); icons.put("archaeological_site", R.drawable.archaeological2); icons.put("farm", R.drawable.marketplace); icons.put("variety_store", R.drawable.convenience); icons.put("food_court", R.drawable.marketplace); icons.put("fort", R.drawable.castle2); icons.put("general", R.drawable.department_store); icons.put("hardware", R.drawable.mine); icons.put("doityourself", R.drawable.diy); icons.put("organic", R.drawable.greengrocer); icons.put("laundry", R.drawable.laundrette); icons.put("kindergarten", R.drawable.nursery3); icons.put("manor", R.drawable.house); icons.put("alpine_hut", R.drawable.alpinehut); icons.put("wilderness_hut", R.drawable.alpinehut); icons.put("ship", R.drawable.sailing); icons.put("music_venue", R.drawable.music); icons.put("music_venue", R.drawable.music); icons.put("nursing_home", R.drawable.hotel2); icons.put("optician", R.drawable.opticians); icons.put("viewpoint", R.drawable.photo); icons.put("crossing", R.drawable.zebra_crossing); icons.put("guest_house", R.drawable.bed_and_breakfast2); icons.put("photo_booth", R.drawable.photo); icons.put("picnic_site", R.drawable.picnic); icons.put("services", R.drawable.picnic); icons.put("public_bookcase", R.drawable.library); icons.put("register_office", R.drawable.administrative); icons.put("mini_roundabout", R.drawable.roundabout_anticlockwise); icons.put("ruins", R.drawable.ruin); icons.put("caravan_site", R.drawable.caravan_park); icons.put("tailor", R.drawable.clothes); icons.put("taxi", R.drawable.taxi_rank); icons.put("tomb", R.drawable.memorial); icons.put("travel_agency", R.drawable.aerodrome); icons.put("video", R.drawable.video_rental); icons.put("waste_basket", R.drawable.waste_bin); icons.put("waste_basket", R.drawable.waste_bin); icons.put("artwork", R.drawable.art_gallery); icons.put("aerodrome", R.drawable.aerodrome); icons.put("aircraft", R.drawable.aerodrome); icons.put("alcohol", R.drawable.bar); icons.put("atm", R.drawable.atm); icons.put("attraction", R.drawable.attraction); icons.put("bank", R.drawable.bank); icons.put("bar", R.drawable.bar); icons.put("bus_stop", R.drawable.bus_stop); icons.put("bicycle_parking", R.drawable.bicycle_parking); icons.put("bicycle_rental", R.drawable.bicycle_rental); icons.put("biergarten", R.drawable.biergarten); icons.put("cafe", R.drawable.cafe); icons.put("car_rental", R.drawable.car_rental); icons.put("church", R.drawable.place_of_worship); icons.put("cinema", R.drawable.cinema); icons.put("city", R.drawable.town); icons.put("commercial", R.drawable.mall); icons.put("courthouse", R.drawable.courthouse); icons.put("dentist", R.drawable.dentist); icons.put("doctors", R.drawable.doctors); icons.put("drinking_water", R.drawable.drinking_water); icons.put("embassy", R.drawable.embassy); icons.put("entrance", R.drawable.entrance); icons.put("fast_food", R.drawable.fast_food); icons.put("fire_station", R.drawable.fire_station); icons.put("fuel", R.drawable.fuel); icons.put("hamlet", R.drawable.town); icons.put("hospital", R.drawable.hospital); icons.put("hotel", R.drawable.hotel); icons.put("house", R.drawable.house); icons.put("housenumber", R.drawable.house); icons.put("hunting_stand", R.drawable.hunting_stand); icons.put("locality", R.drawable.town); icons.put("mall", R.drawable.mall); icons.put("nightclub", R.drawable.nightclub); icons.put("neighbourhood", R.drawable.house); icons.put("parking", R.drawable.parking); icons.put("pharmacy", R.drawable.pharmacy); icons.put("place_of_worship", R.drawable.place_of_worship); icons.put("police", R.drawable.police); icons.put("political", R.drawable.administrative); icons.put("primary", R.drawable.street); icons.put("prison", R.drawable.prison); icons.put("pub", R.drawable.pub); icons.put("recycling", R.drawable.recycling); icons.put("religious_administrative", R.drawable.place_of_worship); icons.put("residential", R.drawable.house); icons.put("restaurant", R.drawable.restaurant); icons.put("retail", R.drawable.mall); icons.put("road", R.drawable.street); icons.put("secondary", R.drawable.street); icons.put("stadium", R.drawable.stadium); icons.put("station", R.drawable.bus_stop); icons.put("street", R.drawable.street); icons.put("suburb", R.drawable.town); icons.put("subway_entrance", R.drawable.bus_stop); icons.put("platform", R.drawable.bus_stop); icons.put("supermarket", R.drawable.mall); icons.put("terminal", R.drawable.aerodrome); icons.put("tertiary", R.drawable.street); icons.put("theatre", R.drawable.theatre); icons.put("toilets", R.drawable.toilets); icons.put("town", R.drawable.town); icons.put("townhall", R.drawable.townhall); icons.put("track", R.drawable.street); icons.put("village", R.drawable.town); icons.put("phone", R.drawable.sos); } /** * Add a bitmap to the memory cache. * * @param key Key in the cache. * @param bitmap Bitmap to add. */ public void addBitmapToMemoryCache(String key, Bitmap bitmap) { cache.put(key, bitmap); } /** * Get a bitmap from the memory cache. * * @param key Key of the bitmap in the cache. * @return The Bitmap. */ public Bitmap getBitmapFromMemCache(String key) { return cache.get(key); } /** * Handle Bitmap load, storage and retrieval for MapFragment. * <br/> * Put the marker corresponding to a poiType into a color pin. The color depends of the poi's state. * * @param poiType The PoiType of the desired bitmap. * @param state State of the Poi. * @return The marker corresponding to the poiType and the poi state. */ public Bitmap getMarkerBitmap(PoiType poiType, Poi.State state) { try { Integer markerId = null; Integer iconId = getIconDrawableId(poiType); Bitmap markerWrapper; Bitmap icon; // 3 states, n-pois -> 3 x n bitmaps. // Two choices: either we store all overlay combinations, or we store the markers and always process overlays. // As there might be more POIs than the number of combinations, I chose to store all combinations. switch (state) { case NORMAL: markerId = R.drawable.marker_white; break; case NOT_SYNCED: markerId = R.drawable.marker_grey; break; case SELECTED: markerId = R.drawable.marker_blue; break; case MOVING: markerId = R.drawable.marker_red; break; } String bitmapCacheId = markerId.toString() + iconId.toString(); // Try to retrieve bmOverlay from cache Bitmap bmOverlay = getBitmapFromMemCache(bitmapCacheId); // If we don't have the combination into memory yet, compute it manually if (bmOverlay == null) { // If still too slow (lots of sources), we might change this and also include partials into cache // Right now, I don't think the use case proves its usefulness markerWrapper = BitmapFactory.decodeResource(context.getResources(), markerId); icon = BitmapFactory.decodeResource(context.getResources(), iconId); bmOverlay = Bitmap.createBitmap(markerWrapper.getWidth(), markerWrapper.getHeight(), markerWrapper.getConfig()); Canvas canvas = new Canvas(bmOverlay); int x = markerWrapper.getWidth() / 2 - icon.getWidth() / 2; int y = markerWrapper.getHeight() / 2 - icon.getHeight() / 2 - (int) (0.05 * markerWrapper.getHeight()); canvas.drawBitmap(markerWrapper, 0, 0, null); canvas.drawBitmap(icon, x, y, null); addBitmapToMemoryCache(bitmapCacheId, bmOverlay); } return bmOverlay; } catch (Exception e) { throw new IllegalStateException(e); } } /** * Get the white icon corresponding to a poiType. * * @param poiType the PoiType or null for notes. * @return The white icon. */ public Drawable getIconWhite(PoiType poiType) { Bitmap myBitmap = BitmapFactory.decodeResource(context.getResources(), poiType == null ? R.drawable.open_book : getIconDrawableId(poiType)); myBitmap = myBitmap.copy(myBitmap.getConfig(), true); int[] allpixels = new int[myBitmap.getHeight() * myBitmap.getWidth()]; myBitmap.getPixels(allpixels, 0, myBitmap.getWidth(), 0, 0, myBitmap.getWidth(), myBitmap.getHeight()); for (int i = 0; i < myBitmap.getHeight() * myBitmap.getWidth(); i++) { if (allpixels[i] != 0) { int A = Color.alpha(allpixels[i]); // inverting byte for each R/G/B channel int R = 255 - Color.red(allpixels[i]); int G = 255 - Color.green(allpixels[i]); int B = 255 - Color.blue(allpixels[i]); // set newly-inverted pixel to output image allpixels[i] = Color.argb(A, R, G, B); } } myBitmap.setPixels(allpixels, 0, myBitmap.getWidth(), 0, 0, myBitmap.getWidth(), myBitmap.getHeight()); return new BitmapDrawable(context.getResources(), myBitmap); } /** * Get the icon drawable id corresponding to the PoiType id. * * @param poiType The PoiType. * @return The icon drawable id. */ public Integer getIconDrawableId(PoiType poiType) { return getDrawableId(poiType == null ? "" : poiType.getIcon()); } /** * Get the note bitmap with the color corresponding to the state of the note. * * @param state The state of the note. * @return The bitmap in the corresponding color. */ public Bitmap getNoteBitmap(Note.State state) { // Try to retrieve bmOverlay from cache Bitmap bmOverlay = getBitmapFromMemCache(state.toString()); Integer markerId; // If we don't have the combination into memory yet, compute it manually if (bmOverlay == null) { switch (state) { case OPEN: markerId = R.drawable.note_pink; break; case CLOSED: markerId = R.drawable.note_green; break; case SYNC: markerId = R.drawable.note_grey; break; case SELECTED: markerId = R.drawable.note_blue; break; case MOVING: markerId = R.drawable.note_orange; break; default: markerId = R.drawable.note_green; break; } // If still too slow (lots of sources), we might change this and also include partials into cache // Right now, I don't think the use case proves its usefulness bmOverlay = BitmapFactory.decodeResource(context.getResources(), markerId); addBitmapToMemoryCache(BITMAP_NOTE_ID + state.toString(), bmOverlay); } return bmOverlay; } /** * Get the nodeRef bitmap with the color corresponding to the state of the node. * * @param state The state of the node. * @return The bitmap in the corresponding color. */ public Bitmap getNodeRefBitmap(PoiNodeRef.State state) { // Try to retrieve bmOverlay from cache Bitmap bmOverlay = getBitmapFromMemCache(state.toString()); Integer markerId; // If we don't have the combination into memory yet, compute it manually if (bmOverlay == null) { switch (state) { case NONE: markerId = R.drawable.waypoint; break; case SELECTED: markerId = R.drawable.waypoint_selected; break; case MOVING: markerId = R.drawable.waypoint_edition; break; default: markerId = R.drawable.waypoint; break; } // If still too slow (lots of sources), we might change this and also include partials into cache // Right now, I don't think the use case proves its usefulness bmOverlay = drawableToBitmap(ContextCompat.getDrawable(context, markerId)); addBitmapToMemoryCache(BITMAP_POI_NODE_REF_ID + state.toString(), bmOverlay); } return bmOverlay; } public static Bitmap drawableToBitmap(Drawable drawable) { if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } /** * Get the drawable corresponding to the icon name. * * @param iconName The icon name. * @return The drawable. */ public Drawable getDrawable(String iconName) { return ContextCompat.getDrawable(context, getDrawableId(iconName)); } /** * Get the drawable id corresponding to the icon name. * * @param iconName The icon name. * @return The drawable id. */ int getDrawableId(String iconName) { int iconId = 0; if (iconName != null) { if (icons.containsKey(iconName)) { return icons.get(iconName); } else { iconId = context.getResources().getIdentifier(iconName, "drawable", context.getPackageName()); } } return iconId != 0 ? iconId : R.drawable.default_osm_marker; } }