/* * 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.map.writer; import gnu.trove.list.array.TLongArrayList; import gnu.trove.map.hash.TLongObjectHashMap; import gnu.trove.map.hash.TShortIntHashMap; import gnu.trove.procedure.TObjectProcedure; import gnu.trove.set.TLongSet; import gnu.trove.set.hash.TLongHashSet; import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.mapsforge.core.model.BoundingBox; import org.mapsforge.core.model.CoordinatesUtil; import org.mapsforge.core.util.MercatorProjection; import org.mapsforge.map.writer.model.MapWriterConfiguration; import org.mapsforge.map.writer.model.NodeResolver; import org.mapsforge.map.writer.model.TDNode; import org.mapsforge.map.writer.model.TDRelation; import org.mapsforge.map.writer.model.TDWay; import org.mapsforge.map.writer.model.TileBasedDataProcessor; import org.mapsforge.map.writer.model.TileCoordinate; import org.mapsforge.map.writer.model.TileData; import org.mapsforge.map.writer.model.TileGridLayout; import org.mapsforge.map.writer.model.WayResolver; import org.mapsforge.map.writer.model.ZoomIntervalConfiguration; import org.mapsforge.map.writer.util.GeoUtils; import com.vividsolutions.jts.geom.TopologyException; abstract class BaseTileBasedDataProcessor implements TileBasedDataProcessor, NodeResolver, WayResolver { protected static final Logger LOGGER = Logger.getLogger(BaseTileBasedDataProcessor.class.getName()); protected final org.mapsforge.core.model.BoundingBox boundingbox; protected TileGridLayout[] tileGridLayouts; protected final ZoomIntervalConfiguration zoomIntervalConfiguration; protected final int bboxEnlargement; protected final String preferredLanguage; protected final boolean skipInvalidRelations; protected final TLongObjectHashMap<TLongArrayList> outerToInnerMapping; protected final TLongSet innerWaysWithoutAdditionalTags; protected final Map<TileCoordinate, TLongHashSet> tilesToCoastlines; // accounting protected float[] countWays; protected float[] countWayTileFactor; protected final TShortIntHashMap histogramPoiTags; protected final TShortIntHashMap histogramWayTags; protected long maxWayID = Long.MIN_VALUE; // public BaseTileBasedDataProcessor(double minLat, double maxLat, double minLon, double maxLon, // ZoomIntervalConfiguration zoomIntervalConfiguration, int bboxEnlargement, String preferredLanguage) { // this(new Rect(minLon, maxLon, minLat, maxLat), zoomIntervalConfiguration, bboxEnlargement, // preferredLanguage); // // } public BaseTileBasedDataProcessor(MapWriterConfiguration configuration) { super(); this.boundingbox = configuration.getBboxConfiguration(); this.zoomIntervalConfiguration = configuration.getZoomIntervalConfiguration(); this.tileGridLayouts = new TileGridLayout[this.zoomIntervalConfiguration.getNumberOfZoomIntervals()]; this.bboxEnlargement = configuration.getBboxEnlargement(); this.preferredLanguage = configuration.getPreferredLanguage(); this.skipInvalidRelations = configuration.isSkipInvalidRelations(); this.outerToInnerMapping = new TLongObjectHashMap<TLongArrayList>(); this.innerWaysWithoutAdditionalTags = new TLongHashSet(); this.tilesToCoastlines = new HashMap<TileCoordinate, TLongHashSet>(); this.countWays = new float[this.zoomIntervalConfiguration.getNumberOfZoomIntervals()]; this.countWayTileFactor = new float[this.zoomIntervalConfiguration.getNumberOfZoomIntervals()]; this.histogramPoiTags = new TShortIntHashMap(); this.histogramWayTags = new TShortIntHashMap(); // compute horizontal and vertical tile coordinate offsets for all // base zoom levels for (int i = 0; i < this.zoomIntervalConfiguration.getNumberOfZoomIntervals(); i++) { TileCoordinate upperLeft = new TileCoordinate((int) MercatorProjection.longitudeToTileX( this.boundingbox.minLongitude, this.zoomIntervalConfiguration.getBaseZoom(i)), (int) MercatorProjection.latitudeToTileY(this.boundingbox.maxLatitude, this.zoomIntervalConfiguration.getBaseZoom(i)), this.zoomIntervalConfiguration.getBaseZoom(i)); this.tileGridLayouts[i] = new TileGridLayout(upperLeft, computeNumberOfHorizontalTiles(i), computeNumberOfVerticalTiles(i)); } } @Override public BoundingBox getBoundingBox() { return this.boundingbox; } @Override public TileGridLayout getTileGridLayout(int zoomIntervalIndex) { return this.tileGridLayouts[zoomIntervalIndex]; } @Override public ZoomIntervalConfiguration getZoomIntervalConfiguration() { return this.zoomIntervalConfiguration; } @Override public long cumulatedNumberOfTiles() { long cumulated = 0; for (int i = 0; i < this.zoomIntervalConfiguration.getNumberOfZoomIntervals(); i++) { cumulated += this.tileGridLayouts[i].getAmountTilesHorizontal() * this.tileGridLayouts[i].getAmountTilesVertical(); } return cumulated; } protected void countPoiTags(TDNode poi) { if (poi == null || poi.getTags() == null) { return; } for (short tag : poi.getTags()) { this.histogramPoiTags.adjustOrPutValue(tag, 1, 1); } } protected void countWayTags(TDWay way) { if (way == null || way.getTags() == null) { return; } for (short tag : way.getTags()) { this.histogramWayTags.adjustOrPutValue(tag, 1, 1); } } protected void countWayTags(short[] tags) { if (tags == null) { return; } for (short tag : tags) { this.histogramWayTags.adjustOrPutValue(tag, 1, 1); } } protected void addPOI(TDNode poi) { if (!poi.isPOI()) { return; } byte minZoomLevel = poi.getZoomAppear(); for (int i = 0; i < this.zoomIntervalConfiguration.getNumberOfZoomIntervals(); i++) { // is POI seen in a zoom interval? if (minZoomLevel <= this.zoomIntervalConfiguration.getMaxZoom(i)) { long tileCoordinateX = MercatorProjection.longitudeToTileX( CoordinatesUtil.microdegreesToDegrees(poi.getLongitude()), this.zoomIntervalConfiguration.getBaseZoom(i)); long tileCoordinateY = MercatorProjection.latitudeToTileY( CoordinatesUtil.microdegreesToDegrees(poi.getLatitude()), this.zoomIntervalConfiguration.getBaseZoom(i)); TileData tileData = getTileImpl(i, (int) tileCoordinateX, (int) tileCoordinateY); if (tileData != null) { tileData.addPOI(poi); countPoiTags(poi); } } } } protected void addWayToTiles(TDWay way, int enlargement) { int bboxEnlargementLocal = enlargement; byte minZoomLevel = way.getMinimumZoomLevel(); for (int i = 0; i < this.zoomIntervalConfiguration.getNumberOfZoomIntervals(); i++) { // is way seen in a zoom interval? if (minZoomLevel <= this.zoomIntervalConfiguration.getMaxZoom(i)) { Set<TileCoordinate> matchedTiles = GeoUtils.mapWayToTiles(way, this.zoomIntervalConfiguration.getBaseZoom(i), bboxEnlargementLocal); boolean added = false; for (TileCoordinate matchedTile : matchedTiles) { TileData td = getTileImpl(i, matchedTile.getX(), matchedTile.getY()); if (td != null) { countWayTags(way); this.countWayTileFactor[i]++; added = true; td.addWay(way); } } if (added) { this.countWays[i]++; } } } } protected abstract TileData getTileImpl(int zoom, int tileX, int tileY); protected abstract void handleVirtualOuterWay(TDWay virtualWay); protected abstract void handleAdditionalRelationTags(TDWay virtualWay, TDRelation relation); protected abstract void handleVirtualInnerWay(TDWay virtualWay); private int computeNumberOfHorizontalTiles(int zoomIntervalIndex) { long tileCoordinateLeft = MercatorProjection.longitudeToTileX(this.boundingbox.minLongitude, this.zoomIntervalConfiguration.getBaseZoom(zoomIntervalIndex)); long tileCoordinateRight = MercatorProjection.longitudeToTileX(this.boundingbox.maxLongitude, this.zoomIntervalConfiguration.getBaseZoom(zoomIntervalIndex)); assert tileCoordinateLeft <= tileCoordinateRight; assert tileCoordinateLeft - tileCoordinateRight + 1 < Integer.MAX_VALUE; LOGGER.finer("basezoom: " + this.zoomIntervalConfiguration.getBaseZoom(zoomIntervalIndex) + "\t+n_horizontal: " + (tileCoordinateRight - tileCoordinateLeft + 1)); return (int) (tileCoordinateRight - tileCoordinateLeft + 1); } private int computeNumberOfVerticalTiles(int zoomIntervalIndex) { long tileCoordinateBottom = MercatorProjection.latitudeToTileY(this.boundingbox.minLatitude, this.zoomIntervalConfiguration.getBaseZoom(zoomIntervalIndex)); long tileCoordinateTop = MercatorProjection.latitudeToTileY(this.boundingbox.maxLatitude, this.zoomIntervalConfiguration.getBaseZoom(zoomIntervalIndex)); assert tileCoordinateBottom >= tileCoordinateTop; assert tileCoordinateBottom - tileCoordinateTop + 1 <= Integer.MAX_VALUE; LOGGER.finer("basezoom: " + this.zoomIntervalConfiguration.getBaseZoom(zoomIntervalIndex) + "\t+n_vertical: " + (tileCoordinateBottom - tileCoordinateTop + 1)); return (int) (tileCoordinateBottom - tileCoordinateTop + 1); } protected class RelationHandler implements TObjectProcedure<TDRelation> { private final WayPolygonizer polygonizer = new WayPolygonizer(); private List<Integer> inner; private List<Deque<TDWay>> extractedPolygons; private Map<Integer, List<Integer>> outerToInner; @Override public boolean execute(TDRelation relation) { if (relation == null) { return false; } this.extractedPolygons = null; this.outerToInner = null; TDWay[] members = relation.getMemberWays(); try { this.polygonizer.polygonizeAndRelate(members); } catch (TopologyException e) { LOGGER.log(Level.FINE, "cannot relate extracted polygons to each other for relation: " + relation.getId(), e); } // skip invalid relations if (!this.polygonizer.getDangling().isEmpty()) { if (BaseTileBasedDataProcessor.this.skipInvalidRelations) { LOGGER.fine("skipping relation that contains dangling ways which could not be merged to polygons: " + relation.getId()); return true; } LOGGER.fine("relation contains dangling ways which could not be merged to polygons: " + relation.getId()); } else if (!this.polygonizer.getIllegal().isEmpty()) { if (BaseTileBasedDataProcessor.this.skipInvalidRelations) { LOGGER.fine("skipping relation contains illegal closed ways with fewer than 4 nodes: " + relation.getId()); return true; } LOGGER.fine("relation contains illegal closed ways with fewer than 4 nodes: " + relation.getId()); } this.extractedPolygons = this.polygonizer.getPolygons(); this.outerToInner = this.polygonizer.getOuterToInner(); for (Entry<Integer, List<Integer>> entry : this.outerToInner.entrySet()) { Deque<TDWay> outerPolygon = this.extractedPolygons.get(entry.getKey().intValue()); this.inner = null; this.inner = entry.getValue(); byte shape = TDWay.SIMPLE_POLYGON; // does it contain inner ways? if (this.inner != null && !this.inner.isEmpty()) { shape = TDWay.MULTI_POLYGON; } TDWay outerWay = null; if (outerPolygon.size() > 1) { // we need to create a new way from a set of ways // collect the way nodes and use the tags of the relation // if one of the ways has its own tags, we should ignore them, // ways with relevant tags will be added separately later if (!relation.isRenderRelevant()) { LOGGER.fine("constructed outer polygon in relation has no known tags: " + relation.getId()); continue; } // merge way nodes from outer way segments List<TDNode> waynodeList = new ArrayList<TDNode>(); for (TDWay outerSegment : outerPolygon) { if (outerSegment.isReversedInRelation()) { for (int i = outerSegment.getWayNodes().length - 1; i >= 0; i--) { waynodeList.add(outerSegment.getWayNodes()[i]); } } else { for (TDNode tdNode : outerSegment.getWayNodes()) { waynodeList.add(tdNode); } } } TDNode[] waynodes = waynodeList.toArray(new TDNode[waynodeList.size()]); // create new virtual way which represents the outer way // use maxWayID counter to create unique id outerWay = new TDWay(++BaseTileBasedDataProcessor.this.maxWayID, relation.getLayer(), relation.getName(), relation.getHouseNumber(), relation.getRef(), relation.getTags(), shape, waynodes); // add the newly created way to matching tiles addWayToTiles(outerWay, BaseTileBasedDataProcessor.this.bboxEnlargement); handleVirtualOuterWay(outerWay); // adjust tag statistics, cannot be omitted!!! countWayTags(relation.getTags()); } // the outer way consists of only one segment else { outerWay = outerPolygon.getFirst(); // is it a polygon that we have seen already and which was // identified as a polgyon containing inner ways? if (BaseTileBasedDataProcessor.this.outerToInnerMapping.contains(outerWay.getId())) { shape = TDWay.MULTI_POLYGON; } outerWay.setShape(shape); // we merge the name, ref, tag information of the relation to the outer way // TODO is this true? // a relation that addresses an already closed way, is normally used to add // additional information to the way outerWay.mergeRelationInformation(relation); // only consider the way, if it has tags, otherwise the renderer cannot interpret // the way if (outerWay.isRenderRelevant()) { // handle relation tags handleAdditionalRelationTags(outerWay, relation); addWayToTiles(outerWay, BaseTileBasedDataProcessor.this.bboxEnlargement); countWayTags(outerWay.getTags()); } } // relate inner ways to outer way addInnerWays(outerWay); } return true; } private void addInnerWays(TDWay outer) { if (this.inner != null && !this.inner.isEmpty()) { TLongArrayList innerList = BaseTileBasedDataProcessor.this.outerToInnerMapping.get(outer.getId()); if (innerList == null) { innerList = new TLongArrayList(); BaseTileBasedDataProcessor.this.outerToInnerMapping.put(outer.getId(), innerList); } for (Integer innerIndex : this.inner) { Deque<TDWay> innerSegments = this.extractedPolygons.get(innerIndex.intValue()); TDWay innerWay = null; if (innerSegments.size() == 1) { innerWay = innerSegments.getFirst(); if (innerWay.hasTags() && outer.hasTags()) { short[] iTags = innerWay.getTags(); short[] oTags = outer.getTags(); int contained = 0; for (short iTagID : iTags) { for (short oTagID : oTags) { if (iTagID == oTagID) { contained++; } } } if (contained == iTags.length) { BaseTileBasedDataProcessor.this.innerWaysWithoutAdditionalTags.add(innerWay.getId()); } } } else { List<TDNode> waynodeList = new ArrayList<TDNode>(); for (TDWay innerSegment : innerSegments) { if (innerSegment.isReversedInRelation()) { for (int i = innerSegment.getWayNodes().length - 1; i >= 0; i--) { waynodeList.add(innerSegment.getWayNodes()[i]); } } else { for (TDNode tdNode : innerSegment.getWayNodes()) { waynodeList.add(tdNode); } } } TDNode[] waynodes = waynodeList.toArray(new TDNode[waynodeList.size()]); // TODO which layer? innerWay = new TDWay(++BaseTileBasedDataProcessor.this.maxWayID, (byte) 0, null, null, null, waynodes); handleVirtualInnerWay(innerWay); // does not need to be added to corresponding tiles // virtual inner ways do not have any tags, they are holes in the outer polygon } innerList.add(innerWay.getId()); } } } } protected class WayHandler implements TObjectProcedure<TDWay> { @Override public boolean execute(TDWay way) { if (way == null) { return true; } // we only consider ways that have tags and which have not already // added as outer way of a relation // inner ways without additional tags are also not considered as they are processed as part of a // multi polygon if (way.isRenderRelevant() && !BaseTileBasedDataProcessor.this.outerToInnerMapping.contains(way.getId()) && !BaseTileBasedDataProcessor.this.innerWaysWithoutAdditionalTags.contains(way.getId())) { addWayToTiles(way, BaseTileBasedDataProcessor.this.bboxEnlargement); } return true; } } }