diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java
index 2551c1a6..0ba01557 100644
--- a/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/Planetiler.java
@@ -7,6 +7,7 @@ import com.onthegomap.planetiler.config.Arguments;
import com.onthegomap.planetiler.config.MbtilesMetadata;
import com.onthegomap.planetiler.config.PlanetilerConfig;
import com.onthegomap.planetiler.mbtiles.MbtilesWriter;
+import com.onthegomap.planetiler.reader.GeoPackageReader;
import com.onthegomap.planetiler.reader.NaturalEarthReader;
import com.onthegomap.planetiler.reader.ShapefileReader;
import com.onthegomap.planetiler.reader.osm.OsmInputFile;
@@ -337,6 +338,31 @@ public class Planetiler {
}));
}
+ /**
+ * Adds a new OGC GeoPackage source that will be processed when {@link #run()} is called.
+ *
+ * If the file does not exist and {@code download=true} argument is set, then the file will first be downloaded from
+ * {@code defaultUrl}.
+ *
+ * To override the location of the {@code geopackage} file, set {@code name_path=newpath.gpkg} in the arguments and to
+ * override the download URL set {@code name_url=http://url/of/file.gpkg}.
+ *
+ * @param name string to use in stats and logs to identify this stage
+ * @param defaultPath path to the input file to use if {@code name_path} key is not set through arguments
+ * @param defaultUrl remote URL that the file to download if {@code download=true} argument is set and {@code
+ * name_url} argument is not set
+ * @return this runner instance for chaining
+ * @see GeoPackageReader
+ * @see Downloader
+ */
+ public Planetiler addGeoPackageSource(String name, Path defaultPath, String defaultUrl) {
+ Path path = getPath(name, "geopackage", defaultPath, defaultUrl);
+ return addStage(name, "Process features in " + path,
+ ifSourceUsed(name,
+ () -> GeoPackageReader.process(name, List.of(path), featureGroup, config, profile, stats)));
+ }
+
+
/**
* Adds a new Natural Earth sqlite file source that will be processed when {@link #run()} is called.
*
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/GeoPackageReader.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/GeoPackageReader.java
new file mode 100644
index 00000000..15a42b65
--- /dev/null
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/GeoPackageReader.java
@@ -0,0 +1,109 @@
+package com.onthegomap.planetiler.reader;
+
+import com.onthegomap.planetiler.Profile;
+import com.onthegomap.planetiler.collection.FeatureGroup;
+import com.onthegomap.planetiler.config.PlanetilerConfig;
+import com.onthegomap.planetiler.stats.Stats;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.function.Consumer;
+import mil.nga.geopackage.GeoPackage;
+import mil.nga.geopackage.GeoPackageManager;
+import mil.nga.geopackage.features.user.FeatureColumns;
+import mil.nga.geopackage.features.user.FeatureDao;
+import mil.nga.geopackage.geom.GeoPackageGeometryData;
+import org.geotools.geometry.jts.JTS;
+import org.geotools.geometry.jts.WKBReader;
+import org.geotools.referencing.CRS;
+import org.locationtech.jts.geom.Geometry;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.MathTransform;
+
+/**
+ * Utility that reads {@link SourceFeature SourceFeatures} from the vector geometries contained in a GeoPackage file.
+ */
+public class GeoPackageReader extends SimpleReader {
+
+ private final GeoPackage geoPackage;
+
+ GeoPackageReader(String sourceName, Path input) {
+ super(sourceName);
+
+ geoPackage = GeoPackageManager.open(false, input.toFile());
+ }
+
+ /**
+ * Renders map features for all elements from an OGC GeoPackage based on the mapping logic defined in {@code
+ * profile}.
+ *
+ * @param sourceName string ID for this reader to use in logs and stats
+ * @param sourcePaths paths to the {@code .gpkg} files on disk
+ * @param writer consumer for rendered features
+ * @param config user-defined parameters controlling number of threads and log interval
+ * @param profile logic that defines what map features to emit for each source feature
+ * @param stats to keep track of counters and timings
+ * @throws IllegalArgumentException if a problem occurs reading the input file
+ */
+ public static void process(String sourceName, List sourcePaths, FeatureGroup writer, PlanetilerConfig config,
+ Profile profile, Stats stats) {
+ SourceFeatureProcessor.processFiles(
+ sourceName,
+ sourcePaths,
+ path -> new GeoPackageReader(sourceName, path),
+ writer, config, profile, stats
+ );
+ }
+
+ @Override
+ public long getFeatureCount() {
+ long numFeatures = 0;
+
+ for (String name : geoPackage.getFeatureTables()) {
+ FeatureDao features = geoPackage.getFeatureDao(name);
+ numFeatures += features.count();
+ }
+ return numFeatures;
+ }
+
+ @Override
+ public void readFeatures(Consumer next) throws Exception {
+ CoordinateReferenceSystem latLonCRS = CRS.decode("EPSG:4326");
+ long id = 0;
+
+ for (var featureName : geoPackage.getFeatureTables()) {
+ FeatureDao features = geoPackage.getFeatureDao(featureName);
+
+ MathTransform transform = CRS.findMathTransform(
+ CRS.decode("EPSG:" + features.getSrsId()),
+ latLonCRS);
+
+ for (var feature : features.queryForAll()) {
+ GeoPackageGeometryData geometryData = feature.getGeometry();
+ if (geometryData == null) {
+ continue;
+ }
+
+ Geometry featureGeom = (new WKBReader()).read(geometryData.getWkb());
+ Geometry latLonGeom = (transform.isIdentity()) ? featureGeom : JTS.transform(featureGeom, transform);
+
+ FeatureColumns columns = feature.getColumns();
+ SimpleFeature geom = SimpleFeature.create(latLonGeom, new HashMap<>(columns.columnCount()),
+ sourceName, featureName, ++id);
+
+ for (int i = 0; i < columns.columnCount(); ++i) {
+ if (i != columns.getGeometryIndex()) {
+ geom.setTag(columns.getColumnName(i), feature.getValue(i));
+ }
+ }
+
+ next.accept(geom);
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ geoPackage.close();
+ }
+}
diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java
index d9ff4eb9..1681c22f 100644
--- a/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/PlanetilerTests.java
@@ -1667,6 +1667,7 @@ class PlanetilerTests {
.addOsmSource("osm", tempOsm)
.addNaturalEarthSource("ne", TestUtils.pathToResource("natural_earth_vector.sqlite"))
.addShapefileSource("shapefile", TestUtils.pathToResource("shapefile.zip"))
+ .addGeoPackageSource("geopackage", TestUtils.pathToResource("geopackage.gpkg"), null)
.setOutput("mbtiles", mbtiles)
.run();
@@ -1743,12 +1744,52 @@ class PlanetilerTests {
}
}
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "",
+ "--write-threads=2 --process-threads=2 --feature-read-threads=2 --threads=4",
+ })
+ void testPlanetilerRunnerGeoPackage(String args) throws Exception {
+ Path mbtiles = tempDir.resolve("output.mbtiles");
+
+ Planetiler.create(Arguments.fromArgs((args + " --tmpdir=" + tempDir.resolve("data")).split("\\s+")))
+ .setProfile(new Profile.NullProfile() {
+ @Override
+ public void processFeature(SourceFeature source, FeatureCollector features) {
+ features.point("stations")
+ .setZoomRange(0, 14)
+ .setAttr("name", source.getString("name"));
+ }
+ })
+ .addGeoPackageSource("geopackage", TestUtils.pathToResource("geopackage.gpkg"), null)
+ .setOutput("mbtiles", mbtiles)
+ .run();
+
+ try (Mbtiles db = Mbtiles.newReadOnlyDatabase(mbtiles)) {
+ Set uniqueNames = new HashSet<>();
+ long featureCount = 0;
+ var tileMap = TestUtils.getTileMap(db);
+ for (var tile : tileMap.values()) {
+ for (var feature : tile) {
+ feature.geometry().validate();
+ featureCount++;
+ uniqueNames.add((String) feature.attrs().get("name"));
+ }
+ }
+
+ assertTrue(featureCount > 0);
+ assertEquals(86, uniqueNames.size());
+ assertTrue(uniqueNames.contains("Van Dörn Street"));
+ }
+ }
+
private void runWithProfile(Path tempDir, Profile profile, boolean force) throws Exception {
Planetiler.create(Arguments.of("tmpdir", tempDir, "force", Boolean.toString(force)))
.setProfile(profile)
.addOsmSource("osm", TestUtils.pathToResource("monaco-latest.osm.pbf"))
.addNaturalEarthSource("ne", TestUtils.pathToResource("natural_earth_vector.sqlite"))
.addShapefileSource("shapefile", TestUtils.pathToResource("shapefile.zip"))
+ .addGeoPackageSource("geopackage", TestUtils.pathToResource("geopackage.gpkg"), null)
.setOutput("mbtiles", tempDir.resolve("output.mbtiles"))
.run();
}
diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/GeoPackageReaderTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/GeoPackageReaderTest.java
new file mode 100644
index 00000000..e8594137
--- /dev/null
+++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/GeoPackageReaderTest.java
@@ -0,0 +1,53 @@
+package com.onthegomap.planetiler.reader;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.onthegomap.planetiler.TestUtils;
+import com.onthegomap.planetiler.collection.IterableOnce;
+import com.onthegomap.planetiler.geo.GeoUtils;
+import com.onthegomap.planetiler.stats.Stats;
+import com.onthegomap.planetiler.worker.WorkerPipeline;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.locationtech.jts.geom.Geometry;
+
+class GeoPackageReaderTest {
+
+ @Test
+ @Timeout(30)
+ void testReadGeoPackage() {
+ Path path = TestUtils.pathToResource("geopackage.gpkg");
+
+ try (
+ var reader = new GeoPackageReader("test", path)
+ ) {
+ for (int i = 1; i <= 2; i++) {
+ assertEquals(86, reader.getFeatureCount());
+ List points = new ArrayList<>();
+ List names = new ArrayList<>();
+ WorkerPipeline.start("test", Stats.inMemory())
+ .readFromTiny("files", List.of(Path.of("dummy-path")))
+ .addWorker("geopackage", 1, (IterableOnce p, Consumer next) -> reader.readFeatures(next))
+ .addBuffer("reader_queue", 100, 1)
+ .sinkToConsumer("counter", 1, elem -> {
+ assertTrue(elem.getTag("name") instanceof String);
+ assertEquals("test", elem.getSource());
+ assertEquals("stations", elem.getSourceLayer());
+ points.add(elem.latLonGeometry());
+ names.add(elem.getTag("name").toString());
+ }).await();
+ assertEquals(86, points.size());
+ assertTrue(names.contains("Van Dörn Street"));
+ var gc = GeoUtils.JTS_FACTORY.createGeometryCollection(points.toArray(new Geometry[0]));
+ var centroid = gc.getCentroid();
+ assertEquals(-77.0297995, centroid.getX(), 5, "iter " + i);
+ assertEquals(38.9119684, centroid.getY(), 5, "iter " + i);
+ }
+ }
+ }
+}
diff --git a/planetiler-core/src/test/resources/geopackage.gpkg b/planetiler-core/src/test/resources/geopackage.gpkg
new file mode 100644
index 0000000000000000000000000000000000000000..04cc66ee356ce6a53c3202dbd4b44955a3de46a5
GIT binary patch
literal 114688
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 planetiler.addOsmSource(source.id(), localPath, source.url());
case SHAPEFILE -> planetiler.addShapefileSource(source.id(), localPath, source.url());
+ case GEOPACKAGE -> planetiler.addGeoPackageSource(source.id(), localPath, source.url());
default -> throw new IllegalArgumentException("Unhandled source type for " + source.id() + ": " + sourceType);
}
}
diff --git a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/DataSourceType.java b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/DataSourceType.java
index b128f136..39e12c3f 100644
--- a/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/DataSourceType.java
+++ b/planetiler-custommap/src/main/java/com/onthegomap/planetiler/custommap/configschema/DataSourceType.java
@@ -6,5 +6,7 @@ public enum DataSourceType {
@JsonProperty("osm")
OSM,
@JsonProperty("shapefile")
- SHAPEFILE
+ SHAPEFILE,
+ @JsonProperty("geopackage")
+ GEOPACKAGE
}
diff --git a/planetiler-custommap/src/test/resources/validSchema/road_motorway.yml b/planetiler-custommap/src/test/resources/validSchema/road_motorway.yml
index eb928576..558e65d0 100644
--- a/planetiler-custommap/src/test/resources/validSchema/road_motorway.yml
+++ b/planetiler-custommap/src/test/resources/validSchema/road_motorway.yml
@@ -9,6 +9,9 @@ sources:
osm:
type: osm
url: geofabrik:rhode-island
+ gpkg:
+ type: geopackage
+ url: https://example.com/geopackage.gpkg
tag_mappings:
bridge: boolean # input=bridge, output=bridge, type=boolean
layer: long