/* * 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.Collections; 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.JobTheme; import org.mapsforge.android.maps.mapgenerator.MapGenerator; import org.mapsforge.android.maps.mapgenerator.MapGeneratorJob; import org.mapsforge.android.maps.rendertheme.RenderCallback; import org.mapsforge.android.maps.rendertheme.RenderTheme; import org.mapsforge.android.maps.rendertheme.RenderThemeHandler; import org.mapsforge.core.model.GeoPoint; 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.xml.sax.SAXException; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Paint; /** * A DatabaseRenderer renders map tiles by reading from a {@link MapDatabase}. */ public class DatabaseRenderer implements MapGenerator, 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 Paint PAINT_WATER_TILE_HIGHTLIGHT = new Paint(Paint.ANTI_ALIAS_FLAG); 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 float[][] WATER_TILE_COORDINATES = new float[][] { { 0, 0, Tile.TILE_SIZE, 0, Tile.TILE_SIZE, Tile.TILE_SIZE, 0, Tile.TILE_SIZE, 0, 0 } }; private static final byte ZOOM_MAX = 22; private static RenderTheme getRenderTheme(JobTheme jobTheme) { try { return RenderThemeHandler.getRenderTheme(jobTheme); } catch (ParserConfigurationException | IOException | SAXException e) { LOGGER.log(Level.SEVERE, null, e); } return null; } 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 float[][] coordinates; private Tile currentTile; private List<List<ShapePaintContainer>> drawingLayers; private final LabelPlacement labelPlacement; private MapDatabase mapDatabase; private List<PointTextContainer> nodes; private final List<SymbolContainer> pointSymbols; private float poiX; private float poiY; private JobTheme 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. */ public DatabaseRenderer() { this.canvasRasterer = new CanvasRasterer(); this.labelPlacement = new LabelPlacement(); this.ways = new ArrayList<>(LAYERS); this.wayNames = new ArrayList<>(64); this.nodes = new ArrayList<>(64); this.areaLabels = new ArrayList<>(64); this.waySymbols = new ArrayList<>(64); this.pointSymbols = new ArrayList<>(64); PAINT_WATER_TILE_HIGHTLIGHT.setStyle(Paint.Style.FILL); PAINT_WATER_TILE_HIGHTLIGHT.setColor(Color.CYAN); } @Override public void cleanup() { if (this.renderTheme != null) { this.renderTheme.destroy(); } } @Override public boolean executeJob(MapGeneratorJob mapGeneratorJob, Bitmap bitmap) { this.currentTile = mapGeneratorJob.tile; JobTheme jobTheme = mapGeneratorJob.jobParameters.jobTheme; if (!jobTheme.equals(this.previousJobTheme)) { 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; } @Override public GeoPoint getStartPoint() { if (this.mapDatabase != null && this.mapDatabase.hasOpenFile()) { MapFileInfo mapFileInfo = this.mapDatabase.getMapFileInfo(); if (mapFileInfo.startPosition != null) { return mapFileInfo.startPosition; } else if (mapFileInfo.mapCenter != null) { return mapFileInfo.mapCenter; } } return 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; } @Override public byte getZoomLevelMax() { return ZOOM_MAX; } @Override public void renderArea(Paint paint, int level) { this.drawingLayers.get(level).add(new ShapePaintContainer(this.shapeContainer, paint)); } @Override public void renderAreaCaption(String caption, float verticalOffset, Paint paint, Paint stroke) { float[] centerPosition = GeometryUtils.calculateCenterOfBoundingBox(this.coordinates[0]); this.areaLabels.add(new PointTextContainer(caption, centerPosition[0], centerPosition[1], paint, stroke)); } @Override public void renderAreaSymbol(Bitmap symbol) { float[] centerPosition = GeometryUtils.calculateCenterOfBoundingBox(this.coordinates[0]); this.pointSymbols.add(new SymbolContainer(symbol, centerPosition[0] - (symbol.getWidth() >> 1), centerPosition[1] - (symbol.getHeight() >> 1))); } @Override public void renderPointOfInterestCaption(String caption, float verticalOffset, Paint paint, Paint stroke) { this.nodes.add(new PointTextContainer(caption, this.poiX, this.poiY + verticalOffset, paint, stroke)); } @Override public void renderPointOfInterestCircle(float radius, Paint outline, int level) { this.drawingLayers.get(level).add( new ShapePaintContainer(new CircleContainer(this.poiX, this.poiY, radius), outline)); } @Override public void renderPointOfInterestSymbol(Bitmap symbol) { this.pointSymbols.add(new SymbolContainer(symbol, this.poiX - (symbol.getWidth() >> 1), this.poiY - (symbol.getHeight() >> 1))); } @Override public void renderWay(Paint paint, int level) { this.drawingLayers.get(level).add(new ShapePaintContainer(this.shapeContainer, paint)); } @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 paint, Paint outline) { WayDecorator.renderText(textKey, paint, outline, this.coordinates, this.wayNames); } @Override public boolean requiresInternetConnection() { return false; } /** * @param mapDatabase * the MapDatabase from which the map data will be read. */ public void setMapDatabase(MapDatabase mapDatabase) { this.mapDatabase = mapDatabase; } 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<>(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.poiX = scaleLongitude(pointOfInterest.position.longitudeE6); this.poiY = scaleLatitude(pointOfInterest.position.latitudeE6); 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, Collections.singletonList(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? this.coordinates = way.wayNodes; for (int i = 0; i < this.coordinates.length; ++i) { for (int j = 0; j < this.coordinates[i].length; j += 2) { this.coordinates[i][j] = scaleLongitude(this.coordinates[i][j]); this.coordinates[i][j + 1] = scaleLatitude(this.coordinates[i][j + 1]); } } 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 a latitude value into an Y coordinate on the current tile. * * @param latitude * the latitude value. * @return the Y coordinate on the current tile. */ private float scaleLatitude(float latitude) { return (float) (MercatorProjection.latitudeToPixelY(latitude / (double) 1000000, this.currentTile.zoomLevel) - this.currentTile .getPixelY()); } /** * Converts a longitude value into an X coordinate on the current tile. * * @param longitude * the longitude value. * @return the X coordinate on the current tile. */ private float scaleLongitude(float longitude) { return (float) (MercatorProjection.longitudeToPixelX(longitude / (double) 1000000, this.currentTile.zoomLevel) - this.currentTile .getPixelX()); } /** * 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)); } }