/*
* Copyright 2010, 2011, 2012, 2013 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.map.layer.renderer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.ParserConfigurationException;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.core.model.Point;
import org.mapsforge.core.model.Tag;
import org.mapsforge.core.model.Tile;
import org.mapsforge.core.util.MercatorProjection;
import org.mapsforge.map.reader.MapDatabase;
import org.mapsforge.map.reader.MapReadResult;
import org.mapsforge.map.reader.PointOfInterest;
import org.mapsforge.map.reader.Way;
import org.mapsforge.map.reader.header.MapFileInfo;
import org.mapsforge.map.rendertheme.RenderCallback;
import org.mapsforge.map.rendertheme.XmlRenderTheme;
import org.mapsforge.map.rendertheme.rule.RenderTheme;
import org.mapsforge.map.rendertheme.rule.RenderThemeHandler;
import org.xml.sax.SAXException;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Paint;
/**
* A DatabaseRenderer renders map tiles by reading from a {@link MapDatabase}.
*/
public class DatabaseRenderer implements RenderCallback {
private static final Byte DEFAULT_START_ZOOM_LEVEL = Byte.valueOf((byte) 12);
private static final byte LAYERS = 11;
private static final Logger LOGGER = Logger.getLogger(DatabaseRenderer.class.getName());
private static final double STROKE_INCREASE = 1.5;
private static final byte STROKE_MIN_ZOOM_LEVEL = 12;
private static final Tag TAG_NATURAL_WATER = new Tag("natural", "water");
private static final Point[][] WATER_TILE_COORDINATES = getTilePixelCoordinates();
private static final byte ZOOM_MAX = 22;
private static Point[][] getTilePixelCoordinates() {
Point point1 = new Point(0, 0);
Point point2 = new Point(Tile.TILE_SIZE, 0);
Point point3 = new Point(Tile.TILE_SIZE, Tile.TILE_SIZE);
Point point4 = new Point(0, Tile.TILE_SIZE);
return new Point[][] { { point1, point2, point3, point4, point1 } };
}
private static byte getValidLayer(byte layer) {
if (layer < 0) {
return 0;
} else if (layer >= LAYERS) {
return LAYERS - 1;
} else {
return layer;
}
}
private final List<PointTextContainer> areaLabels;
private final CanvasRasterer canvasRasterer;
private Point[][] coordinates;
private RendererJob currentRendererJob;
private List<List<ShapePaintContainer>> drawingLayers;
private final LabelPlacement labelPlacement;
private final MapDatabase mapDatabase;
private List<PointTextContainer> nodes;
private final List<SymbolContainer> pointSymbols;
private Point poiPosition;
private XmlRenderTheme previousJobTheme;
private float previousTextScale;
private byte previousZoomLevel;
private RenderTheme renderTheme;
private ShapeContainer shapeContainer;
private final List<WayTextContainer> wayNames;
private final List<List<List<ShapePaintContainer>>> ways;
private final List<SymbolContainer> waySymbols;
/**
* Constructs a new DatabaseRenderer.
*
* @param mapDatabase
* the MapDatabase from which the map data will be read.
*/
public DatabaseRenderer(MapDatabase mapDatabase) {
this.mapDatabase = mapDatabase;
this.canvasRasterer = new CanvasRasterer();
this.labelPlacement = new LabelPlacement();
this.ways = new ArrayList<List<List<ShapePaintContainer>>>(LAYERS);
this.wayNames = new ArrayList<WayTextContainer>(64);
this.nodes = new ArrayList<PointTextContainer>(64);
this.areaLabels = new ArrayList<PointTextContainer>(64);
this.waySymbols = new ArrayList<SymbolContainer>(64);
this.pointSymbols = new ArrayList<SymbolContainer>(64);
}
/**
* Called when a job needs to be executed.
*
* @param rendererJob
* the job that should be executed.
*/
public Bitmap executeJob(RendererJob rendererJob) {
this.currentRendererJob = rendererJob;
XmlRenderTheme jobTheme = rendererJob.xmlRenderTheme;
if (!jobTheme.equals(this.previousJobTheme)) {
this.renderTheme = getRenderTheme(jobTheme);
if (this.renderTheme == null) {
this.previousJobTheme = null;
return null;
}
createWayLists();
this.previousJobTheme = jobTheme;
this.previousZoomLevel = Byte.MIN_VALUE;
}
byte zoomLevel = rendererJob.tile.zoomLevel;
if (zoomLevel != this.previousZoomLevel) {
setScaleStrokeWidth(zoomLevel);
this.previousZoomLevel = zoomLevel;
}
float textScale = rendererJob.textScale;
if (Float.compare(textScale, this.previousTextScale) != 0) {
this.renderTheme.scaleTextSize(textScale);
this.previousTextScale = textScale;
}
if (this.mapDatabase != null) {
MapReadResult mapReadResult = this.mapDatabase.readMapData(rendererJob.tile);
processReadMapData(mapReadResult);
}
this.nodes = this.labelPlacement.placeLabels(this.nodes, this.pointSymbols, this.areaLabels, rendererJob.tile);
Bitmap bitmap = Bitmap.createBitmap(Tile.TILE_SIZE, Tile.TILE_SIZE,Config.ARGB_8888);
this.canvasRasterer.setCanvasBitmap(bitmap);
this.canvasRasterer.fill(this.renderTheme.getMapBackground());
this.canvasRasterer.drawWays(this.ways);
this.canvasRasterer.drawSymbols(this.waySymbols);
this.canvasRasterer.drawSymbols(this.pointSymbols);
this.canvasRasterer.drawWayNames(this.wayNames);
this.canvasRasterer.drawNodes(this.nodes);
this.canvasRasterer.drawNodes(this.areaLabels);
clearLists();
return bitmap;
}
/**
* @return the start point (may be null).
*/
public LatLong getStartPoint() {
if (this.mapDatabase != null && this.mapDatabase.hasOpenFile()) {
MapFileInfo mapFileInfo = this.mapDatabase.getMapFileInfo();
if (mapFileInfo.startPosition != null) {
return mapFileInfo.startPosition;
}
return mapFileInfo.boundingBox.getCenterPoint();
}
return null;
}
/**
* @return the start zoom level (may be null).
*/
public Byte getStartZoomLevel() {
if (this.mapDatabase != null && this.mapDatabase.hasOpenFile()) {
MapFileInfo mapFileInfo = this.mapDatabase.getMapFileInfo();
if (mapFileInfo.startZoomLevel != null) {
return mapFileInfo.startZoomLevel;
}
}
return DEFAULT_START_ZOOM_LEVEL;
}
/**
* @return the maximum zoom level.
*/
public byte getZoomLevelMax() {
return ZOOM_MAX;
}
@Override
public void renderArea(Paint fill, Paint stroke, int level) {
List<ShapePaintContainer> list = this.drawingLayers.get(level);
list.add(new ShapePaintContainer(this.shapeContainer, fill));
list.add(new ShapePaintContainer(this.shapeContainer, stroke));
}
@Override
public void renderAreaCaption(String caption, float verticalOffset, Paint fill, Paint stroke) {
Point centerPosition = GeometryUtils.calculateCenterOfBoundingBox(this.coordinates[0]);
this.areaLabels.add(new PointTextContainer(caption, centerPosition.x, centerPosition.y, fill, stroke));
}
@Override
public void renderAreaSymbol(Bitmap symbol) {
Point centerPosition = GeometryUtils.calculateCenterOfBoundingBox(this.coordinates[0]);
int halfSymbolWidth = symbol.getWidth() / 2;
int halfSymbolHeight = symbol.getHeight() / 2;
double pointX = centerPosition.x - halfSymbolWidth;
double pointY = centerPosition.y - halfSymbolHeight;
Point shiftedCenterPosition = new Point(pointX, pointY);
this.pointSymbols.add(new SymbolContainer(symbol, shiftedCenterPosition));
}
@Override
public void renderPointOfInterestCaption(String caption, float verticalOffset, Paint fill, Paint stroke) {
this.nodes.add(new PointTextContainer(caption, this.poiPosition.x, this.poiPosition.y + verticalOffset, fill,
stroke));
}
@Override
public void renderPointOfInterestCircle(float radius, Paint fill, Paint stroke, int level) {
List<ShapePaintContainer> list = this.drawingLayers.get(level);
list.add(new ShapePaintContainer(new CircleContainer(this.poiPosition, radius), fill));
list.add(new ShapePaintContainer(new CircleContainer(this.poiPosition, radius), stroke));
}
@Override
public void renderPointOfInterestSymbol(Bitmap symbol) {
int halfSymbolWidth = symbol.getWidth() / 2;
int halfSymbolHeight = symbol.getHeight() / 2;
double pointX = this.poiPosition.x - halfSymbolWidth;
double pointY = this.poiPosition.y - halfSymbolHeight;
Point shiftedCenterPosition = new Point(pointX, pointY);
this.pointSymbols.add(new SymbolContainer(symbol, shiftedCenterPosition));
}
@Override
public void renderWay(Paint stroke, int level) {
this.drawingLayers.get(level).add(new ShapePaintContainer(this.shapeContainer, stroke));
}
@Override
public void renderWaySymbol(Bitmap symbolBitmap, boolean alignCenter, boolean repeatSymbol) {
WayDecorator.renderSymbol(symbolBitmap, alignCenter, repeatSymbol, this.coordinates, this.waySymbols);
}
@Override
public void renderWayText(String textKey, Paint fill, Paint stroke) {
WayDecorator.renderText(textKey, fill, stroke, this.coordinates, this.wayNames);
}
private void clearLists() {
for (int i = this.ways.size() - 1; i >= 0; --i) {
List<List<ShapePaintContainer>> innerWayList = this.ways.get(i);
for (int j = innerWayList.size() - 1; j >= 0; --j) {
innerWayList.get(j).clear();
}
}
this.areaLabels.clear();
this.nodes.clear();
this.pointSymbols.clear();
this.wayNames.clear();
this.waySymbols.clear();
}
private void createWayLists() {
int levels = this.renderTheme.getLevels();
this.ways.clear();
for (byte i = LAYERS - 1; i >= 0; --i) {
List<List<ShapePaintContainer>> innerWayList = new ArrayList<List<ShapePaintContainer>>(levels);
for (int j = levels - 1; j >= 0; --j) {
innerWayList.add(new ArrayList<ShapePaintContainer>(0));
}
this.ways.add(innerWayList);
}
}
private RenderTheme getRenderTheme(XmlRenderTheme jobTheme) {
try {
return RenderThemeHandler.getRenderTheme(jobTheme);
} catch (ParserConfigurationException e) {
LOGGER.log(Level.SEVERE, null, e);
} catch (SAXException e) {
LOGGER.log(Level.SEVERE, null, e);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, null, e);
}
return null;
}
private void processReadMapData(MapReadResult mapReadResult) {
if (mapReadResult == null) {
return;
}
for (PointOfInterest pointOfInterest : mapReadResult.pointOfInterests) {
renderPointOfInterest(pointOfInterest);
}
for (Way way : mapReadResult.ways) {
renderWay(way);
}
if (mapReadResult.isWater) {
renderWaterBackground();
}
}
private void renderPointOfInterest(PointOfInterest pointOfInterest) {
this.drawingLayers = this.ways.get(getValidLayer(pointOfInterest.layer));
this.poiPosition = scaleLatLong(pointOfInterest.position);
this.renderTheme.matchNode(this, pointOfInterest.tags, this.currentRendererJob.tile.zoomLevel);
}
private void renderWaterBackground() {
this.drawingLayers = this.ways.get(0);
this.coordinates = WATER_TILE_COORDINATES;
this.shapeContainer = new PolylineContainer(this.coordinates);
this.renderTheme.matchClosedWay(this, Arrays.asList(TAG_NATURAL_WATER), this.currentRendererJob.tile.zoomLevel);
}
private void renderWay(Way way) {
this.drawingLayers = this.ways.get(getValidLayer(way.layer));
// TODO what about the label position?
LatLong[][] latLongs = way.latLongs;
this.coordinates = new Point[latLongs.length][];
for (int i = 0; i < this.coordinates.length; ++i) {
this.coordinates[i] = new Point[latLongs[i].length];
for (int j = 0; j < this.coordinates[i].length; ++j) {
this.coordinates[i][j] = scaleLatLong(latLongs[i][j]);
}
}
this.shapeContainer = new PolylineContainer(this.coordinates);
if (GeometryUtils.isClosedWay(this.coordinates[0])) {
this.renderTheme.matchClosedWay(this, way.tags, this.currentRendererJob.tile.zoomLevel);
} else {
this.renderTheme.matchLinearWay(this, way.tags, this.currentRendererJob.tile.zoomLevel);
}
}
/**
* Converts the given LatLong into XY coordinates on the current object.
*
* @param latLong
* the LatLong to convert.
* @return the XY coordinates on the current object.
*/
private Point scaleLatLong(LatLong latLong) {
double pixelX = MercatorProjection.longitudeToPixelX(latLong.longitude, this.currentRendererJob.tile.zoomLevel)
- MercatorProjection.tileToPixel(this.currentRendererJob.tile.tileX);
double pixelY = MercatorProjection.latitudeToPixelY(latLong.latitude, this.currentRendererJob.tile.zoomLevel)
- MercatorProjection.tileToPixel(this.currentRendererJob.tile.tileY);
return new Point((float) pixelX, (float) pixelY);
}
/**
* Sets the scale stroke factor for the given zoom level.
*
* @param zoomLevel
* the zoom level for which the scale stroke factor should be set.
*/
private void setScaleStrokeWidth(byte zoomLevel) {
int zoomLevelDiff = Math.max(zoomLevel - STROKE_MIN_ZOOM_LEVEL, 0);
this.renderTheme.scaleStrokeWidth((float) Math.pow(STROKE_INCREASE, zoomLevelDiff));
}
}