From bf75d3bead3d8c27afe29c946e04875e189c1927 Mon Sep 17 00:00:00 2001
From: Hannes Janetzek <hannes.janetzek@gmail.com>
Date: Thu, 24 Jan 2013 22:04:39 +0100
Subject: [PATCH] - use only one RenderInstruction cache item when instructions
 are the same for multiple zoom-level - do common checks in Rule instead of
 calling interface methods

---
 src/org/oscim/theme/MatchingCacheKey.java |  28 ++-
 src/org/oscim/theme/NegativeRule.java     |  13 +-
 src/org/oscim/theme/PositiveRule.java     |  17 +-
 src/org/oscim/theme/RenderTheme.java      | 226 ++++++++++++++++------
 src/org/oscim/theme/Rule.java             |  30 +--
 src/org/oscim/theme/RuleOptimizer.java    |   5 +-
 6 files changed, 211 insertions(+), 108 deletions(-)

diff --git a/src/org/oscim/theme/MatchingCacheKey.java b/src/org/oscim/theme/MatchingCacheKey.java
index 741c90b1..85fc6a7d 100644
--- a/src/org/oscim/theme/MatchingCacheKey.java
+++ b/src/org/oscim/theme/MatchingCacheKey.java
@@ -19,26 +19,22 @@ import org.oscim.core.Tag;
 class MatchingCacheKey {
 	int mHashCodeValue;
 	Tag[] mTags;
-	byte mZoomLevel;
 
 	MatchingCacheKey() {
 	}
 
-	MatchingCacheKey(Tag[] tags, byte zoomLevel) {
+	MatchingCacheKey(Tag[] tags) {
 		mTags = tags;
-		mZoomLevel = zoomLevel;
 		mHashCodeValue = calculateHashCode();
 	}
 
 	MatchingCacheKey(MatchingCacheKey key) {
 		mTags = key.mTags;
-		mZoomLevel = key.mZoomLevel;
 		mHashCodeValue = key.mHashCodeValue;
 	}
 
-	void set(Tag[] tags, byte zoomLevel) {
+	void set(Tag[] tags) {
 		mTags = tags;
-		mZoomLevel = zoomLevel;
 
 		int result = 7;
 
@@ -47,7 +43,7 @@ class MatchingCacheKey {
 				break;
 			result = 31 * result + mTags[i].hashCode();
 		}
-		result = 31 * result + mZoomLevel;
+		result = 31 * result;
 
 		mHashCodeValue = result;
 	}
@@ -57,15 +53,9 @@ class MatchingCacheKey {
 		if (this == obj) {
 			return true;
 		}
-		// else if (!(obj instanceof MatchingCacheKey)) {
-		// return false;
-		// }
 
 		MatchingCacheKey other = (MatchingCacheKey) obj;
 
-		if (mZoomLevel != other.mZoomLevel)
-			return false;
-
 		if (mTags == null) {
 			return (other.mTags == null);
 		} else if (other.mTags == null)
@@ -76,10 +66,14 @@ class MatchingCacheKey {
 			return false;
 		}
 
-		for (int i = 0; i < length; i++)
-			if (mTags[i] != other.mTags[i])
-				return false;
+		for (int i = 0; i < length; i++) {
+			if (mTags[i] == other.mTags[i])
+				continue;
+			if (mTags[i].key == other.mTags[i].key && mTags[i].value == other.mTags[i].value)
+				continue;
 
+			return false;
+		}
 		return true;
 	}
 
@@ -99,7 +93,7 @@ class MatchingCacheKey {
 				break;
 			result = 31 * result + mTags[i].hashCode();
 		}
-		result = 31 * result + mZoomLevel;
+		result = 31 * result;
 
 		return result;
 	}
diff --git a/src/org/oscim/theme/NegativeRule.java b/src/org/oscim/theme/NegativeRule.java
index 6220759d..8388397f 100644
--- a/src/org/oscim/theme/NegativeRule.java
+++ b/src/org/oscim/theme/NegativeRule.java
@@ -27,17 +27,12 @@ class NegativeRule extends Rule {
 	}
 
 	@Override
-	boolean matchesNode(Tag[] tags, byte zoomLevel) {
-		return mZoomMin <= zoomLevel && mZoomMax >= zoomLevel
-				&& (mElement != Element.WAY)
-				&& mAttributeMatcher.matches(tags);
+	boolean matchesNode(Tag[] tags) {
+		return mAttributeMatcher.matches(tags);
 	}
 
 	@Override
-	boolean matchesWay(Tag[] tags, byte zoomLevel, int closed) {
-		return mZoomMin <= zoomLevel && mZoomMax >= zoomLevel
-				&& (mElement != Element.NODE)
-				&& (mClosed == closed || mClosed == Closed.ANY)
-				&& mAttributeMatcher.matches(tags);
+	boolean matchesWay(Tag[] tags) {
+		return mAttributeMatcher.matches(tags);
 	}
 }
diff --git a/src/org/oscim/theme/PositiveRule.java b/src/org/oscim/theme/PositiveRule.java
index f7b7558f..8843e42f 100644
--- a/src/org/oscim/theme/PositiveRule.java
+++ b/src/org/oscim/theme/PositiveRule.java
@@ -36,21 +36,14 @@ class PositiveRule extends Rule {
 	}
 
 	@Override
-	boolean matchesNode(Tag[] tags, byte zoomLevel) {
-		return (mElement != Element.WAY)
-				&& mZoomMin <= zoomLevel
-				&& mZoomMax >= zoomLevel
-				&& (mKeyMatcher == null || mKeyMatcher.matches(tags))
+	boolean matchesNode(Tag[] tags) {
+		return (mKeyMatcher == null || mKeyMatcher.matches(tags))
 				&& (mValueMatcher == null || mValueMatcher.matches(tags));
 	}
 
 	@Override
-	boolean matchesWay(Tag[] tags, byte zoomLevel, int closed) {
-		return (mElement != Element.NODE)
-				&& mZoomMin <= zoomLevel
-				&& mZoomMax >= zoomLevel
-				&& (mClosed == closed || mClosed == Closed.ANY)
-				&& (mKeyMatcher == null || mKeyMatcher.matches(tags))
-				&& (mValueMatcher == null || mValueMatcher.matches(tags));
+	boolean matchesWay(Tag[] tags) {
+		return (mKeyMatcher == null || mKeyMatcher.matches(tags)) &&
+				(mValueMatcher == null || mValueMatcher.matches(tags));
 	}
 }
diff --git a/src/org/oscim/theme/RenderTheme.java b/src/org/oscim/theme/RenderTheme.java
index bda3fde1..2fb6ace3 100644
--- a/src/org/oscim/theme/RenderTheme.java
+++ b/src/org/oscim/theme/RenderTheme.java
@@ -28,7 +28,9 @@ import android.graphics.Color;
  * A RenderTheme defines how ways and nodes are drawn.
  */
 public class RenderTheme {
-	private static final int MATCHING_CACHE_SIZE = 1024;
+	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,
@@ -82,9 +84,15 @@ public class RenderTheme {
 	private final int mMapBackground;
 	private final ArrayList<Rule> mRulesList;
 
-	private final LRUCache<MatchingCacheKey, RenderInstruction[]> mMatchingCacheNodes;
-	private final LRUCache<MatchingCacheKey, RenderInstruction[]> mMatchingCacheWay;
-	private final LRUCache<MatchingCacheKey, RenderInstruction[]> mMatchingCacheArea;
+	private final LRUCache<MatchingCacheKey, RenderInstructionItem> mMatchingCacheNodes;
+	private final LRUCache<MatchingCacheKey, RenderInstructionItem> mMatchingCacheWay;
+	private final LRUCache<MatchingCacheKey, RenderInstructionItem> mMatchingCacheArea;
+
+	class RenderInstructionItem {
+		RenderInstructionItem next;
+		int zoom;
+		RenderInstruction[] list;
+	}
 
 	RenderTheme(int mapBackground, float baseStrokeWidth, float baseTextSize) {
 		mMapBackground = mapBackground;
@@ -92,11 +100,11 @@ public class RenderTheme {
 		mBaseTextSize = baseTextSize;
 		mRulesList = new ArrayList<Rule>();
 
-		mMatchingCacheNodes = new LRUCache<MatchingCacheKey, RenderInstruction[]>(
+		mMatchingCacheNodes = new LRUCache<MatchingCacheKey, RenderInstructionItem>(
 				MATCHING_CACHE_SIZE);
-		mMatchingCacheWay = new LRUCache<MatchingCacheKey, RenderInstruction[]>(
+		mMatchingCacheWay = new LRUCache<MatchingCacheKey, RenderInstructionItem>(
 				MATCHING_CACHE_SIZE);
-		mMatchingCacheArea = new LRUCache<MatchingCacheKey, RenderInstruction[]>(
+		mMatchingCacheArea = new LRUCache<MatchingCacheKey, RenderInstructionItem>(
 				MATCHING_CACHE_SIZE);
 	}
 
@@ -130,6 +138,12 @@ public class RenderTheme {
 		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
 	 *            ...
@@ -142,41 +156,92 @@ public class RenderTheme {
 	public synchronized RenderInstruction[] matchNode(IRenderCallback renderCallback,
 			Tag[] tags, byte zoomLevel) {
 
-		RenderInstruction[] renderInstructions = null;
+		// list of renderinsctruction items in cache
+		RenderInstructionItem ris = null;
 
-		MatchingCacheKey matchingCacheKey;
+		// the item matching tags and zoomlevel
+		RenderInstructionItem ri = null;
 
-		matchingCacheKey = new MatchingCacheKey(tags, zoomLevel);
+		MatchingCacheKey matchingCacheKey = new MatchingCacheKey(tags);
 		boolean found = mMatchingCacheNodes.containsKey(matchingCacheKey);
+		int zoomMask = 1 << zoomLevel;
+
 		if (found) {
-			renderInstructions = mMatchingCacheNodes.get(matchingCacheKey);
-		} else {
-			// cache miss
-			List<RenderInstruction> matchingList = new ArrayList<RenderInstruction>(4);
+			ris = mMatchingCacheNodes.get(matchingCacheKey);
+
+			for (ri = ris; ri != null; ri = ri.next)
+				if ((ri.zoom & zoomMask) != 0)
+					// cache hit
+					break;
+		}
+
+		if (ri == null) {
+			// cache miss 
+			List<RenderInstruction> matches = mMatchingList;
+			matches.clear();
 			for (int i = 0, n = mRulesList.size(); i < n; ++i)
-				mRulesList.get(i)
-						.matchNode(renderCallback, tags, zoomLevel, matchingList);
+				mRulesList.get(i).matchNode(renderCallback, tags, zoomLevel, matches);
 
-			int size = matchingList.size();
-			if (size > 0) {
-				renderInstructions = new RenderInstruction[size];
-				matchingList.toArray(renderInstructions);
+			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)
+					ris.next = ri;
+				else
+					mMatchingCacheNodes.put(matchingCacheKey, ri);
 			}
-			mMatchingCacheNodes.put(matchingCacheKey, renderInstructions);
 		}
 
-		if (renderInstructions != null) {
-			for (int i = 0, n = renderInstructions.length; i < n; i++)
-				renderInstructions[i].renderNode(renderCallback, tags);
-		}
+		if (ri.list != null)
+			render(renderCallback, ri.list, tags);
 
-		return renderInstructions;
+		return ri.list;
 
 	}
 
-	// private int missCnt = 0;
-	// private int hitCnt = 0;
+	private int missCnt = 0;
+	private int hitCnt = 0;
 	private MatchingCacheKey mCacheKey = new MatchingCacheKey();
+	private ArrayList<RenderInstruction> mMatchingList = new ArrayList<RenderInstruction>(4);
 
 	/**
 	 * Matches a way with the given parameters against this RenderTheme.
@@ -196,9 +261,13 @@ public class RenderTheme {
 	public synchronized RenderInstruction[] matchWay(IRenderCallback renderCallback,
 			Tag[] tags, byte zoomLevel, boolean closed, boolean render) {
 
-		RenderInstruction[] renderInstructions = null;
+		// list of renderinsctruction items in cache
+		RenderInstructionItem ris = null;
 
-		LRUCache<MatchingCacheKey, RenderInstruction[]> matchingCache;
+		// the item matching tags and zoomlevel
+		RenderInstructionItem ri = null;
+
+		LRUCache<MatchingCacheKey, RenderInstructionItem> matchingCache;
 		MatchingCacheKey matchingCacheKey;
 
 		if (closed) {
@@ -206,38 +275,87 @@ public class RenderTheme {
 		} else {
 			matchingCache = mMatchingCacheWay;
 		}
+		int zoomMask = 1 << zoomLevel;
 
-		mCacheKey.set(tags, zoomLevel);
-		renderInstructions = matchingCache.get(mCacheKey);
+		mCacheKey.set(tags);
+		ris = matchingCache.get(mCacheKey);
 
-		if (renderInstructions != null) {
-			// Log.d("RenderTheme", hitCnt++ + "Cache Hit");
-		} else if (!matchingCache.containsKey(mCacheKey)) {
-			matchingCacheKey = new MatchingCacheKey(mCacheKey);
+		for (ri = ris; ri != null; ri = ri.next)
+			if ((ri.zoom & zoomMask) != 0)
+				// cache hit
+				break;
 
+		if (ri == null) {
 			// cache miss
-			// Log.d("RenderTheme", missCnt++ + " Cache Miss");
+			//Log.d(TAG, missCnt++ + " / " + hitCnt + " Cache Miss");
 
 			int c = (closed ? Closed.YES : Closed.NO);
-			List<RenderInstruction> matchingList = new ArrayList<RenderInstruction>(4);
-			for (int i = 0, n = mRulesList.size(); i < n; ++i) {
-				mRulesList.get(i).matchWay(renderCallback, tags, zoomLevel, c,
-						matchingList);
-			}
-			int size = matchingList.size();
-			if (size > 0) {
-				renderInstructions = new RenderInstruction[size];
-				matchingList.toArray(renderInstructions);
-			}
-			matchingCache.put(matchingCacheKey, renderInstructions);
-		}
+			List<RenderInstruction> matches = mMatchingList;
+			matches.clear();
 
-		if (render && renderInstructions != null) {
-			for (int i = 0, n = renderInstructions.length; i < n; i++)
-				renderInstructions[i].renderWay(renderCallback, tags);
-		}
+			for (int i = 0, n = mRulesList.size(); i < n; ++i)
+				mRulesList.get(i).matchWay(renderCallback, tags, zoomLevel, c, matches);
 
-		return renderInstructions;
+			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, " same instructions " + size + " " + Arrays.deepToString(tags));
+
+			} else {
+				//Log.d(TAG, " 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) {
+					ris.next = ri;
+				}
+				else {
+					matchingCacheKey = new MatchingCacheKey(mCacheKey);
+					matchingCache.put(matchingCacheKey, 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) {
diff --git a/src/org/oscim/theme/Rule.java b/src/org/oscim/theme/Rule.java
index 5fd9c32b..aa73e31b 100644
--- a/src/org/oscim/theme/Rule.java
+++ b/src/org/oscim/theme/Rule.java
@@ -173,8 +173,7 @@ abstract class Rule {
 	final int mElement;
 	final int mClosed;
 
-	Rule(int element, int closed, byte zoomMin,
-			byte zoomMax) {
+	Rule(int element, int closed, byte zoomMin, byte zoomMax) {
 
 		mClosed = closed;
 		mElement = element;
@@ -193,13 +192,17 @@ abstract class Rule {
 		mSubRules.add(rule);
 	}
 
-	abstract boolean matchesNode(Tag[] tags, byte zoomLevel);
+	abstract boolean matchesNode(Tag[] tags);
 
-	abstract boolean matchesWay(Tag[] tags, byte zoomLevel, int closed);
+	abstract boolean matchesWay(Tag[] tags);
 
 	void matchNode(IRenderCallback renderCallback, Tag[] tags, byte zoomLevel,
 			List<RenderInstruction> matchingList) {
-		if (matchesNode(tags, zoomLevel)) {
+		if ((mElement != Element.WAY)
+				&& mZoomMin <= zoomLevel
+				&& mZoomMax >= zoomLevel
+				&& matchesNode(tags)) {
+
 			for (int i = 0, n = mRenderInstructionArray.length; i < n; i++)
 				matchingList.add(mRenderInstructionArray[i]);
 
@@ -210,13 +213,19 @@ abstract class Rule {
 	}
 
 	void matchWay(IRenderCallback renderCallback, Tag[] tags, byte zoomLevel,
-			int closed,
-			List<RenderInstruction> matchingList) {
+			int closed, List<RenderInstruction> matchingList) {
 
-		if (matchesWay(tags, zoomLevel, closed)) {
+		if ((mElement != Element.NODE)
+				&& mZoomMin <= zoomLevel
+				&& mZoomMax >= zoomLevel
+				&& (mClosed == closed || mClosed == Closed.ANY)
+				&& (matchesWay(tags))) {
+
+			// add instructions for this rule
 			for (int i = 0, n = mRenderInstructionArray.length; i < n; i++)
 				matchingList.add(mRenderInstructionArray[i]);
 
+			// check subrules
 			for (int i = 0, n = mSubRuleArray.length; i < n; i++)
 				mSubRuleArray[i].matchWay(renderCallback, tags, zoomLevel, closed,
 						matchingList);
@@ -230,15 +239,10 @@ abstract class Rule {
 
 		mRenderInstructionArray = new RenderInstruction[mRenderInstructions.size()];
 		mRenderInstructions.toArray(mRenderInstructionArray);
-		// for (int i = 0, n = mRenderInstructions.size(); i < n; i++)
-		// mRenderInstructionArray[i] = mRenderInstructions.get(i);
 
 		mSubRuleArray = new Rule[mSubRules.size()];
 		mSubRules.toArray(mSubRuleArray);
 
-		// for (int i = 0, n = mSubRules.size(); i < n; i++)
-		// mSubRuleArray[i] = mSubRules.get(i);
-
 		mRenderInstructions.clear();
 		mRenderInstructions = null;
 		mSubRules.clear();
diff --git a/src/org/oscim/theme/RuleOptimizer.java b/src/org/oscim/theme/RuleOptimizer.java
index 1cc8b3d6..4b1bbde7 100644
--- a/src/org/oscim/theme/RuleOptimizer.java
+++ b/src/org/oscim/theme/RuleOptimizer.java
@@ -17,7 +17,6 @@ package org.oscim.theme;
 import java.util.Stack;
 
 final class RuleOptimizer {
-	// private static final Logger LOG = Logger.getLogger(RuleOptimizer.class.getName());
 
 	private static AttributeMatcher optimizeKeyMatcher(AttributeMatcher attributeMatcher,
 			Stack<Rule> ruleStack) {
@@ -26,7 +25,7 @@ final class RuleOptimizer {
 				PositiveRule positiveRule = (PositiveRule) ruleStack.get(i);
 				if (positiveRule.mKeyMatcher != null
 						&& positiveRule.mKeyMatcher.isCoveredBy(attributeMatcher)) {
-					return null; // AnyMatcher.getInstance();
+					return null;
 				}
 			}
 		}
@@ -42,7 +41,7 @@ final class RuleOptimizer {
 
 				if (positiveRule.mValueMatcher != null
 						&& positiveRule.mValueMatcher.isCoveredBy(attributeMatcher)) {
-					return null; // AnyMatcher.getInstance();
+					return null;
 				}
 			}
 		}