/* * Copyright 2010, 2011, 2012 mapsforge.org * Copyright 2013 Hannes Janetzek * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later version. * * This program 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ package org.oscim.theme; import java.util.ArrayList; import java.util.List; import org.oscim.core.Tag; import org.oscim.theme.renderinstruction.RenderInstruction; import org.oscim.utils.LRUCache; import org.xml.sax.Attributes; import android.graphics.Color; /** * A RenderTheme defines how ways and nodes are drawn. */ public class RenderTheme { private final static String TAG = RenderTheme.class.getName(); private static final int MATCHING_CACHE_SIZE = 512; private static final int RENDER_THEME_VERSION = 1; private static void validate(String elementName, Integer version, float baseStrokeWidth, float baseTextSize) { if (version == null) { throw new IllegalArgumentException("missing attribute version for element:" + elementName); } else if (version.intValue() != RENDER_THEME_VERSION) { throw new IllegalArgumentException("invalid render theme version:" + version); } else if (baseStrokeWidth < 0) { throw new IllegalArgumentException("base-stroke-width must not be negative: " + baseStrokeWidth); } else if (baseTextSize < 0) { throw new IllegalArgumentException("base-text-size must not be negative: " + baseTextSize); } } static RenderTheme create(String elementName, Attributes attributes) { Integer version = null; int mapBackground = Color.WHITE; float baseStrokeWidth = 1; float baseTextSize = 1; for (int i = 0; i < attributes.getLength(); ++i) { String name = attributes.getLocalName(i); String value = attributes.getValue(i); if ("schemaLocation".equals(name)) { continue; } else if ("version".equals(name)) { version = Integer.valueOf(Integer.parseInt(value)); } else if ("map-background".equals(name)) { mapBackground = Color.parseColor(value); } else if ("base-stroke-width".equals(name)) { baseStrokeWidth = Float.parseFloat(value); } else if ("base-text-size".equals(name)) { baseTextSize = Float.parseFloat(value); } else { RenderThemeHandler.logUnknownAttribute(elementName, name, value, i); } } validate(elementName, version, baseStrokeWidth, baseTextSize); return new RenderTheme(mapBackground, baseStrokeWidth, baseTextSize); } private final float mBaseStrokeWidth; private final float mBaseTextSize; private int mLevels; private final int mMapBackground; private final ArrayList<Rule> mRulesList; private final LRUCache<MatchingCacheKey, RenderInstructionItem> mNodesCache; private final LRUCache<MatchingCacheKey, RenderInstructionItem> mWayCache; private final LRUCache<MatchingCacheKey, RenderInstructionItem> mAreaCache; private MatchingCacheKey mAreaCacheKey = new MatchingCacheKey(); private MatchingCacheKey mWayCacheKey = new MatchingCacheKey(); private MatchingCacheKey mNodeCacheKey = new MatchingCacheKey(); private ArrayList<RenderInstruction> mWayInstructionList = new ArrayList<RenderInstruction>(4); private ArrayList<RenderInstruction> mAreaInstructionList = new ArrayList<RenderInstruction>(4); private ArrayList<RenderInstruction> mNodeInstructionList = new ArrayList<RenderInstruction>(4); private RenderInstructionItem mPrevAreaItem; private RenderInstructionItem mPrevWayItem; private RenderInstructionItem mPrevNodeItem; class RenderInstructionItem { RenderInstructionItem next; int zoom; RenderInstruction[] list; MatchingCacheKey key; } RenderTheme(int mapBackground, float baseStrokeWidth, float baseTextSize) { mMapBackground = mapBackground; mBaseStrokeWidth = baseStrokeWidth; mBaseTextSize = baseTextSize; mRulesList = new ArrayList<Rule>(); mNodesCache = new LRUCache<MatchingCacheKey, RenderInstructionItem>( MATCHING_CACHE_SIZE); mWayCache = new LRUCache<MatchingCacheKey, RenderInstructionItem>( MATCHING_CACHE_SIZE); mAreaCache = new LRUCache<MatchingCacheKey, RenderInstructionItem>( MATCHING_CACHE_SIZE); } /** * Must be called when this RenderTheme gets destroyed to clean up and free * resources. */ public void destroy() { mNodesCache.clear(); mAreaCache.clear(); mWayCache.clear(); for (int i = 0, n = mRulesList.size(); i < n; ++i) { mRulesList.get(i).onDestroy(); } } /** * @return the number of distinct drawing levels required by this * RenderTheme. */ public int getLevels() { return mLevels; } /** * @return the map background color of this RenderTheme. * @see Color */ public int getMapBackground() { return mMapBackground; } private static void render(IRenderCallback renderCallback, RenderInstruction[] renderInstructions, Tag[] tags) { for (int i = 0, n = renderInstructions.length; i < n; i++) renderInstructions[i].renderNode(renderCallback, tags); } /** * @param renderCallback * ... * @param tags * ... * @param zoomLevel * ... * @return ... */ public RenderInstruction[] matchNode(IRenderCallback renderCallback, Tag[] tags, byte zoomLevel) { // list of renderinsctruction items in cache RenderInstructionItem ris = null; // the item matching tags and zoomlevel RenderInstructionItem ri = null; synchronized (mNodesCache) { int zoomMask = 1 << zoomLevel; MatchingCacheKey cacheKey = mNodeCacheKey; if (mPrevNodeItem == null || (mPrevNodeItem.zoom & zoomMask) == 0) { // previous instructions zoom does not match cacheKey.set(tags, null); } else { // compare if tags match previous instructions if (cacheKey.set(tags, mPrevNodeItem.key)) ri = mPrevNodeItem; } if (ri == null) { boolean found = mNodesCache.containsKey(cacheKey); if (found) { ris = mNodesCache.get(cacheKey); for (ri = ris; ri != null; ri = ri.next) if ((ri.zoom & zoomMask) != 0) // cache hit break; } } if (ri == null) { // cache miss List<RenderInstruction> matches = mNodeInstructionList; matches.clear(); for (int i = 0, n = mRulesList.size(); i < n; ++i) mRulesList.get(i).matchNode(renderCallback, tags, zoomLevel, matches); int size = matches.size(); // check if same instructions are used in another level for (ri = ris; ri != null; ri = ri.next) { if (size == 0) { if (ri.list != null) continue; // both matchinglists are empty break; } if (ri.list == null) continue; if (ri.list.length != size) continue; int i = 0; for (RenderInstruction r : ri.list) { if (r != matches.get(i)) break; i++; } if (i == size) // both matching lists contain the same items break; } if (ri != null) { // we found a same matchting list on another zoomlevel ri.zoom |= zoomMask; } else { ri = new RenderInstructionItem(); ri.zoom = zoomMask; if (size > 0) { ri.list = new RenderInstruction[size]; matches.toArray(ri.list); } // attach this list to the one found for MatchingKey if (ris != null) { ri.next = ris.next; ri.key = ris.key; ris.next = ri; } else { ri.key = new MatchingCacheKey(cacheKey); mNodesCache.put(ri.key, ri); } } } } if (ri.list != null) render(renderCallback, ri.list, tags); mPrevNodeItem = ri; return ri.list; } //private int missCnt = 0; //private int hitCnt = 0; /** * Matches a way with the given parameters against this RenderTheme. * * @param renderCallback * the callback implementation which will be executed on each * match. * @param tags * the tags of the way. * @param zoomLevel * the zoom level at which the way should be matched. * @param closed * way is Closed * @param render * ... * @return currently processed render instructions */ public RenderInstruction[] matchWay(IRenderCallback renderCallback, Tag[] tags, byte zoomLevel, boolean closed, boolean render) { // list of renderinsctruction items in cache RenderInstructionItem ris = null; RenderInstructionItem prevInstructions = null; // the item matching tags and zoomlevel RenderInstructionItem ri = null; // temporary matching instructions list List<RenderInstruction> matches; LRUCache<MatchingCacheKey, RenderInstructionItem> matchingCache; MatchingCacheKey cacheKey; if (closed) matchingCache = mAreaCache; else matchingCache = mWayCache; int zoomMask = 1 << zoomLevel; synchronized (matchingCache) { if (closed) { cacheKey = mAreaCacheKey; matches = mAreaInstructionList; prevInstructions = mPrevAreaItem; } else { cacheKey = mWayCacheKey; matches = mWayInstructionList; prevInstructions = mPrevWayItem; } if (prevInstructions == null || (prevInstructions.zoom & zoomMask) == 0) { // previous instructions zoom does not match cacheKey.set(tags, null); } else { // compare if tags match previous instructions if (cacheKey.set(tags, prevInstructions.key)) { //Log.d(TAG, "same as previous " + Arrays.deepToString(tags)); ri = prevInstructions; } } if (ri == null) { ris = matchingCache.get(cacheKey); for (ri = ris; ri != null; ri = ri.next) if ((ri.zoom & zoomMask) != 0) // cache hit break; } if (ri == null) { // cache miss //Log.d(TAG, missCnt++ + " / " + hitCnt + " Cache Miss"); int c = (closed ? Closed.YES : Closed.NO); //List<RenderInstruction> matches = mMatchingList; matches.clear(); for (int i = 0, n = mRulesList.size(); i < n; ++i) mRulesList.get(i).matchWay(renderCallback, tags, zoomLevel, c, matches); int size = matches.size(); // check if same instructions are used in another level for (ri = ris; ri != null; ri = ri.next) { if (size == 0) { if (ri.list != null) continue; // both matchinglists are empty break; } if (ri.list == null) continue; if (ri.list.length != size) continue; int i = 0; for (RenderInstruction r : ri.list) { if (r != matches.get(i)) break; i++; } if (i == size) // both matching lists contain the same items break; } if (ri != null) { // we found a same matchting list on another zoomlevel ri.zoom |= zoomMask; //Log.d(TAG, // zoomLevel + " same instructions " + size + " " // + Arrays.deepToString(tags)); } else { //Log.d(TAG, // zoomLevel + " new instructions " + size + " " // + Arrays.deepToString(tags)); ri = new RenderInstructionItem(); ri.zoom = zoomMask; if (size > 0) { ri.list = new RenderInstruction[size]; matches.toArray(ri.list); } // attach this list to the one found for MatchingKey if (ris != null) { ri.next = ris.next; ri.key = ris.key; ris.next = ri; } else { ri.key = new MatchingCacheKey(cacheKey); matchingCache.put(ri.key, ri); } } } if (closed) mPrevAreaItem = ri; else mPrevWayItem = ri; } if (render && ri.list != null) { for (int i = 0, n = ri.list.length; i < n; i++) ri.list[i].renderWay(renderCallback, tags); } return ri.list; } void addRule(Rule rule) { mRulesList.add(rule); } void complete() { mRulesList.trimToSize(); for (int i = 0, n = mRulesList.size(); i < n; ++i) { mRulesList.get(i).onComplete(); } } /** * Scales the stroke width of this RenderTheme by the given factor. * * @param scaleFactor * the factor by which the stroke width should be scaled. */ public void scaleStrokeWidth(float scaleFactor) { for (int i = 0, n = mRulesList.size(); i < n; ++i) { mRulesList.get(i).scaleStrokeWidth(scaleFactor * mBaseStrokeWidth); } } /** * Scales the text size of this RenderTheme by the given factor. * * @param scaleFactor * the factor by which the text size should be scaled. */ public void scaleTextSize(float scaleFactor) { for (int i = 0, n = mRulesList.size(); i < n; ++i) { mRulesList.get(i).scaleTextSize(scaleFactor * mBaseTextSize); } } void setLevels(int levels) { mLevels = levels; } }