diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/ShapefileReader.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/ShapefileReader.java index 01a8ad7b..af689bfd 100644 --- a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/ShapefileReader.java +++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/ShapefileReader.java @@ -21,7 +21,10 @@ import org.opengis.filter.Filter; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; +import org.opengis.referencing.operation.OperationNotFoundException; import org.opengis.referencing.operation.TransformException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Utility that reads {@link SourceFeature SourceFeatures} from the geometries contained in an ESRI shapefile. @@ -33,6 +36,7 @@ import org.opengis.referencing.operation.TransformException; * Shapefile Specification */ public class ShapefileReader extends SimpleReader { + private static final Logger LOGGER = LoggerFactory.getLogger(ShapefileReader.class); private final FeatureCollection inputSource; private final String[] attributeNames; @@ -53,7 +57,7 @@ public class ShapefileReader extends SimpleReader { CoordinateReferenceSystem src = sourceProjection == null ? source.getSchema().getCoordinateReferenceSystem() : CRS.decode(sourceProjection); CoordinateReferenceSystem dest = CRS.decode("EPSG:4326", true); - transformToLatLon = CRS.findMathTransform(src, dest); + transformToLatLon = findMathTransform(input, src, dest); if (transformToLatLon.isIdentity()) { transformToLatLon = null; } @@ -68,6 +72,19 @@ public class ShapefileReader extends SimpleReader { } } + private static MathTransform findMathTransform(Path input, CoordinateReferenceSystem src, + CoordinateReferenceSystem dest) throws FactoryException { + try { + return CRS.findMathTransform(src, dest); + } catch (OperationNotFoundException e) { + var result = CRS.findMathTransform(src, dest, true); + LOGGER.warn( + "Failed to parse projection from {} (\"{}\") using lenient mode instead which may result in data inconsistencies", + input.getFileName(), e.getMessage()); + return result; + } + } + /** * Renders map features for all elements from an ESRI Shapefile based on the mapping logic defined in {@code profile}. * Overrides the coordinate reference system defined in the shapefile. diff --git a/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/ShapefileReaderTest.java b/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/ShapefileReaderTest.java index 9fe2ab74..011997a4 100644 --- a/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/ShapefileReaderTest.java +++ b/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/ShapefileReaderTest.java @@ -4,7 +4,6 @@ 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.util.FileUtils; @@ -14,13 +13,24 @@ import java.nio.file.FileSystems; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; +import java.util.Map; +import org.geotools.data.DefaultTransaction; +import org.geotools.data.shapefile.ShapefileDataStore; +import org.geotools.data.shapefile.ShapefileDataStoreFactory; +import org.geotools.data.simple.SimpleFeatureStore; +import org.geotools.feature.DefaultFeatureCollection; +import org.geotools.feature.simple.SimpleFeatureBuilder; +import org.geotools.feature.simple.SimpleFeatureTypeBuilder; +import org.geotools.referencing.CRS; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Point; +import org.opengis.referencing.FactoryException; +import org.opengis.referencing.operation.TransformException; class ShapefileReaderTest { @TempDir @@ -45,6 +55,51 @@ class ShapefileReaderTest { testReadShapefile(dest.resolve("shapefile").resolve("stations.shp")); } + @Test + void testReadShapefileLeniently(@TempDir Path dir) throws IOException, TransformException, FactoryException { + var shpPath = dir.resolve("test.shp"); + var dataStoreFactory = new ShapefileDataStoreFactory(); + var newDataStore = + (ShapefileDataStore) dataStoreFactory.createNewDataStore(Map.of("url", shpPath.toUri().toURL())); + + var builder = new SimpleFeatureTypeBuilder(); + builder.setName("the_geom"); + builder.setCRS(CRS.parseWKT( + """ + PROJCS["SWEREF99_TM",GEOGCS["GCS_SWEREF99",DATUM["D_SWEREF99",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",15.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]] + """)); + + builder.add("the_geom", Point.class); + builder.add("value", Integer.class); + builder.setDefaultGeometry("the_geom"); + var type = builder.buildFeatureType(); + newDataStore.createSchema(type); + + try (var transaction = new DefaultTransaction("create")) { + var typeName = newDataStore.getTypeNames()[0]; + var featureSource = newDataStore.getFeatureSource(typeName); + var featureStore = (SimpleFeatureStore) featureSource; + featureStore.setTransaction(transaction); + var collection = new DefaultFeatureCollection(); + var featureBuilder = new SimpleFeatureBuilder(type); + featureBuilder.add(TestUtils.newPoint(1, 2)); + featureBuilder.add(3); + var feature = featureBuilder.buildFeature(null); + collection.add(feature); + featureStore.addFeatures(collection); + transaction.commit(); + } + + try (var reader = new ShapefileReader(null, "test", shpPath)) { + assertEquals(1, reader.getFeatureCount()); + List features = new ArrayList<>(); + reader.readFeatures(features::add); + assertEquals(10.5113, features.get(0).latLonGeometry().getCentroid().getX(), 1e-4); + assertEquals(0, features.get(0).latLonGeometry().getCentroid().getY(), 1e-4); + assertEquals(3, features.get(0).getTag("value")); + } + } + private static void testReadShapefile(Path path) { try (var reader = new ShapefileReader(null, "test", path)) { @@ -53,8 +108,7 @@ class ShapefileReaderTest { List points = new ArrayList<>(); List names = new ArrayList<>(); WorkerPipeline.start("test", Stats.inMemory()) - .readFromTiny("files", List.of(path)) - .addWorker("reader", 1, (IterableOnce p, Consumer next) -> reader.readFeatures(next)) + .fromGenerator("source", reader::readFeatures) .addBuffer("reader_queue", 100, 1) .sinkToConsumer("counter", 1, elem -> { assertTrue(elem.getTag("name") instanceof String);