diff --git a/src/main/java/ch/geowerkstatt/lk2dxf/GeometryObject.java b/src/main/java/ch/geowerkstatt/lk2dxf/GeometryObject.java new file mode 100644 index 0000000..6804717 --- /dev/null +++ b/src/main/java/ch/geowerkstatt/lk2dxf/GeometryObject.java @@ -0,0 +1,50 @@ +package ch.geowerkstatt.lk2dxf; + +import ch.interlis.iom.IomObject; +import ch.interlis.iom_j.itf.impl.jtsext.geom.JtsextGeometryFactory; +import ch.interlis.iox.IoxException; +import ch.interlis.iox_j.jts.Iox2jtsext; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.GeometryFactory; + +public record GeometryObject(Geometry geometry, IomObject iomObject) { + private static final String BASKET_NAME = "SIA405_LKMap_2015_LV95.SIA405_LKMap"; + private static final GeometryFactory GEOMETRY_FACTORY = new JtsextGeometryFactory(); + + /** + * Creates a new geometry object from the given {@link IomObject}. + * @param iomObject The {@link IomObject} to create the geometry object from. + * @return A geometry object containing the {@link IomObject} and its extracted {@link Geometry}. + * @throws IllegalArgumentException If the object tag is not supported. + * @throws RuntimeException If an error occurs while extracting the geometry. + */ + public static GeometryObject create(IomObject iomObject) { + try { + Geometry geometry = switch (iomObject.getobjecttag()) { + case BASKET_NAME + ".LKPunkt" -> readPoint(iomObject, "SymbolPos"); + case BASKET_NAME + ".LKLinie" -> readLine(iomObject, "Linie"); + case BASKET_NAME + ".LKFlaeche" -> readSurface(iomObject, "Flaeche"); + case BASKET_NAME + ".LKObjekt_Text" -> readPoint(iomObject, "TextPos"); + default -> throw new IllegalArgumentException("Unsupported object tag: " + iomObject.getobjecttag()); + }; + return new GeometryObject(geometry, iomObject); + } catch (IoxException e) { + throw new RuntimeException("Error creating geometry for object with id \"" + iomObject.getobjectoid() + "\".", e); + } + } + + private static Geometry readPoint(IomObject iomObject, String attributeName) throws IoxException { + IomObject position = iomObject.getattrobj(attributeName, 0); + return GEOMETRY_FACTORY.createPoint(Iox2jtsext.coord2JTS(position)); + } + + private static Geometry readLine(IomObject iomObject, String attributeName) throws IoxException { + IomObject line = iomObject.getattrobj(attributeName, 0); + return Iox2jtsext.polyline2JTS(line, false, 0.0); + } + + private static Geometry readSurface(IomObject iomObject, String attributeName) throws IoxException { + IomObject surface = iomObject.getattrobj(attributeName, 0); + return Iox2jtsext.surface2JTS(surface, 0.0); + } +} diff --git a/src/main/java/ch/geowerkstatt/lk2dxf/LK2DxfOptions.java b/src/main/java/ch/geowerkstatt/lk2dxf/LK2DxfOptions.java index 646ea9d..029156e 100644 --- a/src/main/java/ch/geowerkstatt/lk2dxf/LK2DxfOptions.java +++ b/src/main/java/ch/geowerkstatt/lk2dxf/LK2DxfOptions.java @@ -1,5 +1,8 @@ package ch.geowerkstatt.lk2dxf; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.io.WKTReader; + import java.util.List; import java.util.Optional; @@ -9,4 +12,19 @@ public record LK2DxfOptions( Optional perimeterWkt, Optional logfile, boolean trace) { + + /** + * Parses the perimeter WKT string to a {@link Geometry}. + * @return The parsed perimeter geometry, or an empty optional if the wkt was empty. + * @throws IllegalStateException If the wkt could not be parsed. + */ + public Optional parsePerimeter() { + return perimeterWkt.map(wkt -> { + try { + return new WKTReader().read(wkt); + } catch (com.vividsolutions.jts.io.ParseException e) { + throw new IllegalStateException("Error parsing perimeter WKT.", e); + } + }); + } } diff --git a/src/main/java/ch/geowerkstatt/lk2dxf/LKMapXtfReader.java b/src/main/java/ch/geowerkstatt/lk2dxf/LKMapXtfReader.java index c0aecda..9f51d32 100644 --- a/src/main/java/ch/geowerkstatt/lk2dxf/LKMapXtfReader.java +++ b/src/main/java/ch/geowerkstatt/lk2dxf/LKMapXtfReader.java @@ -14,7 +14,10 @@ import ch.interlis.iox_j.utility.ReaderFactory; import java.io.File; +import java.util.Spliterator; import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; /** * A reader for LKMap INTERLIS transfer files. @@ -24,6 +27,7 @@ public final class LKMapXtfReader implements AutoCloseable { private static final ReaderFactory READER_FACTORY = new ReaderFactory(); private final IoxReader reader; + private LKMapXtfReaderState state = null; /** * Creates a new reader for LKMap INTERLIS transfer files. @@ -37,43 +41,106 @@ public LKMapXtfReader(File xtfFile) throws IoxException { } /** - * Reads the objects streamed by the reader and passes them to the consumer. - * @param consumer A consumer to process the objects. - * @throws IoxException If an error occurs while reading the objects. - * @throws IllegalStateException If the transfer file is not in the expected format. + * Reads the objects as a sequential stream. + * Advancing the stream may throw an exception when reading invalid data. + * @return A stream of objects contained in the xtf file. + * @throws IllegalStateException If this method is called more than once. */ - public void readObjects(Consumer consumer) throws IoxException { - IoxEvent event = reader.read(); - if (!(event instanceof StartTransferEvent)) { - throw new IllegalStateException("Expected start transfer event, got: " + event); + public Stream readObjects() { + if (state != null) { + throw new IllegalStateException("readObjects() can only be called once"); } + state = LKMapXtfReaderState.INITIALIZED; - event = reader.read(); - while (!(event instanceof EndTransferEvent)) { - if (event instanceof StartBasketEvent startBasketEvent) { - if (!BASKET_NAME.equals(startBasketEvent.getType())) { - throw new IllegalStateException("Invalid basket type: " + startBasketEvent.getType()); + return StreamSupport.stream(new XtfReaderSpliterator(), false); + } + + @Override + public void close() throws Exception { + reader.close(); + } + + private enum LKMapXtfReaderState { + INITIALIZED, + TRANSFER, + BASKET, + COMPLETED, + } + + /** + * A sequential spliterator for reading objects from the surrounding {@link LKMapXtfReader}. + * Advancing the spliterator will read from the xtf reader and may throw an exception when reading invalid data. + */ + private class XtfReaderSpliterator implements Spliterator { + @Override + public boolean tryAdvance(Consumer action) { + try { + IoxEvent event = reader.read(); + while (event != null) { + switch (event) { + case StartTransferEvent ignored -> { + if (state != LKMapXtfReaderState.INITIALIZED) { + throw new IllegalStateException("Unexpected start transfer event in state: " + state); + } + state = LKMapXtfReaderState.TRANSFER; + System.out.println("Start transfer"); + } + case StartBasketEvent startBasketEvent -> { + if (state != LKMapXtfReaderState.TRANSFER) { + throw new IllegalStateException("Unexpected start basket event in state: " + state); + } + if (!BASKET_NAME.equals(startBasketEvent.getType())) { + throw new IllegalStateException("Invalid basket type: " + startBasketEvent.getType()); + } + state = LKMapXtfReaderState.BASKET; + System.out.println("Start basket \"" + startBasketEvent.getBid() + "\""); + } + case ObjectEvent objectEvent -> { + if (state != LKMapXtfReaderState.BASKET) { + throw new IllegalStateException("Unexpected object event in state: " + state); + } + action.accept(objectEvent.getIomObject()); + return true; + } + case EndBasketEvent ignored -> { + if (state != LKMapXtfReaderState.BASKET) { + throw new IllegalStateException("Unexpected end basket event in state: " + state); + } + state = LKMapXtfReaderState.TRANSFER; + System.out.println("End basket"); + } + case EndTransferEvent ignored -> { + if (state != LKMapXtfReaderState.TRANSFER) { + throw new IllegalStateException("Unexpected end transfer event in state: " + state); + } + state = LKMapXtfReaderState.COMPLETED; + System.out.println("End transfer"); + return false; + } + default -> throw new IllegalStateException("Unexpected iox event: " + event); + } + event = reader.read(); } - } else { - throw new IllegalStateException("Expected start basket event, got: " + event); - } - event = reader.read(); - while (event instanceof ObjectEvent objectEvent) { - consumer.accept(objectEvent.getIomObject()); - event = reader.read(); + throw new IllegalStateException("Unexpected end of file"); + } catch (IoxException e) { + throw new RuntimeException(e); } + } - if (!(event instanceof EndBasketEvent)) { - throw new IllegalStateException("Expected end basket event, got: " + event); - } + @Override + public Spliterator trySplit() { + return null; + } - event = reader.read(); + @Override + public long estimateSize() { + return Long.MAX_VALUE; } - } - @Override - public void close() throws Exception { - reader.close(); + @Override + public int characteristics() { + return IMMUTABLE | NONNULL; + } } } diff --git a/src/main/java/ch/geowerkstatt/lk2dxf/Main.java b/src/main/java/ch/geowerkstatt/lk2dxf/Main.java index 788b8c0..4c0dac1 100644 --- a/src/main/java/ch/geowerkstatt/lk2dxf/Main.java +++ b/src/main/java/ch/geowerkstatt/lk2dxf/Main.java @@ -1,5 +1,6 @@ package ch.geowerkstatt.lk2dxf; +import com.vividsolutions.jts.geom.Geometry; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; @@ -10,6 +11,8 @@ import java.io.File; import java.util.List; import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; public final class Main { private static final String OPTION_HELP = "help"; @@ -50,11 +53,19 @@ public static void main(String[] args) { } private static void processFiles(LK2DxfOptions options) { + Optional perimeter = options.parsePerimeter(); + for (String xtfFile : options.xtfFiles()) { try (LKMapXtfReader reader = new LKMapXtfReader(new File(xtfFile))) { - reader.readObjects(iomObject -> { - System.out.println(iomObject.getobjectoid()); - }); + AtomicInteger counter = new AtomicInteger(); + Stream objects = reader.readObjects() + .map(GeometryObject::create); + + if (perimeter.isPresent()) { + objects = objects.filter(o -> perimeter.get().intersects(o.geometry())); + } + + objects.forEach(o -> System.out.println(counter.incrementAndGet() + ": " + o.iomObject().getobjectoid())); } catch (Exception e) { System.err.println("Failed to process file: " + xtfFile); e.printStackTrace(); diff --git a/src/test/data/LKMapXtfReaderTest/ValidMultipleBaskets.xtf b/src/test/data/LKMapXtfReaderTest/ValidMultipleBaskets.xtf new file mode 100644 index 0000000..eaac0e2 --- /dev/null +++ b/src/test/data/LKMapXtfReaderTest/ValidMultipleBaskets.xtf @@ -0,0 +1,100 @@ + + + + + + + + + + test data + + + + + basket1object001 + + + Test + Test + 20241030 + + + Test + unbekannt + in_Betrieb + + + + + + 2669992.781 + 1208904.666 + + + 2669971.614 + 1206904.416 + + + 2672924.364 + 1206936.166 + + + 2672818.531 + 1208915.250 + + + 2669992.781 + 1208904.666 + + + + + + Wasser.unbekannt + + + basket1object002 + + + Test + Test + 20241030 + + + Test + unbekannt + in_Betrieb + + + 2669992.781 + 1208904.666 + + + Wasser.Absperrorgan + + + + + basket2object001 + + + Test + Test + 20241030 + + + Test + ungenau + in_Betrieb + + + 2669992.781 + 1208904.666 + + + Fernwaerme.unbekannt + + + + \ No newline at end of file diff --git a/src/test/java/ch/geowerkstatt/lk2dxf/LKMapXtfReaderTest.java b/src/test/java/ch/geowerkstatt/lk2dxf/LKMapXtfReaderTest.java index cf69adf..7df1b93 100644 --- a/src/test/java/ch/geowerkstatt/lk2dxf/LKMapXtfReaderTest.java +++ b/src/test/java/ch/geowerkstatt/lk2dxf/LKMapXtfReaderTest.java @@ -1,10 +1,10 @@ package ch.geowerkstatt.lk2dxf; +import ch.interlis.iom.IomObject; import org.junit.jupiter.api.Test; import java.io.File; -import java.util.ArrayList; -import java.util.List; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -15,18 +15,40 @@ public final class LKMapXtfReaderTest { @Test public void readValidXtf() throws Exception { try (LKMapXtfReader reader = new LKMapXtfReader(new File(TEST_DIR + "Valid.xtf"))) { - List objectIds = new ArrayList<>(); + String[] objectIds = reader + .readObjects() + .map(IomObject::getobjectoid) + .toArray(String[]::new); - reader.readObjects(iomObject -> objectIds.add(iomObject.getobjectoid())); + assertArrayEquals(new String[] {"obj1"}, objectIds); + } + } - assertArrayEquals(new String[] {"obj1"}, objectIds.toArray()); + @Test + public void readValidXtfMultipleBaskets() throws Exception { + try (LKMapXtfReader reader = new LKMapXtfReader(new File(TEST_DIR + "ValidMultipleBaskets.xtf"))) { + String[] objectIds = reader + .readObjects() + .map(IomObject::getobjectoid) + .toArray(String[]::new); + + assertArrayEquals(new String[] {"basket1object001", "basket1object002", "basket2object001"}, objectIds); } } @Test public void readXtfForWrongModel() throws Exception { try (LKMapXtfReader reader = new LKMapXtfReader(new File(TEST_DIR + "WrongModel.xtf"))) { - assertThrows(IllegalStateException.class, () -> reader.readObjects(iomObject -> { })); + Stream objectStream = reader.readObjects(); + assertThrows(IllegalStateException.class, objectStream::toList, "Streaming invalid data should throw an exception"); + } + } + + @Test + public void multipleReadsNotAllowed() throws Exception { + try (LKMapXtfReader reader = new LKMapXtfReader(new File(TEST_DIR + "Valid.xtf"))) { + reader.readObjects(); + assertThrows(IllegalStateException.class, reader::readObjects, "Multiple calls to readObjects should throw an exception"); } } }