/* * Copyright 2010, 2011, 2012 mapsforge.org * * 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.mapsforge.android.maps.rendertheme; import java.util.ArrayList; import java.util.List; import org.mapsforge.android.maps.rendertheme.renderinstruction.RenderInstruction; import org.mapsforge.core.model.Tag; import org.mapsforge.core.util.LRUCache; import org.xml.sax.Attributes; import android.graphics.Color; /** * A RenderTheme defines how ways and nodes are drawn. */ public class RenderTheme { 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 baseStrokeWidth; private final float baseTextSize; private int levels; private final int mapBackground; private final LRUCache<MatchingCacheKey, List<RenderInstruction>> matchingCache; private final ArrayList<Rule> rulesList; RenderTheme(int mapBackground, float baseStrokeWidth, float baseTextSize) { this.mapBackground = mapBackground; this.baseStrokeWidth = baseStrokeWidth; this.baseTextSize = baseTextSize; this.rulesList = new ArrayList<>(); this.matchingCache = new LRUCache<>(MATCHING_CACHE_SIZE); } /** * Must be called when this RenderTheme gets destroyed to clean up and free resources. */ public void destroy() { this.matchingCache.clear(); for (int i = 0, n = this.rulesList.size(); i < n; ++i) { this.rulesList.get(i).onDestroy(); } } /** * @return the number of distinct drawing levels required by this RenderTheme. */ public int getLevels() { return this.levels; } /** * @return the map background color of this RenderTheme. * @see Color */ public int getMapBackground() { return this.mapBackground; } /** * Matches a closed 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. */ public void matchClosedWay(RenderCallback renderCallback, List<Tag> tags, byte zoomLevel) { matchWay(renderCallback, tags, zoomLevel, Closed.YES); } /** * Matches a linear 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. */ public void matchLinearWay(RenderCallback renderCallback, List<Tag> tags, byte zoomLevel) { matchWay(renderCallback, tags, zoomLevel, Closed.NO); } /** * Matches a node 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 node. * @param zoomLevel * the zoom level at which the node should be matched. */ public void matchNode(RenderCallback renderCallback, List<Tag> tags, byte zoomLevel) { for (int i = 0, n = this.rulesList.size(); i < n; ++i) { this.rulesList.get(i).matchNode(renderCallback, tags, zoomLevel); } } /** * 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 = this.rulesList.size(); i < n; ++i) { this.rulesList.get(i).scaleStrokeWidth(scaleFactor * this.baseStrokeWidth); } } /** * 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 = this.rulesList.size(); i < n; ++i) { this.rulesList.get(i).scaleTextSize(scaleFactor * this.baseTextSize); } } private void matchWay(RenderCallback renderCallback, List<Tag> tags, byte zoomLevel, Closed closed) { MatchingCacheKey matchingCacheKey = new MatchingCacheKey(tags, zoomLevel, closed); List<RenderInstruction> matchingList = this.matchingCache.get(matchingCacheKey); if (matchingList != null) { // cache hit for (int i = 0, n = matchingList.size(); i < n; ++i) { matchingList.get(i).renderWay(renderCallback, tags); } return; } // cache miss matchingList = new ArrayList<>(); for (int i = 0, n = this.rulesList.size(); i < n; ++i) { this.rulesList.get(i).matchWay(renderCallback, tags, zoomLevel, closed, matchingList); } this.matchingCache.put(matchingCacheKey, matchingList); } void addRule(Rule rule) { this.rulesList.add(rule); } void complete() { this.rulesList.trimToSize(); for (int i = 0, n = this.rulesList.size(); i < n; ++i) { this.rulesList.get(i).onComplete(); } } void setLevels(int levels) { this.levels = levels; } }