diff --git a/settings.gradle b/settings.gradle index 3733ffea..673f8532 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,7 @@ include ':vtm-ios-example' include ':vtm-jeo' include ':vtm-json' include ':vtm-jts' +include ':vtm-mvt' include ':vtm-playground' include ':vtm-tests' include ':vtm-theme-comparator' diff --git a/vtm-android-example/AndroidManifest.xml b/vtm-android-example/AndroidManifest.xml index a95510e9..fcecbdc7 100644 --- a/vtm-android-example/AndroidManifest.xml +++ b/vtm-android-example/AndroidManifest.xml @@ -87,6 +87,9 @@ + diff --git a/vtm-android-example/build.gradle b/vtm-android-example/build.gradle index 6d0f2b91..4087b1e2 100644 --- a/vtm-android-example/build.gradle +++ b/vtm-android-example/build.gradle @@ -10,6 +10,7 @@ dependencies { implementation project(':vtm-jeo') implementation project(':vtm-json') implementation project(':vtm-jts') + implementation project(':vtm-mvt') implementation project(':vtm-themes') implementation "org.slf4j:slf4j-android:$slf4jVersion" @@ -35,7 +36,9 @@ android { defaultConfig { versionCode versionCode() versionName versionName() - minSdkVersion androidMinSdk() + // FIXME Minimum API Level by mapbox-vector-tile + //minSdkVersion androidMinSdk() + minSdkVersion 26 targetSdkVersion androidTargetSdk() } @@ -64,6 +67,14 @@ android { exclude 'META-INF/LICENSE' exclude 'META-INF/NOTICE' } + + buildTypes { + all { + minifyEnabled true + useProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } } task run(dependsOn: 'installDebug') { diff --git a/vtm-android-example/proguard-rules.pro b/vtm-android-example/proguard-rules.pro new file mode 100644 index 00000000..584189fa --- /dev/null +++ b/vtm-android-example/proguard-rules.pro @@ -0,0 +1,10 @@ +-keep class com.** { *; } +-dontwarn com.** +-keep class jsqlite.** { *; } +-dontwarn jsqlite.** +-keep class okhttp3.** { *; } +-dontwarn okhttp3.** +-keep class okio.** { *; } +-dontwarn okio.** +-keep class org.** { *; } +-dontwarn org.** diff --git a/vtm-android-example/src/org/oscim/android/test/OpenMapTilesMvtMapActivity.java b/vtm-android-example/src/org/oscim/android/test/OpenMapTilesMvtMapActivity.java new file mode 100644 index 00000000..7d1e02b8 --- /dev/null +++ b/vtm-android-example/src/org/oscim/android/test/OpenMapTilesMvtMapActivity.java @@ -0,0 +1,68 @@ +/* + * 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 + * 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.os.Bundle; + +import org.oscim.android.cache.TileCache; +import org.oscim.layers.TileGridLayer; +import org.oscim.layers.tile.buildings.BuildingLayer; +import org.oscim.layers.tile.vector.VectorTileLayer; +import org.oscim.layers.tile.vector.labeling.LabelLayer; +import org.oscim.theme.VtmThemes; +import org.oscim.tiling.source.OkHttpEngine; +import org.oscim.tiling.source.UrlTileSource; +import org.oscim.tiling.source.mvt.OpenMapTilesMvtTileSource; + +public class OpenMapTilesMvtMapActivity extends MapActivity { + + private static final boolean USE_CACHE = false; + + private TileCache mCache; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + UrlTileSource tileSource = OpenMapTilesMvtTileSource.builder() + .apiKey("xxxxxxx") // Put a proper API key + .httpFactory(new OkHttpEngine.OkHttpFactory()) + //.locale("en") + .build(); + + if (USE_CACHE) { + // Cache the tiles into a local SQLite database + mCache = new TileCache(this, null, "tile.db"); + mCache.setCacheSize(512 * (1 << 10)); + tileSource.setCache(mCache); + } + + VectorTileLayer l = mMap.setBaseMap(tileSource); + mMap.setTheme(VtmThemes.OPENMAPTILES); + + mMap.layers().add(new BuildingLayer(mMap, l)); + mMap.layers().add(new LabelLayer(mMap, l)); + + mMap.layers().add(new TileGridLayer(mMap, getResources().getDisplayMetrics().density)); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (mCache != null) + mCache.dispose(); + } +} 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 30ed6350..c9cc9284 100644 --- a/vtm-android-example/src/org/oscim/android/test/Samples.java +++ b/vtm-android-example/src/org/oscim/android/test/Samples.java @@ -83,6 +83,7 @@ public class Samples extends Activity { linearLayout.addView(createButton(MapsforgeMapActivity.class)); /*linearLayout.addView(createButton(MapzenMvtMapActivity.class)); linearLayout.addView(createButton(MapzenGeojsonMapActivity.class));*/ + linearLayout.addView(createButton(OpenMapTilesMvtMapActivity.class)); linearLayout.addView(createButton(OpenMapTilesGeojsonMapActivity.class)); linearLayout.addView(createButton(GdxMapActivity.class)); diff --git a/vtm-mvt/build.gradle b/vtm-mvt/build.gradle new file mode 100644 index 00000000..5cfeda96 --- /dev/null +++ b/vtm-mvt/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'java-library' +apply plugin: 'maven' + +dependencies { + api project(':vtm') + api 'com.wdtinc:mapbox-vector-tile:2.0.0' +} + +sourceSets { + main.java.srcDirs = ['src'] +} + +if (project.hasProperty("SONATYPE_USERNAME")) { + afterEvaluate { + project.apply from: "${rootProject.projectDir}/deploy.gradle" + } +} diff --git a/vtm/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java b/vtm-mvt/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java similarity index 96% rename from vtm/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java rename to vtm-mvt/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java index b9f4d7e4..0ae1dc59 100644 --- a/vtm/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java +++ b/vtm-mvt/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java @@ -66,6 +66,6 @@ public class MapzenMvtTileSource extends UrlTileSource { @Override public ITileDataSource getDataSource() { - return new UrlTileDataSource(this, new TileDecoder(locale), getHttpEngine()); + return new UrlTileDataSource(this, new MvtTileDecoder(locale), getHttpEngine()); } } diff --git a/vtm-mvt/src/org/oscim/tiling/source/mvt/MvtTileDecoder.java b/vtm-mvt/src/org/oscim/tiling/source/mvt/MvtTileDecoder.java new file mode 100644 index 00000000..83fb6ee2 --- /dev/null +++ b/vtm-mvt/src/org/oscim/tiling/source/mvt/MvtTileDecoder.java @@ -0,0 +1,181 @@ +/* + * Copyright 2014 Hannes Janetzek + * Copyright 2017 devemux86 + * Copyright 2018 boldtrn + * + * This file is part of the OpenScienceMap project (http://www.opensciencemap.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 . + */ +package org.oscim.tiling.source.mvt; + +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.LineString; +import com.vividsolutions.jts.geom.MultiLineString; +import com.vividsolutions.jts.geom.MultiPoint; +import com.vividsolutions.jts.geom.MultiPolygon; +import com.vividsolutions.jts.geom.Point; +import com.vividsolutions.jts.geom.Polygon; +import com.wdtinc.mapbox_vector_tile.adapt.jts.MvtReader; +import com.wdtinc.mapbox_vector_tile.adapt.jts.TagKeyValueMapConverter; +import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsLayer; +import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsMvt; + +import org.oscim.core.MapElement; +import org.oscim.core.Tag; +import org.oscim.core.Tile; +import org.oscim.tiling.ITileDataSink; +import org.oscim.tiling.source.ITileDecoder; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +public class MvtTileDecoder implements ITileDecoder { + private final String mLocale; + + private final static float REF_TILE_SIZE = 4096.0f; + private float mScale; + + private final GeometryFactory mGeomFactory; + private final MapElement mMapElement; + private ITileDataSink mTileDataSink; + + public MvtTileDecoder() { + this(""); + } + + public MvtTileDecoder(String locale) { + mLocale = locale; + mGeomFactory = new GeometryFactory(); + mMapElement = new MapElement(); + mMapElement.layer = 5; + } + + @Override + public boolean decode(Tile tile, ITileDataSink sink, InputStream is) + throws IOException { + + mTileDataSink = sink; + mScale = REF_TILE_SIZE / Tile.SIZE; + + JtsMvt jtsMvt = MvtReader.loadMvt( + is, + mGeomFactory, + new TagKeyValueMapConverter(), + MvtReader.RING_CLASSIFIER_V1); + + + for (JtsLayer layer : jtsMvt.getLayers()) { + for (Geometry geometry : layer.getGeometries()) { + parseGeometry(layer.getName(), geometry, (Map) geometry.getUserData()); + } + } + + return true; + } + + private void parseGeometry(String layerName, Geometry geometry, Map tags) { + mMapElement.clear(); + mMapElement.tags.clear(); + + parseTags(tags, layerName); + if (mMapElement.tags.size() == 0) { + return; + } + + boolean err = false; + if (geometry instanceof Point) { + mMapElement.startPoints(); + processCoordinateArray(geometry.getCoordinates(), false); + } else if (geometry instanceof MultiPoint) { + MultiPoint multiPoint = (MultiPoint) geometry; + for (int i = 0; i < multiPoint.getNumGeometries(); i++) { + mMapElement.startPoints(); + processCoordinateArray(multiPoint.getGeometryN(i).getCoordinates(), false); + } + } else if (geometry instanceof LineString) { + processLineString((LineString) geometry); + } else if (geometry instanceof MultiLineString) { + MultiLineString multiLineString = (MultiLineString) geometry; + for (int i = 0; i < multiLineString.getNumGeometries(); i++) { + processLineString((LineString) multiLineString.getGeometryN(i)); + } + } else if (geometry instanceof Polygon) { + Polygon polygon = (Polygon) geometry; + processPolygon(polygon); + } else if (geometry instanceof MultiPolygon) { + MultiPolygon multiPolygon = (MultiPolygon) geometry; + for (int i = 0; i < multiPolygon.getNumGeometries(); i++) { + processPolygon((Polygon) multiPolygon.getGeometryN(i)); + } + } else { + err = true; + } + + if (!err) { + mTileDataSink.process(mMapElement); + } + } + + private void processLineString(LineString lineString) { + mMapElement.startLine(); + processCoordinateArray(lineString.getCoordinates(), false); + } + + private void processPolygon(Polygon polygon) { + mMapElement.startPolygon(); + processCoordinateArray(polygon.getExteriorRing().getCoordinates(), true); + for (int i = 0; i < polygon.getNumInteriorRing(); i++) { + mMapElement.startHole(); + processCoordinateArray(polygon.getInteriorRingN(i).getCoordinates(), true); + } + } + + private void processCoordinateArray(Coordinate[] coordinates, boolean removeLast) { + int length = removeLast ? coordinates.length - 1 : coordinates.length; + for (int i = 0; i < length; i++) { + mMapElement.addPoint((float) coordinates[i].x / mScale, (float) coordinates[i].y / mScale); + } + } + + private void parseTags(Map map, String layerName) { + mMapElement.tags.add(new Tag("layer", layerName)); + boolean hasName = false; + String fallbackName = null; + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + String val = (value instanceof String) ? (String) value : String.valueOf(value); + if (key.startsWith(Tag.KEY_NAME)) { + int len = key.length(); + if (len == 4) { + fallbackName = val; + continue; + } + if (len < 7) + continue; + if (mLocale.equals(key.substring(5))) { + hasName = true; + mMapElement.tags.add(new Tag(Tag.KEY_NAME, val, false)); + } + } else { + mMapElement.tags.add(new Tag(key, val)); + } + } + if (!hasName && fallbackName != null) + mMapElement.tags.add(new Tag(Tag.KEY_NAME, fallbackName, false)); + } +} + diff --git a/vtm-mvt/src/org/oscim/tiling/source/mvt/OpenMapTilesMvtTileSource.java b/vtm-mvt/src/org/oscim/tiling/source/mvt/OpenMapTilesMvtTileSource.java new file mode 100644 index 00000000..f61d5320 --- /dev/null +++ b/vtm-mvt/src/org/oscim/tiling/source/mvt/OpenMapTilesMvtTileSource.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013 Hannes Janetzek + * Copyright 2016-2017 devemux86 + * Copyright 2018 boldtrn + * + * This file is part of the OpenScienceMap project (http://www.opensciencemap.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 . + */ +package org.oscim.tiling.source.mvt; + +import org.oscim.tiling.ITileDataSource; +import org.oscim.tiling.source.UrlTileDataSource; +import org.oscim.tiling.source.UrlTileSource; + +public class OpenMapTilesMvtTileSource extends UrlTileSource { + + private final static String DEFAULT_URL = "https://free.tilehosting.com/data/v3"; + private final static String DEFAULT_PATH = "/{Z}/{X}/{Y}.pbf.pict"; + + public static class Builder> extends UrlTileSource.Builder { + private String locale = ""; + + public Builder() { + super(DEFAULT_URL, DEFAULT_PATH, 1, 14); + } + + public T locale(String locale) { + this.locale = locale; + return self(); + } + + public OpenMapTilesMvtTileSource build() { + return new OpenMapTilesMvtTileSource(this); + } + } + + @SuppressWarnings("rawtypes") + public static Builder builder() { + return new Builder(); + } + + private final String locale; + + public OpenMapTilesMvtTileSource(Builder builder) { + super(builder); + this.locale = builder.locale; + } + + public OpenMapTilesMvtTileSource() { + this(builder()); + } + + public OpenMapTilesMvtTileSource(String urlString) { + this(builder().url(urlString)); + } + + @Override + public ITileDataSource getDataSource() { + return new UrlTileDataSource(this, new MvtTileDecoder(locale), getHttpEngine()); + } +} diff --git a/vtm-playground/build.gradle b/vtm-playground/build.gradle index 12de8206..34d42e2d 100644 --- a/vtm-playground/build.gradle +++ b/vtm-playground/build.gradle @@ -9,6 +9,7 @@ dependencies { implementation project(':vtm-jeo') implementation project(':vtm-json') implementation project(':vtm-jts') + implementation project(':vtm-mvt') implementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" implementation "org.slf4j:slf4j-jdk14:$slf4jVersion" } diff --git a/vtm-playground/src/org/oscim/test/OpenMapTilesMvtTest.java b/vtm-playground/src/org/oscim/test/OpenMapTilesMvtTest.java new file mode 100644 index 00000000..fbc462fe --- /dev/null +++ b/vtm-playground/src/org/oscim/test/OpenMapTilesMvtTest.java @@ -0,0 +1,65 @@ +/* + * 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 + * 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.test; + +import org.oscim.gdx.GdxMapApp; +import org.oscim.layers.tile.buildings.BuildingLayer; +import org.oscim.layers.tile.vector.VectorTileLayer; +import org.oscim.layers.tile.vector.labeling.LabelLayer; +import org.oscim.theme.VtmThemes; +import org.oscim.tiling.source.OkHttpEngine; +import org.oscim.tiling.source.UrlTileSource; +import org.oscim.tiling.source.mvt.OpenMapTilesMvtTileSource; + +import java.io.File; +import java.util.UUID; + +import okhttp3.Cache; +import okhttp3.OkHttpClient; + +public class OpenMapTilesMvtTest extends GdxMapApp { + + private static final boolean USE_CACHE = false; + + @Override + public void createLayers() { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + if (USE_CACHE) { + // Cache the tiles into file system + File cacheDirectory = new File(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString()); + int cacheSize = 10 * 1024 * 1024; // 10 MB + Cache cache = new Cache(cacheDirectory, cacheSize); + builder.cache(cache); + } + OkHttpEngine.OkHttpFactory factory = new OkHttpEngine.OkHttpFactory(builder); + + UrlTileSource tileSource = OpenMapTilesMvtTileSource.builder() + .apiKey("xxxxxxx") // Put a proper API key + .httpFactory(factory) + //.locale("en") + .build(); + + VectorTileLayer l = mMap.setBaseMap(tileSource); + mMap.setTheme(VtmThemes.OPENMAPTILES); + + mMap.layers().add(new BuildingLayer(mMap, l)); + mMap.layers().add(new LabelLayer(mMap, l)); + } + + public static void main(String[] args) { + GdxMapApp.init(); + GdxMapApp.run(new OpenMapTilesMvtTest()); + } +} diff --git a/vtm-tests/build.gradle b/vtm-tests/build.gradle index 5e466bae..cee19e71 100644 --- a/vtm-tests/build.gradle +++ b/vtm-tests/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'java' dependencies { implementation project(':vtm-http') + implementation project(':vtm-mvt') testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.0' testImplementation 'junit:junit:4.12' testImplementation 'org.easytesting:fest-assert-core:2.0M10' diff --git a/vtm-tests/resources/mvt-test.pbf b/vtm-tests/resources/mvt-test.pbf new file mode 100644 index 00000000..c5752ed3 Binary files /dev/null and b/vtm-tests/resources/mvt-test.pbf differ diff --git a/vtm-tests/test/org/oscim/tiling/source/mvt/MvtTileDecoderTest.java b/vtm-tests/test/org/oscim/tiling/source/mvt/MvtTileDecoderTest.java new file mode 100644 index 00000000..35146b8b --- /dev/null +++ b/vtm-tests/test/org/oscim/tiling/source/mvt/MvtTileDecoderTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018 boldtrn + * Copyright 2018 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.mvt; + +import org.junit.Test; +import org.oscim.backend.canvas.Bitmap; +import org.oscim.core.MapElement; +import org.oscim.core.Tile; +import org.oscim.tiling.ITileDataSink; +import org.oscim.tiling.QueryResult; + +import static org.junit.Assert.assertEquals; + +public class MvtTileDecoderTest { + + @Test + public void tileDecodingTest() throws Exception { + MvtTileDecoder decoder = new MvtTileDecoder(); + Tile tile = new Tile(0, 0, (byte) 0); + ITileDataSink sink = new ITileDataSink() { + @Override + public void process(MapElement element) { + if (element.tags.contains("class", "ocean")) + assertEquals(4, element.getNumPoints()); + if (element.tags.contains("layer", "water_name")) + assertEquals("Irish Sea", element.tags.getValue("name")); + } + + @Override + public void setTileImage(Bitmap bitmap) { + } + + @Override + public void completed(QueryResult result) { + } + }; + decoder.decode(tile, sink, getClass().getResourceAsStream("/mvt-test.pbf")); + } +}