/* * 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.mapgenerator.databaserenderer; 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.android.maps.mapgenerator.MapGeneratorJob; import org.mapsforge.android.maps.mapgenerator.MapRenderer; import org.mapsforge.core.model.GeoPoint; 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.graphics.android.AndroidGraphics; import org.mapsforge.map.graphics.Bitmap; import org.mapsforge.map.graphics.Paint; import org.mapsforge.map.graphics.Style; 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.GraphicAdapter.Color; 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; /** * A DatabaseRenderer renders map tiles by reading from a {@link MapDatabase}. */ public class DatabaseRenderer implements RenderCallback, MapRenderer { 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 Paint PAINT_WATER_TILE_HIGHTLIGHT = AndroidGraphics.INSTANCE.getPaint(); 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 = 30; private static RenderTheme getRenderTheme(XmlRenderTheme jobTheme) { try { return RenderThemeHandler.getRenderTheme(AndroidGraphics.INSTANCE, 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 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 Tile currentTile; 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); PAINT_WATER_TILE_HIGHTLIGHT.setStyle(Style.FILL); PAINT_WATER_TILE_HIGHTLIGHT.setColor(AndroidGraphics.INSTANCE.getColor(Color.CYAN)); } /** * Called when a job needs to be executed. * * @param mapGeneratorJob * the job that should be executed. * @param bitmap * the bitmap for the generated map tile. * @return true if the job was executed successfully, false otherwise. */ @Override public boolean executeJob(MapGeneratorJob mapGeneratorJob, android.graphics.Bitmap bitmap) { this.currentTile = mapGeneratorJob.tile; XmlRenderTheme jobTheme = mapGeneratorJob.jobParameters.jobTheme; if (!jobTheme.equals(this.previousJobTheme)) { if (this.renderTheme != null) { this.renderTheme.destroy(); } this.renderTheme = getRenderTheme(jobTheme); if (this.renderTheme == null) { this.previousJobTheme = null; return false; } createWayLists(); this.previousJobTheme = jobTheme; this.previousZoomLevel = Byte.MIN_VALUE; // invalidate the previousTextScale so that textScale from jobParameters will // be applied next time this.previousTextScale = -1; } byte zoomLevel = this.currentTile.zoomLevel; if (zoomLevel != this.previousZoomLevel) { setScaleStrokeWidth(zoomLevel); this.previousZoomLevel = zoomLevel; } float textScale = mapGeneratorJob.jobParameters.textScale; if (Float.compare(textScale, this.previousTextScale) != 0) { this.renderTheme.scaleTextSize(textScale); this.previousTextScale = textScale; } if (this.mapDatabase != null) { MapReadResult mapReadResult = this.mapDatabase.readMapData(this.currentTile); processReadMapData(mapReadResult); } this.nodes = this.labelPlacement.placeLabels(this.nodes, this.pointSymbols, this.areaLabels, this.currentTile); 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); if (mapGeneratorJob.debugSettings.drawTileFrames) { this.canvasRasterer.drawTileFrame(); } if (mapGeneratorJob.debugSettings.drawTileCoordinates) { this.canvasRasterer.drawTileCoordinates(this.currentTile); } clearLists(); return true; } /** * @return the start point (may be null). */ @Override public GeoPoint 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). */ @Override 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. */ @Override 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 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 = scaleGeoPoint(pointOfInterest.position); this.renderTheme.matchNode(this, pointOfInterest.tags, this.currentTile.zoomLevel); } private void renderWaterBackground() { this.drawingLayers = this.ways.get(0); this.coordinates = WATER_TILE_COORDINATES; this.shapeContainer = new WayContainer(this.coordinates); this.renderTheme.matchClosedWay(this, Arrays.asList(TAG_NATURAL_WATER), this.currentTile.zoomLevel); } private void renderWay(Way way) { this.drawingLayers = this.ways.get(getValidLayer(way.layer)); // TODO what about the label position? GeoPoint[][] geoPoints = way.geoPoints; this.coordinates = new Point[geoPoints.length][]; for (int i = 0; i < this.coordinates.length; ++i) { this.coordinates[i] = new Point[geoPoints[i].length]; for (int j = 0; j < this.coordinates[i].length; ++j) { this.coordinates[i][j] = scaleGeoPoint(geoPoints[i][j]); } } this.shapeContainer = new WayContainer(this.coordinates); if (GeometryUtils.isClosedWay(this.coordinates[0])) { this.renderTheme.matchClosedWay(this, way.tags, this.currentTile.zoomLevel); } else { this.renderTheme.matchLinearWay(this, way.tags, this.currentTile.zoomLevel); } } /** * Converts the given GeoPoint into XY coordinates on the current tile. * * @param geoPoint * the GeoPoint to convert. * @return the XY coordinates on the current tile. */ private Point scaleGeoPoint(GeoPoint geoPoint) { double pixelX = MercatorProjection.longitudeToPixelX(geoPoint.longitude, this.currentTile.zoomLevel) - this.currentTile.getPixelX(); double pixelY = MercatorProjection.latitudeToPixelY(geoPoint.latitude, this.currentTile.zoomLevel) - this.currentTile.getPixelY(); 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)); } @Override public void destroy() { if (this.renderTheme != null) { this.renderTheme.destroy(); this.renderTheme = null; } } @Override public String getFileName() { return "Mapsforge"; } @Override public void start() { // nothing } @Override public void stop() { // nothing } @Override public boolean isWorking() { return true; } }