From 5c2b404b3f139c7ff01b3256e301b119863e59a8 Mon Sep 17 00:00:00 2001 From: Emux Date: Sun, 10 Sep 2017 17:04:19 +0300 Subject: [PATCH] Mapsforge Reverse Geocoding #383 (#398) * Mapsforge Reverse Geocoding #383 * Mapsforge Reverse Geocoding example #383 --- docs/Changelog.md | 1 + vtm-android-example/AndroidManifest.xml | 3 + vtm-android-example/res/values/strings.xml | 1 + .../android/test/MapsforgeMapActivity.java | 11 +- .../oscim/android/test/PoiSearchActivity.java | 7 +- .../android/test/ReverseGeocodeActivity.java | 145 +++++++++ .../src/org/oscim/android/test/Samples.java | 1 + vtm/src/org/oscim/layers/tile/MapTile.java | 5 + .../tiling/source/mapfile/MapDatabase.java | 274 +++++++++++++++++- .../tiling/source/mapfile/MapReadResult.java | 77 +++++ .../source/mapfile/MultiMapDatabase.java | 111 ++++++- .../mapfile/MultiMapFileTileSource.java | 12 +- .../tiling/source/mapfile/PoiWayBundle.java | 28 ++ .../source/mapfile/PointOfInterest.java | 77 +++++ .../source/mapfile/QueryCalculations.java | 56 +++- .../org/oscim/tiling/source/mapfile/Way.java | 114 ++++++++ 16 files changed, 887 insertions(+), 36 deletions(-) create mode 100644 vtm-android-example/src/org/oscim/android/test/ReverseGeocodeActivity.java create mode 100644 vtm/src/org/oscim/tiling/source/mapfile/MapReadResult.java create mode 100644 vtm/src/org/oscim/tiling/source/mapfile/PoiWayBundle.java create mode 100644 vtm/src/org/oscim/tiling/source/mapfile/PointOfInterest.java create mode 100644 vtm/src/org/oscim/tiling/source/mapfile/Way.java diff --git a/docs/Changelog.md b/docs/Changelog.md index fd2ecd67b..e2e2d8fa1 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -6,6 +6,7 @@ - Render themes: line symbol [#124](https://github.com/mapsforge/vtm/issues/124) - Render themes: stroke dash array [#131](https://github.com/mapsforge/vtm/issues/131) - POI Search example [#394](https://github.com/mapsforge/vtm/issues/394) +- Mapsforge Reverse Geocoding [#383](https://github.com/mapsforge/vtm/issues/383) - Core utilities [#396](https://github.com/mapsforge/vtm/issues/396) - Mapsforge fix artifacts zoom > 17 [#231](https://github.com/mapsforge/vtm/issues/231) - vtm-theme-comparator module [#387](https://github.com/mapsforge/vtm/issues/387) diff --git a/vtm-android-example/AndroidManifest.xml b/vtm-android-example/AndroidManifest.xml index dc87e92b7..27e3093b1 100644 --- a/vtm-android-example/AndroidManifest.xml +++ b/vtm-android-example/AndroidManifest.xml @@ -97,6 +97,9 @@ + diff --git a/vtm-android-example/res/values/strings.xml b/vtm-android-example/res/values/strings.xml index be9f04600..434e8d22a 100644 --- a/vtm-android-example/res/values/strings.xml +++ b/vtm-android-example/res/values/strings.xml @@ -18,5 +18,6 @@ Show nature Hide nature Grid + Reverse Geocoding diff --git a/vtm-android-example/src/org/oscim/android/test/MapsforgeMapActivity.java b/vtm-android-example/src/org/oscim/android/test/MapsforgeMapActivity.java index a8d5d0391..03365bd6c 100644 --- a/vtm-android-example/src/org/oscim/android/test/MapsforgeMapActivity.java +++ b/vtm-android-example/src/org/oscim/android/test/MapsforgeMapActivity.java @@ -53,6 +53,7 @@ public class MapsforgeMapActivity extends MapActivity { private TileGridLayer mGridLayer; private DefaultMapScaleBar mMapScaleBar; private Menu mMenu; + MapFileTileSource mTileSource; @Override protected void onCreate(Bundle savedInstanceState) { @@ -152,12 +153,12 @@ protected void onActivityResult(int requestCode, int resultCode, Intent intent) return; } - MapFileTileSource tileSource = new MapFileTileSource(); - tileSource.setPreferredLanguage("en"); + mTileSource = new MapFileTileSource(); + mTileSource.setPreferredLanguage("en"); String file = intent.getStringExtra(FilePicker.SELECTED_FILE); - if (tileSource.setMapFile(file)) { + if (mTileSource.setMapFile(file)) { - VectorTileLayer l = mMap.setBaseMap(tileSource); + VectorTileLayer l = mMap.setBaseMap(mTileSource); loadTheme(null); mMap.layers().add(new BuildingLayer(mMap, l)); @@ -175,7 +176,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent intent) renderer.setOffset(5 * getResources().getDisplayMetrics().density, 0); mMap.layers().add(mapScaleBarLayer); - MapInfo info = tileSource.getMapInfo(); + MapInfo info = mTileSource.getMapInfo(); MapPosition pos = new MapPosition(); pos.setByBoundingBox(info.boundingBox, Tile.SIZE * 4, Tile.SIZE * 4); mMap.setMapPosition(pos); diff --git a/vtm-android-example/src/org/oscim/android/test/PoiSearchActivity.java b/vtm-android-example/src/org/oscim/android/test/PoiSearchActivity.java index 5968f9986..f7f9ba86b 100644 --- a/vtm-android-example/src/org/oscim/android/test/PoiSearchActivity.java +++ b/vtm-android-example/src/org/oscim/android/test/PoiSearchActivity.java @@ -86,8 +86,11 @@ protected void onActivityResult(int requestCode, int resultCode, Intent intent) super.onActivityResult(requestCode, resultCode, intent); if (requestCode == SELECT_MAP_FILE) { - startActivityForResult(new Intent(this, PoiFilePicker.class), - SELECT_POI_FILE); + if (mTileSource != null) + startActivityForResult(new Intent(this, PoiFilePicker.class), + SELECT_POI_FILE); + else + finish(); } else if (requestCode == SELECT_POI_FILE) { if (resultCode != RESULT_OK || intent == null || intent.getStringExtra(FilePicker.SELECTED_FILE) == null) { finish(); diff --git a/vtm-android-example/src/org/oscim/android/test/ReverseGeocodeActivity.java b/vtm-android-example/src/org/oscim/android/test/ReverseGeocodeActivity.java new file mode 100644 index 000000000..184f3fb8c --- /dev/null +++ b/vtm-android-example/src/org/oscim/android/test/ReverseGeocodeActivity.java @@ -0,0 +1,145 @@ +/* + * Copyright 2017 devemux86 + * + * 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 . + */ +package org.oscim.android.test; + +import android.app.AlertDialog; +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; + +import org.oscim.backend.CanvasAdapter; +import org.oscim.core.GeoPoint; +import org.oscim.core.GeometryBuffer; +import org.oscim.core.MercatorProjection; +import org.oscim.core.Point; +import org.oscim.core.Tag; +import org.oscim.core.Tile; +import org.oscim.event.Gesture; +import org.oscim.event.GestureListener; +import org.oscim.event.MotionEvent; +import org.oscim.layers.Layer; +import org.oscim.layers.TileGridLayer; +import org.oscim.map.Map; +import org.oscim.tiling.source.mapfile.MapDatabase; +import org.oscim.tiling.source.mapfile.MapReadResult; +import org.oscim.tiling.source.mapfile.PointOfInterest; +import org.oscim.tiling.source.mapfile.Way; +import org.oscim.utils.GeoPointUtils; + +import java.util.List; + +/** + * Reverse Geocoding with long press. + *

+ * - POI in specified radius.
+ * - Ways containing touch point. + */ +public class ReverseGeocodeActivity extends MapsforgeMapActivity { + + private static final int TOUCH_RADIUS = 32 / 2; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Map events receiver + mMap.layers().add(new MapEventsReceiver(mMap)); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return false; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent intent) { + super.onActivityResult(requestCode, resultCode, intent); + + if (requestCode == SELECT_MAP_FILE) { + // For debug + TileGridLayer gridLayer = new TileGridLayer(mMap, getResources().getDisplayMetrics().density); + mMap.layers().add(gridLayer); + } + } + + private class MapEventsReceiver extends Layer implements GestureListener { + + MapEventsReceiver(Map map) { + super(map); + } + + @Override + public boolean onGesture(Gesture g, MotionEvent e) { + if (g instanceof Gesture.LongPress) { + GeoPoint p = mMap.viewport().fromScreenPoint(e.getX(), e.getY()); + + // Read all labeled POI and ways for the area covered by the tiles under touch + float touchRadius = TOUCH_RADIUS * CanvasAdapter.dpi / CanvasAdapter.DEFAULT_DPI; + long mapSize = MercatorProjection.getMapSize((byte) mMap.getMapPosition().getZoomLevel()); + double pixelX = MercatorProjection.longitudeToPixelX(p.getLongitude(), mapSize); + double pixelY = MercatorProjection.latitudeToPixelY(p.getLatitude(), mapSize); + int tileXMin = MercatorProjection.pixelXToTileX(pixelX - touchRadius, (byte) mMap.getMapPosition().getZoomLevel()); + int tileXMax = MercatorProjection.pixelXToTileX(pixelX + touchRadius, (byte) mMap.getMapPosition().getZoomLevel()); + int tileYMin = MercatorProjection.pixelYToTileY(pixelY - touchRadius, (byte) mMap.getMapPosition().getZoomLevel()); + int tileYMax = MercatorProjection.pixelYToTileY(pixelY + touchRadius, (byte) mMap.getMapPosition().getZoomLevel()); + Tile upperLeft = new Tile(tileXMin, tileYMin, (byte) mMap.getMapPosition().getZoomLevel()); + Tile lowerRight = new Tile(tileXMax, tileYMax, (byte) mMap.getMapPosition().getZoomLevel()); + MapReadResult mapReadResult = ((MapDatabase) mTileSource.getDataSource()).readLabels(upperLeft, lowerRight); + + StringBuilder sb = new StringBuilder(); + + // Filter POI + sb.append("*** POI ***"); + for (PointOfInterest pointOfInterest : mapReadResult.pointOfInterests) { + Point layerXY = new Point(); + mMap.viewport().toScreenPoint(pointOfInterest.position, false, layerXY); + Point tapXY = new Point(e.getX(), e.getY()); + if (layerXY.distance(tapXY) > touchRadius) { + continue; + } + sb.append("\n"); + List tags = pointOfInterest.tags; + for (Tag tag : tags) { + sb.append("\n").append(tag.key).append("=").append(tag.value); + } + } + + // Filter ways + sb.append("\n\n").append("*** WAYS ***"); + for (Way way : mapReadResult.ways) { + if (way.geometryType != GeometryBuffer.GeometryType.POLY + || !GeoPointUtils.contains(way.geoPoints[0], p)) { + continue; + } + sb.append("\n"); + List tags = way.tags; + for (Tag tag : tags) { + sb.append("\n").append(tag.key).append("=").append(tag.value); + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(ReverseGeocodeActivity.this); + builder.setIcon(android.R.drawable.ic_menu_search); + builder.setTitle(R.string.dialog_reverse_geocoding_title); + builder.setMessage(sb); + builder.setPositiveButton(R.string.ok, null); + builder.show(); + + return true; + } + return false; + } + } +} diff --git a/vtm-android-example/src/org/oscim/android/test/Samples.java b/vtm-android-example/src/org/oscim/android/test/Samples.java index 5289c016d..4198bd8b1 100644 --- a/vtm-android-example/src/org/oscim/android/test/Samples.java +++ b/vtm-android-example/src/org/oscim/android/test/Samples.java @@ -122,6 +122,7 @@ public void onClick(View v) { linearLayout.addView(createButton(MultiMapActivity.class)); linearLayout.addView(createLabel("Experiments")); + linearLayout.addView(createButton(ReverseGeocodeActivity.class)); linearLayout.addView(createButton(MapPositionActivity.class)); linearLayout.addView(createButton(S3DBMapActivity.class)); linearLayout.addView(createButton(ThemeStylerActivity.class)); diff --git a/vtm/src/org/oscim/layers/tile/MapTile.java b/vtm/src/org/oscim/layers/tile/MapTile.java index f3df7ebd6..a4a7380bf 100644 --- a/vtm/src/org/oscim/layers/tile/MapTile.java +++ b/vtm/src/org/oscim/layers/tile/MapTile.java @@ -1,5 +1,6 @@ /* * Copyright 2012, 2013 Hannes Janetzek + * Copyright 2017 devemux86 * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -158,6 +159,10 @@ public TileData next() { } } + public MapTile(int tileX, int tileY, int zoomLevel) { + this(null, tileX, tileY, zoomLevel); + } + public MapTile(TileNode node, int tileX, int tileY, int zoomLevel) { super(tileX, tileY, (byte) zoomLevel); this.x = (double) tileX / (1 << zoomLevel); diff --git a/vtm/src/org/oscim/tiling/source/mapfile/MapDatabase.java b/vtm/src/org/oscim/tiling/source/mapfile/MapDatabase.java index e97b923a6..11da4e4cd 100644 --- a/vtm/src/org/oscim/tiling/source/mapfile/MapDatabase.java +++ b/vtm/src/org/oscim/tiling/source/mapfile/MapDatabase.java @@ -1,6 +1,7 @@ /* * Copyright 2010, 2011, 2012 mapsforge.org * Copyright 2013, 2014 Hannes Janetzek + * Copyright 2014-2015 Ludwig M Brinckmann * Copyright 2016-2017 devemux86 * Copyright 2016 Andrey Novikov * @@ -20,6 +21,8 @@ package org.oscim.tiling.source.mapfile; import org.oscim.backend.CanvasAdapter; +import org.oscim.core.BoundingBox; +import org.oscim.core.GeoPoint; import org.oscim.core.GeometryBuffer.GeometryType; import org.oscim.core.MapElement; import org.oscim.core.MercatorProjection; @@ -35,6 +38,9 @@ import java.io.IOException; import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import static org.oscim.core.GeometryBuffer.GeometryType.LINE; import static org.oscim.core.GeometryBuffer.GeometryType.POLY; @@ -167,6 +173,18 @@ public class MapDatabase implements ITileDataSource { */ private static final int WAY_NUMBER_OF_TAGS_BITMASK = 0x0f; + /** + * Way filtering reduces the number of ways returned to only those that are + * relevant for the tile requested, leading to performance gains, but can + * cause line clipping artifacts (particularly at higher zoom levels). The + * risk of clipping can be reduced by either turning way filtering off or by + * increasing the wayFilterDistance which governs how large an area surrounding + * the requested tile will be returned. + * For most use cases the standard settings should be sufficient. + */ + public static boolean wayFilterEnabled = true; + public static int wayFilterDistance = 20; + private long mFileSize; private boolean mDebugFile; private RandomAccessFile mInputFile; @@ -187,6 +205,9 @@ public class MapDatabase implements ITileDataSource { private final MapFileTileSource mTileSource; + private int zoomLevelMin = 0; + private int zoomLevelMax = Byte.MAX_VALUE; + public MapDatabase(MapFileTileSource tileSource) throws IOException { mTileSource = tileSource; try { @@ -305,7 +326,9 @@ private void logDebugSignatures() { * @param mapDataSink the callback which handles the extracted map elements. */ private void processBlock(QueryParameters queryParameters, - SubFileParameter subFileParameter, ITileDataSink mapDataSink) { + SubFileParameter subFileParameter, ITileDataSink mapDataSink, + BoundingBox boundingBox, Selector selector, + MapReadResult mapReadResult) { if (!processBlockSignature()) { return; @@ -339,7 +362,13 @@ private void processBlock(QueryParameters queryParameters, return; } - if (!processPOIs(mapDataSink, poisOnQueryZoomLevel)) { + boolean filterRequired = queryParameters.queryZoomLevel > subFileParameter.baseZoomLevel; + + List pois = null; + if (mapReadResult != null) + pois = new ArrayList<>(); + + if (!processPOIs(mapDataSink, poisOnQueryZoomLevel, boundingBox, filterRequired, pois)) { return; } @@ -355,10 +384,19 @@ private void processBlock(QueryParameters queryParameters, /* move the pointer to the first way */ mReadBuffer.setBufferPosition(firstWayOffset); - if (!processWays(queryParameters, mapDataSink, waysOnQueryZoomLevel)) { + List ways = null; + if (mapReadResult != null && Selector.POIS != selector) + ways = new ArrayList<>(); + + if (!processWays(queryParameters, mapDataSink, waysOnQueryZoomLevel, boundingBox, filterRequired, selector, ways)) { return; } + if (mapReadResult != null) { + if (Selector.POIS == selector) + ways = Collections.emptyList(); + mapReadResult.add(new PoiWayBundle(pois, ways)); + } } // private long mCurrentRow; @@ -405,8 +443,26 @@ private void setTileClipping(QueryParameters queryParameters, long mCurrentRow, //private final static Tag mWaterTag = new Tag("natural", "water"); + /** + * Map rendering. + */ private void processBlocks(ITileDataSink mapDataSink, QueryParameters queryParams, SubFileParameter subFileParameter) throws IOException { + processBlocks(mapDataSink, queryParams, subFileParameter, null, null, null); + } + + /** + * Map data reading. + */ + private void processBlocks(QueryParameters queryParams, + SubFileParameter subFileParameter, BoundingBox boundingBox, + Selector selector, MapReadResult mapReadResult) throws IOException { + processBlocks(null, queryParams, subFileParameter, boundingBox, selector, mapReadResult); + } + + private void processBlocks(ITileDataSink mapDataSink, QueryParameters queryParams, + SubFileParameter subFileParameter, BoundingBox boundingBox, + Selector selector, MapReadResult mapReadResult) throws IOException { /* read and process all blocks from top to bottom and from left to right */ for (long row = queryParams.fromBlockY; row <= queryParams.toBlockY; row++) { @@ -508,7 +564,7 @@ private void processBlocks(ITileDataSink mapDataSink, QueryParameters queryParam mTileLatitude = (int) (tileLatitudeDeg * 1E6); mTileLongitude = (int) (tileLongitudeDeg * 1E6); - processBlock(queryParams, subFileParameter, mapDataSink); + processBlock(queryParams, subFileParameter, mapDataSink, boundingBox, selector, mapReadResult); } } } @@ -539,7 +595,8 @@ private boolean processBlockSignature() { * @return true if the POIs could be processed successfully, false * otherwise. */ - private boolean processPOIs(ITileDataSink mapDataSink, int numberOfPois) { + private boolean processPOIs(ITileDataSink mapDataSink, int numberOfPois, BoundingBox boundingBox, + boolean filterRequired, List pois) { Tag[] poiTags = mTileSource.fileInfo.poiTags; MapElement e = mElem; @@ -605,13 +662,26 @@ private boolean processPOIs(ITileDataSink mapDataSink, int numberOfPois) { e.setLayer(layer); - mapDataSink.process(e); + if (pois != null) { + List tags = new ArrayList<>(); + for (int i = 0; i < e.tags.numTags; i++) + tags.add(e.tags.tags[i]); + GeoPoint position = new GeoPoint(latitude, longitude); + // depending on the zoom level configuration the poi can lie outside + // the tile requested, we filter them out here + if (!filterRequired || boundingBox.contains(position)) { + pois.add(new PointOfInterest(layer, tags, position)); + } + } + + if (mapDataSink != null) + mapDataSink.process(e); } return true; } - private boolean processWayDataBlock(MapElement e, boolean doubleDeltaEncoding, boolean isLine) { + private boolean processWayDataBlock(MapElement e, boolean doubleDeltaEncoding, boolean isLine, List wayCoordinates) { /* get and check the number of way coordinate blocks (VBE-U) */ int numBlocks = mReadBuffer.readUnsignedInt(); if (numBlocks < 1 || numBlocks > Short.MAX_VALUE) { @@ -638,6 +708,14 @@ private boolean processWayDataBlock(MapElement e, boolean doubleDeltaEncoding, b wayLengths[coordinateBlock] = decodeWayNodes(doubleDeltaEncoding, e, len, isLine); + + if (wayCoordinates != null) { + // create the array which will store the current way segment + GeoPoint[] waySegment = new GeoPoint[e.getNumPoints()]; + for (int i = 0; i < e.getNumPoints(); i++) + waySegment[i] = new GeoPoint(e.getPointY(i) / 1E6, e.getPointX(i) / 1E6); + wayCoordinates.add(waySegment); + } } return true; @@ -712,8 +790,9 @@ private int decodeWayNodes(boolean doubleDelta, MapElement e, int length, boolea * @return true if the ways could be processed successfully, false * otherwise. */ - private boolean processWays(QueryParameters queryParameters, - ITileDataSink mapDataSink, int numberOfWays) { + private boolean processWays(QueryParameters queryParameters, ITileDataSink mapDataSink, + int numberOfWays, BoundingBox boundingBox, boolean filterRequired, + Selector selector, List ways) { Tag[] wayTags = mTileSource.fileInfo.wayTags; MapElement e = mElem; @@ -866,7 +945,11 @@ private boolean processWays(QueryParameters queryParameters, for (int wayDataBlock = 0; wayDataBlock < wayDataBlocks; wayDataBlock++) { e.clear(); - if (!processWayDataBlock(e, featureWayDoubleDeltaEncoding, linearFeature)) + List wayNodes = null; + if (ways != null) + wayNodes = new ArrayList<>(); + + if (!processWayDataBlock(e, featureWayDoubleDeltaEncoding, linearFeature, wayNodes)) return false; /* drop invalid outer ring */ @@ -889,13 +972,112 @@ private boolean processWays(QueryParameters queryParameters, e.setLayer(layer); - mapDataSink.process(e); + if (ways != null) { + BoundingBox wayFilterBbox = boundingBox.extendMeters(wayFilterDistance); + GeoPoint[][] wayNodesArray = wayNodes.toArray(new GeoPoint[wayNodes.size()][]); + if (!filterRequired || !wayFilterEnabled || wayFilterBbox.intersectsArea(wayNodesArray)) { + List tags = new ArrayList<>(); + for (int i = 0; i < e.tags.numTags; i++) + tags.add(e.tags.tags[i]); + if (Selector.ALL == selector || hasName || hasHouseNr || hasRef || wayAsLabelTagFilter(tags)) { + GeoPoint labelPos = e.labelPosition != null ? new GeoPoint(e.labelPosition.y / 1E6, e.labelPosition.x / 1E6) : null; + ways.add(new Way(layer, tags, wayNodesArray, labelPos, e.type)); + } + } + } + + if (mapDataSink != null) + mapDataSink.process(e); } } return true; } + /** + * Reads only labels for tile. + * + * @param tile tile for which data is requested. + * @return label data for the tile. + */ + public MapReadResult readLabels(Tile tile) { + return readMapData(tile, tile, Selector.LABELS); + } + + /** + * Reads data for an area defined by the tile in the upper left and the tile in + * the lower right corner. + * Precondition: upperLeft.tileX <= lowerRight.tileX && upperLeft.tileY <= lowerRight.tileY + * + * @param upperLeft tile that defines the upper left corner of the requested area. + * @param lowerRight tile that defines the lower right corner of the requested area. + * @return map data for the tile. + */ + public MapReadResult readLabels(Tile upperLeft, Tile lowerRight) { + return readMapData(upperLeft, lowerRight, Selector.LABELS); + } + + /** + * Reads all map data for the area covered by the given tile at the tile zoom level. + * + * @param tile defines area and zoom level of read map data. + * @return the read map data. + */ + public MapReadResult readMapData(Tile tile) { + return readMapData(tile, tile, Selector.ALL); + } + + /** + * Reads data for an area defined by the tile in the upper left and the tile in + * the lower right corner. + * Precondition: upperLeft.tileX <= lowerRight.tileX && upperLeft.tileY <= lowerRight.tileY + * + * @param upperLeft tile that defines the upper left corner of the requested area. + * @param lowerRight tile that defines the lower right corner of the requested area. + * @return map data for the tile. + */ + public MapReadResult readMapData(Tile upperLeft, Tile lowerRight) { + return readMapData(upperLeft, lowerRight, Selector.ALL); + } + + private MapReadResult readMapData(Tile upperLeft, Tile lowerRight, Selector selector) { + if (mTileSource.fileHeader == null) + return null; + + MapReadResult mapReadResult = new MapReadResult(); + + if (mIntBuffer == null) + mIntBuffer = new int[Short.MAX_VALUE * 2]; + + try { + mTileProjection.setTile(upperLeft); + + QueryParameters queryParameters = new QueryParameters(); + queryParameters.queryZoomLevel = + mTileSource.fileHeader.getQueryZoomLevel(upperLeft.zoomLevel); + + /* get and check the sub-file for the query zoom level */ + SubFileParameter subFileParameter = + mTileSource.fileHeader.getSubFileParameter(queryParameters.queryZoomLevel); + + if (subFileParameter == null) { + log.warn("no sub-file for zoom level: " + + queryParameters.queryZoomLevel); + + return null; + } + + QueryCalculations.calculateBaseTiles(queryParameters, upperLeft, lowerRight, subFileParameter); + QueryCalculations.calculateBlocks(queryParameters, subFileParameter); + processBlocks(queryParameters, subFileParameter, Tile.getBoundingBox(upperLeft, lowerRight), selector, mapReadResult); + } catch (IOException e) { + log.error(e.getMessage()); + return null; + } + + return mapReadResult; + } + private int[] readOptionalLabelPosition() { int[] labelPosition = new int[2]; @@ -908,6 +1090,29 @@ private int[] readOptionalLabelPosition() { return labelPosition; } + /** + * Reads only POI data for tile. + * + * @param tile tile for which data is requested. + * @return POI data for the tile. + */ + public MapReadResult readPoiData(Tile tile) { + return readMapData(tile, tile, Selector.POIS); + } + + /** + * Reads POI data for an area defined by the tile in the upper left and the tile in + * the lower right corner. + * This implementation takes the data storage of a MapFile into account for greater efficiency. + * + * @param upperLeft tile that defines the upper left corner of the requested area. + * @param lowerRight tile that defines the lower right corner of the requested area. + * @return map data for the tile. + */ + public MapReadResult readPoiData(Tile upperLeft, Tile lowerRight) { + return readMapData(upperLeft, lowerRight, Selector.POIS); + } + private int[][] readZoomTable(SubFileParameter subFileParameter) { int rows = subFileParameter.zoomLevelMax - subFileParameter.zoomLevelMin + 1; int[][] zoomTable = new int[rows][2]; @@ -926,6 +1131,51 @@ private int[][] readZoomTable(SubFileParameter subFileParameter) { return zoomTable; } + /** + * Restricts returns of data to zoom level range specified. This can be used to restrict + * the use of this map data base when used in MultiMapDatabase settings. + * + * @param minZoom minimum zoom level supported + * @param maxZoom maximum zoom level supported + */ + public void restrictToZoomRange(int minZoom, int maxZoom) { + this.zoomLevelMax = maxZoom; + this.zoomLevelMin = minZoom; + } + + /** + * Returns true if MapDatabase contains tile. + * + * @param tile tile to be rendered. + * @return true if tile is part of database. + */ + public boolean supportsTile(Tile tile) { + return tile.getBoundingBox().intersects(mTileSource.getMapInfo().boundingBox) + && (tile.zoomLevel >= this.zoomLevelMin && tile.zoomLevel <= this.zoomLevelMax); + } + + /** + * Returns true if a way should be included in the result set for readLabels() + * By default only ways with names, house numbers or a ref are included in the result set + * of readLabels(). This is to reduce the set of ways as much as possible to save memory. + * + * @param tags the tags associated with the way + * @return true if the way should be included in the result set + */ + public boolean wayAsLabelTagFilter(List tags) { + return false; + } + + /** + * The Selector enum is used to specify which data subset is to be retrieved from a MapFile: + * ALL: all data (as in version 0.6.0) + * POIS: only poi data, no ways (new after 0.6.0) + * LABELS: poi data and ways that have a name (new after 0.6.0) + */ + private enum Selector { + ALL, POIS, LABELS + } + static class TileProjection { private static final double COORD_SCALE = 1000000.0; @@ -947,7 +1197,7 @@ void setTile(Tile tile) { /* scales longitude(1e6) to map-pixel */ divx = (180.0 * COORD_SCALE) / (mapExtents >> 1); - /* scale latidute to map-pixel */ + /* scale latitude to map-pixel */ divy = (Math.PI * 2.0) / (mapExtents >> 1); } diff --git a/vtm/src/org/oscim/tiling/source/mapfile/MapReadResult.java b/vtm/src/org/oscim/tiling/source/mapfile/MapReadResult.java new file mode 100644 index 000000000..06bd183f3 --- /dev/null +++ b/vtm/src/org/oscim/tiling/source/mapfile/MapReadResult.java @@ -0,0 +1,77 @@ +/* + * Copyright 2010, 2011, 2012, 2013 mapsforge.org + * Copyright 2014-2015 Ludwig M Brinckmann + * Copyright 2017 devemux86 + * + * 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 . + */ +package org.oscim.tiling.source.mapfile; + +import java.util.ArrayList; +import java.util.List; + +/** + * An immutable container for the data returned from a MapDataStore. + */ +public class MapReadResult { + + /** + * True if the read area is completely covered by water, false otherwise. + */ + public boolean isWater; + + /** + * The read POIs. + */ + public List pointOfInterests; + + /** + * The read ways. + */ + public List ways; + + public MapReadResult() { + this.pointOfInterests = new ArrayList<>(); + this.ways = new ArrayList<>(); + } + + public void add(PoiWayBundle poiWayBundle) { + this.pointOfInterests.addAll(poiWayBundle.pois); + this.ways.addAll(poiWayBundle.ways); + } + + /** + * Adds other MapReadResult by combining pois and ways. Optionally, deduplication can + * be requested (much more expensive). + * + * @param other the MapReadResult to add to this. + * @param deduplicate true if check for duplicates is required. + */ + public void add(MapReadResult other, boolean deduplicate) { + if (deduplicate) { + for (PointOfInterest poi : other.pointOfInterests) { + if (!this.pointOfInterests.contains(poi)) { + this.pointOfInterests.add(poi); + } + } + for (Way way : other.ways) { + if (!this.ways.contains(way)) { + this.ways.add(way); + } + } + } else { + this.pointOfInterests.addAll(other.pointOfInterests); + this.ways.addAll(other.ways); + } + } + +} diff --git a/vtm/src/org/oscim/tiling/source/mapfile/MultiMapDatabase.java b/vtm/src/org/oscim/tiling/source/mapfile/MultiMapDatabase.java index b65ffb17d..468785a7f 100644 --- a/vtm/src/org/oscim/tiling/source/mapfile/MultiMapDatabase.java +++ b/vtm/src/org/oscim/tiling/source/mapfile/MultiMapDatabase.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 devemux86 + * Copyright 2016-2017 devemux86 * * 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 @@ -14,6 +14,7 @@ */ package org.oscim.tiling.source.mapfile; +import org.oscim.core.Tile; import org.oscim.layers.tile.MapTile; import org.oscim.tiling.ITileDataSink; import org.oscim.tiling.ITileDataSource; @@ -41,8 +42,7 @@ public boolean add(MapDatabase mapDatabase) { public void query(MapTile tile, ITileDataSink mapDataSink) { MultiMapDataSink multiMapDataSink = new MultiMapDataSink(mapDataSink); for (MapDatabase mapDatabase : mapDatabases) { - int[] zoomLevels = tileSource.getZoomsByTileSource().get(mapDatabase.getTileSource()); - if (zoomLevels == null || (zoomLevels[0] <= tile.zoomLevel && tile.zoomLevel <= zoomLevels[1])) + if (mapDatabase.supportsTile(tile)) mapDatabase.query(tile, multiMapDataSink); } mapDataSink.completed(multiMapDataSink.getResult()); @@ -61,4 +61,109 @@ public void cancel() { mapDatabase.cancel(); } } + + public MapReadResult readLabels(Tile tile) { + MapReadResult mapReadResult = new MapReadResult(); + for (MapDatabase mdb : mapDatabases) { + if (mdb.supportsTile(tile)) { + MapReadResult result = mdb.readLabels(tile); + if (result == null) { + continue; + } + boolean isWater = mapReadResult.isWater & result.isWater; + mapReadResult.isWater = isWater; + mapReadResult.add(result, false); + } + } + return mapReadResult; + } + + public MapReadResult readLabels(Tile upperLeft, Tile lowerRight) { + MapReadResult mapReadResult = new MapReadResult(); + for (MapDatabase mdb : mapDatabases) { + if (mdb.supportsTile(upperLeft)) { + MapReadResult result = mdb.readLabels(upperLeft, lowerRight); + if (result == null) { + continue; + } + boolean isWater = mapReadResult.isWater & result.isWater; + mapReadResult.isWater = isWater; + mapReadResult.add(result, false); + } + } + return mapReadResult; + } + + public MapReadResult readMapData(Tile tile) { + MapReadResult mapReadResult = new MapReadResult(); + for (MapDatabase mdb : mapDatabases) { + if (mdb.supportsTile(tile)) { + MapReadResult result = mdb.readMapData(tile); + if (result == null) { + continue; + } + boolean isWater = mapReadResult.isWater & result.isWater; + mapReadResult.isWater = isWater; + mapReadResult.add(result, false); + } + } + return mapReadResult; + } + + public MapReadResult readMapData(Tile upperLeft, Tile lowerRight) { + MapReadResult mapReadResult = new MapReadResult(); + for (MapDatabase mdb : mapDatabases) { + if (mdb.supportsTile(upperLeft)) { + MapReadResult result = mdb.readMapData(upperLeft, lowerRight); + if (result == null) { + continue; + } + boolean isWater = mapReadResult.isWater & result.isWater; + mapReadResult.isWater = isWater; + mapReadResult.add(result, false); + } + } + return mapReadResult; + } + + public MapReadResult readPoiData(Tile tile) { + MapReadResult mapReadResult = new MapReadResult(); + for (MapDatabase mdb : mapDatabases) { + if (mdb.supportsTile(tile)) { + MapReadResult result = mdb.readPoiData(tile); + if (result == null) { + continue; + } + boolean isWater = mapReadResult.isWater & result.isWater; + mapReadResult.isWater = isWater; + mapReadResult.add(result, false); + } + } + return mapReadResult; + } + + public MapReadResult readPoiData(Tile upperLeft, Tile lowerRight) { + MapReadResult mapReadResult = new MapReadResult(); + for (MapDatabase mdb : mapDatabases) { + if (mdb.supportsTile(upperLeft)) { + MapReadResult result = mdb.readPoiData(upperLeft, lowerRight); + if (result == null) { + continue; + } + boolean isWater = mapReadResult.isWater & result.isWater; + mapReadResult.isWater = isWater; + mapReadResult.add(result, false); + } + } + return mapReadResult; + } + + public boolean supportsTile(Tile tile) { + for (MapDatabase mdb : mapDatabases) { + if (mdb.supportsTile(tile)) { + return true; + } + } + return false; + } } diff --git a/vtm/src/org/oscim/tiling/source/mapfile/MultiMapFileTileSource.java b/vtm/src/org/oscim/tiling/source/mapfile/MultiMapFileTileSource.java index 93bed95f8..96fbf24e6 100644 --- a/vtm/src/org/oscim/tiling/source/mapfile/MultiMapFileTileSource.java +++ b/vtm/src/org/oscim/tiling/source/mapfile/MultiMapFileTileSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 devemux86 + * Copyright 2016-2017 devemux86 * * 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 @@ -63,16 +63,16 @@ public BoundingBox getBoundingBox() { return boundingBox; } - Map getZoomsByTileSource() { - return zoomsByTileSource; - } - @Override public ITileDataSource getDataSource() { MultiMapDatabase multiMapDatabase = new MultiMapDatabase(this); for (MapFileTileSource mapFileTileSource : mapFileTileSources) { try { - multiMapDatabase.add(new MapDatabase(mapFileTileSource)); + MapDatabase mapDatabase = new MapDatabase(mapFileTileSource); + int[] zoomLevels = zoomsByTileSource.get(mapFileTileSource); + if (zoomLevels != null) + mapDatabase.restrictToZoomRange(zoomLevels[0], zoomLevels[1]); + multiMapDatabase.add(mapDatabase); } catch (IOException e) { log.debug(e.getMessage()); } diff --git a/vtm/src/org/oscim/tiling/source/mapfile/PoiWayBundle.java b/vtm/src/org/oscim/tiling/source/mapfile/PoiWayBundle.java new file mode 100644 index 000000000..757222023 --- /dev/null +++ b/vtm/src/org/oscim/tiling/source/mapfile/PoiWayBundle.java @@ -0,0 +1,28 @@ +/* + * Copyright 2010, 2011, 2012, 2013 mapsforge.org + * Copyright 2017 devemux86 + * + * 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 . + */ +package org.oscim.tiling.source.mapfile; + +import java.util.List; + +public class PoiWayBundle { + final List pois; + final List ways; + + public PoiWayBundle(List pois, List ways) { + this.pois = pois; + this.ways = ways; + } +} diff --git a/vtm/src/org/oscim/tiling/source/mapfile/PointOfInterest.java b/vtm/src/org/oscim/tiling/source/mapfile/PointOfInterest.java new file mode 100644 index 000000000..e2a212cf5 --- /dev/null +++ b/vtm/src/org/oscim/tiling/source/mapfile/PointOfInterest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2010, 2011, 2012, 2013 mapsforge.org + * Copyright 2014-2015 Ludwig M Brinckmann + * Copyright 2017 devemux86 + * + * 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 . + */ +package org.oscim.tiling.source.mapfile; + +import org.oscim.core.GeoPoint; +import org.oscim.core.Tag; + +import java.util.List; + +/** + * An immutable container for all data associated with a single point of interest node (POI). + */ +public class PointOfInterest { + /** + * The layer of this POI + 5 (to avoid negative values). + */ + public final byte layer; + + /** + * The position of this POI. + */ + public final GeoPoint position; + + /** + * The tags of this POI. + */ + public final List tags; + + public PointOfInterest(byte layer, List tags, GeoPoint position) { + this.layer = layer; + this.tags = tags; + this.position = position; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof PointOfInterest)) { + return false; + } + PointOfInterest other = (PointOfInterest) obj; + if (this.layer != other.layer) { + return false; + } else if (!this.tags.equals(other.tags)) { + return false; + } else if (!this.position.equals(other.position)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + layer; + result = prime * result + tags.hashCode(); + result = prime * result + position.hashCode(); + return result; + } + +} diff --git a/vtm/src/org/oscim/tiling/source/mapfile/QueryCalculations.java b/vtm/src/org/oscim/tiling/source/mapfile/QueryCalculations.java index a3d4dd524..2f3805e6f 100644 --- a/vtm/src/org/oscim/tiling/source/mapfile/QueryCalculations.java +++ b/vtm/src/org/oscim/tiling/source/mapfile/QueryCalculations.java @@ -1,5 +1,7 @@ /* * Copyright 2010, 2011, 2012 mapsforge.org + * Copyright 2014-2015 Ludwig M Brinckmann + * Copyright 2017 devemux86 * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -100,18 +102,14 @@ private static int getSecondLevelTileBitmaskUpperRight(long subtileX, long subti } } - static void calculateBaseTiles(QueryParameters queryParameters, Tile tile, - SubFileParameter subFileParameter) { + static void calculateBaseTiles(QueryParameters queryParameters, Tile tile, SubFileParameter subFileParameter) { if (tile.zoomLevel < subFileParameter.baseZoomLevel) { - // calculate the XY numbers of the upper left and lower right - // sub-tiles + // calculate the XY numbers of the upper left and lower right sub-tiles int zoomLevelDifference = subFileParameter.baseZoomLevel - tile.zoomLevel; queryParameters.fromBaseTileX = tile.tileX << zoomLevelDifference; queryParameters.fromBaseTileY = tile.tileY << zoomLevelDifference; - queryParameters.toBaseTileX = queryParameters.fromBaseTileX - + (1 << zoomLevelDifference) - 1; - queryParameters.toBaseTileY = queryParameters.fromBaseTileY - + (1 << zoomLevelDifference) - 1; + queryParameters.toBaseTileX = queryParameters.fromBaseTileX + (1 << zoomLevelDifference) - 1; + queryParameters.toBaseTileY = queryParameters.fromBaseTileY + (1 << zoomLevelDifference) - 1; queryParameters.useTileBitmask = false; } else if (tile.zoomLevel > subFileParameter.baseZoomLevel) { // calculate the XY numbers of the parent base tile @@ -132,6 +130,37 @@ static void calculateBaseTiles(QueryParameters queryParameters, Tile tile, } } + static void calculateBaseTiles(QueryParameters queryParameters, Tile upperLeft, Tile lowerRight, SubFileParameter subFileParameter) { + if (upperLeft.zoomLevel < subFileParameter.baseZoomLevel) { + // here we need to combine multiple base tiles + int zoomLevelDifference = subFileParameter.baseZoomLevel - upperLeft.zoomLevel; + queryParameters.fromBaseTileX = upperLeft.tileX << zoomLevelDifference; + queryParameters.fromBaseTileY = upperLeft.tileY << zoomLevelDifference; + queryParameters.toBaseTileX = (lowerRight.tileX << zoomLevelDifference) + (1 << zoomLevelDifference) - 1; + queryParameters.toBaseTileY = (lowerRight.tileY << zoomLevelDifference) + (1 << zoomLevelDifference) - 1; + queryParameters.useTileBitmask = false; + } else if (upperLeft.zoomLevel > subFileParameter.baseZoomLevel) { + // we might have more than just one base tile as we might span boundaries + int zoomLevelDifference = upperLeft.zoomLevel - subFileParameter.baseZoomLevel; + queryParameters.fromBaseTileX = upperLeft.tileX >>> zoomLevelDifference; + queryParameters.fromBaseTileY = upperLeft.tileY >>> zoomLevelDifference; + queryParameters.toBaseTileX = lowerRight.tileX >>> zoomLevelDifference; + queryParameters.toBaseTileY = lowerRight.tileY >>> zoomLevelDifference; + // TODO understand what is going on here. The tileBitmask is used to extract just + // the data from the base tiles that is relevant for the area, but how can this work + // for a set of tiles, so not using tileBitmask for the moment. + queryParameters.useTileBitmask = true; + queryParameters.queryTileBitmask = QueryCalculations.calculateTileBitmask(upperLeft, lowerRight, zoomLevelDifference); + } else { + // we are on the base zoom level, so we just need all tiles in range + queryParameters.fromBaseTileX = upperLeft.tileX; + queryParameters.fromBaseTileY = upperLeft.tileY; + queryParameters.toBaseTileX = lowerRight.tileX; + queryParameters.toBaseTileY = lowerRight.tileY; + queryParameters.useTileBitmask = false; + } + } + static void calculateBlocks(QueryParameters queryParameters, SubFileParameter subFileParameter) { // calculate the blocks in the file which need to be read queryParameters.fromBlockX = Math.max(queryParameters.fromBaseTileX @@ -171,6 +200,17 @@ static int calculateTileBitmask(Tile tile, int zoomLevelDifference) { } } + static int calculateTileBitmask(Tile upperLeft, Tile lowerRight, int zoomLevelDifference) { + int bitmask = 0; + for (int x = upperLeft.tileX; x <= lowerRight.tileX; x++) { + for (int y = upperLeft.tileY; y <= lowerRight.tileY; y++) { + Tile current = new Tile(x, y, upperLeft.zoomLevel); + bitmask |= calculateTileBitmask(current, zoomLevelDifference); + } + } + return bitmask; + } + private QueryCalculations() { throw new IllegalStateException(); } diff --git a/vtm/src/org/oscim/tiling/source/mapfile/Way.java b/vtm/src/org/oscim/tiling/source/mapfile/Way.java new file mode 100644 index 000000000..2ab54c6ce --- /dev/null +++ b/vtm/src/org/oscim/tiling/source/mapfile/Way.java @@ -0,0 +1,114 @@ +/* + * Copyright 2010, 2011, 2012, 2013 mapsforge.org + * Copyright 2014-2015 Ludwig M Brinckmann + * Copyright 2017 devemux86 + * + * 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 . + */ +package org.oscim.tiling.source.mapfile; + +import org.oscim.core.GeoPoint; +import org.oscim.core.GeometryBuffer; +import org.oscim.core.Tag; + +import java.util.Arrays; +import java.util.List; + +/** + * An immutable container for all data associated with a single way or area (closed way). + */ +public class Way { + /** + * The position of the area label (may be null). + */ + public final GeoPoint labelPosition; + + /** + * The geometry type. + */ + public GeometryBuffer.GeometryType geometryType = GeometryBuffer.GeometryType.NONE; + + /** + * The geographical coordinates of the way nodes. + */ + public final GeoPoint[][] geoPoints; + + /** + * The layer of this way + 5 (to avoid negative values). + */ + public final byte layer; + + /** + * The tags of this way. + */ + public final List tags; + + public Way(byte layer, List tags, GeoPoint[][] geoPoints, GeoPoint labelPosition) { + this.layer = layer; + this.tags = tags; + this.geoPoints = geoPoints; + this.labelPosition = labelPosition; + } + + public Way(byte layer, List tags, GeoPoint[][] geoPoints, GeoPoint labelPosition, final GeometryBuffer.GeometryType geometryType) { + this(layer, tags, geoPoints, labelPosition); + this.geometryType = geometryType; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (!(obj instanceof Way)) { + return false; + } + Way other = (Way) obj; + if (this.layer != other.layer) { + return false; + } else if (!this.tags.equals(other.tags)) { + return false; + } else if (this.labelPosition == null && other.labelPosition != null) { + return false; + } else if (this.labelPosition != null && !this.labelPosition.equals(other.labelPosition)) { + return false; + } else if (this.geoPoints.length != other.geoPoints.length) { + return false; + } else { + for (int i = 0; i < this.geoPoints.length; i++) { + if (this.geoPoints[i].length != other.geoPoints[i].length) { + return false; + } else { + for (int j = 0; j < this.geoPoints[i].length; j++) { + if (!geoPoints[i][j].equals(other.geoPoints[i][j])) { + return false; + } + } + } + } + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + layer; + result = prime * result + tags.hashCode(); + result = prime * result + Arrays.deepHashCode(geoPoints); + if (labelPosition != null) { + result = prime * result + labelPosition.hashCode(); + } + return result; + } + +}