Commit 4c723c5a authored by Radmir Gesler's avatar Radmir Gesler
Browse files

The first look to a point cloud (PC) readers for different PC formats,...

The first look to a point cloud (PC) readers for different PC formats, including a junit test for all parser components and test data.
parent d3ca6500
Pipeline #12358 passed with stage
in 2 minutes and 12 seconds
...@@ -84,6 +84,12 @@ ...@@ -84,6 +84,12 @@
<groupId>com.zaxxer</groupId> <groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId> <artifactId>HikariCP</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.github.mreutegg</groupId>
<artifactId>laszip4j</artifactId>
<version>0.20</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
<resources> <resources>
......
...@@ -66,6 +66,8 @@ public class CityDoctorModel { ...@@ -66,6 +66,8 @@ public class CityDoctorModel {
private CityGMLVersion cityGMLVersion; private CityGMLVersion cityGMLVersion;
private final CityObjectCache cache; private final CityObjectCache cache;
// private PointCloud pointCloud;
public CityDoctorModel(ParserConfiguration config, File file, CityObjectCache cache) { public CityDoctorModel(ParserConfiguration config, File file, CityObjectCache cache) {
if (config == null) { if (config == null) {
throw new IllegalArgumentException("Parser configuration may not be null"); throw new IllegalArgumentException("Parser configuration may not be null");
......
/*
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 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.
*
* CityDoctor2 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 CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.datastructure;
import java.util.Arrays;
/**
* Represents a collection of points in a three-dimensional space, with optional attributes
* such as color, intensity, and classification. Each point is defined by its X, Y, and Z coordinates.
*
* This class is immutable and ensures that all points are stored in separate arrays for each
* attribute. It also supports basic validation and slicing functionality.
*
* @author Radmir Gesler
*/
public final class PointCloud {
public final float[] x;
public final float[] y;
public final float[] z;
// Optional attributes if existing
public final int[] rgb; // packed 0xRRGGBB
public final short[] intensity; // LAS intensity
public final byte[] classification; // LAS classification
public final int size;
public PointCloud(float[] x, float[] y, float[] z,
int[] rgb, short[] intensity, byte[] classification,
int size) {
this.x = x;
this.y = y;
this.z = z;
this.rgb = rgb;
this.intensity = intensity;
this.classification = classification;
this.size = size;
}
/**
* Performs a sanity check on the internal state of the PointCloud to ensure data consistency.
* This method validates the integrity of the core arrays (x, y, z) and optional attributes
* (rgb, intensity, classification) based on the expected size of the PointCloud.
*
* Throws an exception if one of the following conditions is detected:
* - The x, y, or z arrays are null.
* - The length of the x, y, or z arrays is smaller than the specified size of the PointCloud.
* - If the rgb array is not null, its length is checked to ensure it matches or exceeds the size.
* - If the intensity array is not null, its length is checked to ensure it matches or exceeds the size.
* - If the classification array is not null, its length is checked to ensure it matches or exceeds the size.
*
* @throws IllegalStateException if any of the above conditions are violated.
*/
public boolean sanityCheck() {
if (x == null || y == null || z == null)
throw new IllegalStateException("XYZ missing");
if (x.length < size || y.length < size || z.length < size)
throw new IllegalStateException("Array too small");
if (rgb != null && rgb.length < size)
throw new IllegalStateException("RGB array too small");
if (intensity != null && intensity.length < size)
throw new IllegalStateException("Intensity array too small");
if (classification != null && classification.length < size)
throw new IllegalStateException("Classification array too small");
return true;
}
/**
* Extracts a subset of points from the current PointCloud instance based on the specified range.
*
* The method creates a new PointCloud containing only the points within the specified
* range [fromInclusive, toExclusive). If any of the optional attributes (rgb, intensity,
* classification) exist, their values are also subset accordingly. If the range is invalid
* (e.g., toExclusive is less than or equal to fromInclusive), an empty PointCloud will be returned.
*
* @param fromInclusive the starting index (inclusive) of the range of points to include in the slice
* @param toExclusive the ending index (exclusive) of the range of points to include in the slice
* @return a new PointCloud object containing the subset of points within the specified range
*/
public PointCloud slice(int fromInclusive, int toExclusive) {
int n = Math.max(0, toExclusive - fromInclusive);
float[] nx = Arrays.copyOfRange(x, fromInclusive, toExclusive);
float[] ny = Arrays.copyOfRange(y, fromInclusive, toExclusive);
float[] nz = Arrays.copyOfRange(z, fromInclusive, toExclusive);
int[] nrgb = rgb == null ? null : Arrays.copyOfRange(rgb, fromInclusive, toExclusive);
short[] nint = intensity == null ? null : Arrays.copyOfRange(intensity, fromInclusive, toExclusive);
byte[] ncls = classification == null ? null : Arrays.copyOfRange(classification, fromInclusive, toExclusive);
return new PointCloud(nx, ny, nz, nrgb, nint, ncls, n);
}
}
\ No newline at end of file
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 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.
*
* CityDoctor2 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 CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import java.util.Arrays;
/**
* A dynamic byte buffer that can grow in size as needed.
* This class provides functionality to add bytes to the buffer
* and retrieve its contents or size. It ensures that the internal
* storage grows dynamically to accommodate new data.
*
* @author Radmir Gesler
*/
public final class ByteGrowableBuffer {
private byte[] data;
private int size;
public ByteGrowableBuffer() {
this(1024);
}
public ByteGrowableBuffer(int initialCapacity) {
this.data = new byte[Math.max(16, initialCapacity)];
this.size = 0;
}
public void add(byte value) {
ensureCapacity(size + 1);
data[size++] = value;
}
public int size() {
return size;
}
public byte[] toArray() {
return Arrays.copyOf(data, size);
}
private void ensureCapacity(int minCapacity) {
if (minCapacity <= data.length) {
return;
}
int newCapacity = data.length + Math.max(1024, data.length / 2);
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
data = Arrays.copyOf(data, newCapacity);
}
}
\ No newline at end of file
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 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.
*
* CityDoctor2 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 CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import java.util.Arrays;
/**
* A growable buffer for float values that dynamically increases its capacity
* as elements are added. This class provides efficient memory management by
* resizing the internal storage when necessary.
*
* @author Radmir Gesler
*/
public final class FloatGrowableBuffer {
private float[] data;
private int size;
public FloatGrowableBuffer() {
this(1024);
}
public FloatGrowableBuffer(int initialCapacity) {
this.data = new float[Math.max(16, initialCapacity)];
this.size = 0;
}
public void add(float value) {
ensureCapacity(size + 1);
data[size++] = value;
}
public int size() {
return size;
}
public float[] toArray() {
return Arrays.copyOf(data, size);
}
private void ensureCapacity(int minCapacity) {
if (minCapacity <= data.length) {
return;
}
int newCapacity = data.length + Math.max(1024, data.length / 2);
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
data = Arrays.copyOf(data, newCapacity);
}
}
\ No newline at end of file
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 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.
*
* CityDoctor2 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 CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import com.github.mreutegg.laszip4j.LASPoint;
import com.github.mreutegg.laszip4j.LASReader;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
/**
* The {@code LasReader} class provides static methods for reading LAS/LAZ files
* into point cloud data. The class contains utilities for handling LAS file
* headers, choosing point counts, and reading data efficiently into memory.
*
* The implementation is tolerant to LAS version differences:
* - LAS 1.4+: prefers the extended point count
* - LAS <= 1.3: prefers the legacy point count
* - falls back to dynamic reading if header counts are unusable
*
* This class is immutable and cannot be instantiated.
*
* @author Radmir Gesler
*/
public final class LasReader {
private LasReader() {}
public static PointCloud read(Path file) throws IOException {
LASReader reader = new LASReader(new File(file.toString()));
try {
HeaderInfo h = readHeaderInfo(reader);
int expectedCount = chooseExpectedPointCount(h);
if (expectedCount > 0) {
return readPreallocated(reader, expectedCount);
} else {
return readDynamic(reader);
}
} catch (Exception e) {
throw (e instanceof IOException)
? (IOException) e
: new IOException("Failed to read LAS/LAZ: " + file, e);
}
}
/**
* Reads and constructs a {@link HeaderInfo} object using the header information
* from the given {@link LASReader}.
*
* @param reader the LASReader instance from which the header information is extracted
* @return a HeaderInfo object containing the version details, point counts, and
* legacy point count by return extracted from the LAS file header
*/
private static HeaderInfo readHeaderInfo(LASReader reader) {
var header = reader.getHeader();
int versionMajor = header.getVersionMajor();
int versionMinor = header.getVersionMinor();
long legacyPointCount = safeLong(header::getLegacyNumberOfPointRecords);
long pointCount = safeLong(header::getNumberOfPointRecords);
int[] legacyByReturn = safeIntArray(header::getLegacyNumberOfPointsByReturn);
return new HeaderInfo(
versionMajor,
versionMinor,
legacyPointCount,
pointCount,
legacyByReturn
);
}
/**
* Determines the expected number of points in a point cloud based on the header information.
* This method accounts for version-specific differences and validates that the size
* does not exceed the maximum integer value, which is required for in-memory operations.
*
* @param h the {@link HeaderInfo} instance containing version details and point counts
* extracted from a LAS/LAZ file header.
* @return the expected number of points in the point cloud as an integer.
* @throws IOException if the number of points is too large to be processed in memory.
*/
private static int chooseExpectedPointCount(HeaderInfo h) throws IOException {
long n;
if (h.versionMajor > 1 || (h.versionMajor == 1 && h.versionMinor >= 4)) {
n = h.pointCount > 0 ? h.pointCount : h.legacyPointCount;
} else {
n = h.legacyPointCount > 0 ? h.legacyPointCount : h.pointCount;
}
if (n <= 0) {
return 0;
}
if (n > Integer.MAX_VALUE) {
throw new IOException("Point cloud too large for in-memory loading: " + n);
}
return (int) n;
}
/**
* Reads a LAS point cloud using pre-allocated arrays for efficiency, based on an expected number of points.
* The method dynamically adjusts array sizes if the actual number of points exceeds the initial expectation.
* If the header underestimates the point count, the arrays are grown defensively.
*
* @param reader the LASReader instance used to read points from the LAS/LAZ file
* @param expectedCount the initial estimated number of points in the LAS/LAZ file
* @return a PointCloud object containing all the points read from the LAS/LAZ file, including
* their coordinates, intensity, and classification
*/
private static PointCloud readPreallocated(LASReader reader, int expectedCount) {
float[] x = new float[expectedCount];
float[] y = new float[expectedCount];
float[] z = new float[expectedCount];
short[] intensity = new short[expectedCount];
byte[] classification = new byte[expectedCount];
int i = 0;
for (LASPoint p : reader.getPoints()) {
if (i >= x.length) {
int newSize = x.length + Math.max(1024, x.length / 2);
x = Arrays.copyOf(x, newSize);
y = Arrays.copyOf(y, newSize);
z = Arrays.copyOf(z, newSize);
intensity = Arrays.copyOf(intensity, newSize);
classification = Arrays.copyOf(classification, newSize);
}
x[i] = (float) p.getX();
y[i] = (float) p.getY();
z[i] = (float) p.getZ();
intensity[i] = (short) p.getIntensity();
classification[i] = (byte) p.getClassification();
i++;
}
if (i != x.length) {
x = Arrays.copyOf(x, i);
y = Arrays.copyOf(y, i);
z = Arrays.copyOf(z, i);
intensity = Arrays.copyOf(intensity, i);
classification = Arrays.copyOf(classification, i);
}
return new PointCloud(x, y, z, null, intensity, classification, i);
}
/**
* Reads and constructs a {@link PointCloud} object dynamically by processing all the points
* in the provided {@link LASReader}. This method allocates and grows buffers as needed
* to store point data, such as coordinates, intensity, and classification, while processing
* the LAS file contents.
*
* @param reader the {@link LASReader} instance used to read points from the LAS/LAZ file
* @return a {@link PointCloud} object containing the points read from the LAS/LAZ file,
* including their X, Y, Z coordinates, intensity, and classification
*/
private static PointCloud readDynamic(LASReader reader) {
FloatGrowableBuffer xs = new FloatGrowableBuffer(16_384);
FloatGrowableBuffer ys = new FloatGrowableBuffer(16_384);
FloatGrowableBuffer zs = new FloatGrowableBuffer(16_384);
ShortGrowableBuffer intensities = new ShortGrowableBuffer(16_384);
ByteGrowableBuffer classifications = new ByteGrowableBuffer(16_384);
for (LASPoint p : reader.getPoints()) {
xs.add((float) p.getX());
ys.add((float) p.getY());
zs.add((float) p.getZ());
intensities.add((short) p.getIntensity());
classifications.add((byte) p.getClassification());
}
return new PointCloud(
xs.toArray(),
ys.toArray(),
zs.toArray(),
null,
intensities.toArray(),
classifications.toArray(),
xs.size()
);
}
/**
* Safely retrieves a long value from the given {@link LongSupplierEx} instance, handling any exceptions
* that may occur by returning a default value of 0L.
*
* @param s the {@link LongSupplierEx} instance that supplies a long value, potentially throwing an exception
* @return the long value provided by the {@link LongSupplierEx}, or 0L if an exception occurs
*/
private static long safeLong(LongSupplierEx s) {
try {
return s.getAsLong();
} catch (Throwable t) {
return 0L;
}
}
/**
* Safely retrieves an integer array from the given {@link IntArraySupplierEx} instance,
* handling any exceptions or null values by returning an empty array.
*
* @param s the {@link IntArraySupplierEx} instance that supplies an integer array, potentially throwing an exception
* @return the integer array provided by the {@link IntArraySupplierEx}, or an empty array if an exception occurs or the supplied array is null
*/
private static int[] safeIntArray(IntArraySupplierEx s) {
try {
int[] arr = s.get();
return arr != null ? arr : new int[0];
} catch (Throwable t) {
return new int[0];
}
}
/**
* Represents a functional interface that supplies a long value and allows for checked exceptions.
* This is similar to {@link java.util.function.LongSupplier}, but it supports operations
* that may throw a checked exception.
*
* Implementations of this interface are primarily used in cases where
* a long value needs to be retrieved, but the operation may encounter an exception.
*
* Functional Interface:
* This interface is a functional interface, meaning it can be used
* as the assignment target for a lambda expression or method reference.
*/
@FunctionalInterface
private interface LongSupplierEx {
long getAsLong() throws Exception;
}
/**
* Represents a functional interface that supplies an integer array and may throw an exception.
* This interface is typically used in contexts where an integer array needs to be provided,
* but the operation might fail and throw a checked exception.
*
* It is intended to be utilized with methods that safely handle potentially
* exceptional cases when retrieving integer arrays.
*
* Functional method:
* - {@code int[] get()} : Supplies an integer array and allows exception handling.
*/
@FunctionalInterface
private interface IntArraySupplierEx {
int[] get() throws Exception;
}
/**
* Represents header information extracted from a LAS/LAZ file.
* This record encapsulates version details, point counts, and
* legacy-specific data extracted from the file header.
*
* Fields:
* - versionMajor: The major version number of the LAS/LAZ file format.
* - versionMinor: The minor version number of the LAS/LAZ file format.
* - legacyPointCount: The number of points as indicated by legacy headers, which may not account for version-specific enhancements.
* - pointCount: The total number of points in the LAS/LAZ file, accounting for format version differences.
* - legacyByReturn: An array specifying the legacy point count distribution across different return classifications.
*/
private record HeaderInfo(
int versionMajor,
int versionMinor,
long legacyPointCount,
long pointCount,
int[] legacyByReturn
) {
}
}
\ No newline at end of file
/*
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 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.
*
* CityDoctor2 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 CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import java.io.*;
import java.nio.file.Path;
import java.util.Locale;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
/**
* A utility class for parsing and loading point cloud data from external files into
* {@link PointCloud} objects.
*
* @author Radmir Gesler
*/
public class PointCloudParser {
public enum Format {
PLY,
XYZ, // generic x y z text (also covers .pts with xyz only)
LAS, // optional, requires laszip4j
LAZ // optional, requires laszip4j
}
/**
* Reads a point cloud from the given file. The method determines the appropriate
* reader based on the file's extension and parses the data into a {@code PointCloud} object.
* Supported file formats include:
* - .ply (Polygon File Format)
* - .xyz, .pts, .txt (Plain text point cloud)
* - .las, .laz (LASer file format and its compressed variant)
*
* If the file extension is not recognized or not supported, an {@code IOException}
* is thrown.
*
* @param file the {@code Path} of the point cloud file to be read
* @return a {@code PointCloud} object representing the parsed point cloud data
* @throws IOException if an I/O error occurs during reading or if the file format is unsupported
*/
public static PointCloud read(Path file) throws IOException {
String name = file.getFileName().toString().toLowerCase(Locale.ROOT);
if (name.endsWith(".ply")) return PlyReader.read(file);
if (name.endsWith(".xyz") || name.endsWith(".pts") || name.endsWith(".txt")) return XyzReader.read(file);
if (name.endsWith(".las") || name.endsWith(".laz")) return LasReader.read(file);
throw new IOException("Unsupported point cloud format: " + file);
}
/* Maybe for GUI dropdown?*/
public static PointCloud read(Path file, Format format) throws IOException {
return switch (format) {
case PLY -> PlyReader.read(file);
case XYZ -> XyzReader.read(file);
case LAS, LAZ -> LasReader.read(file);
};
}
}
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 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.
*
* CityDoctor2 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 CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import java.util.Arrays;
/**
* A dynamic buffer that holds short values and can grow automatically as new
* elements are added. This buffer provides functionality for appending elements,
* obtaining the current size, and converting the buffer to an array.
*
* @author Radmir Gesler
*/
public final class ShortGrowableBuffer {
private short[] data;
private int size;
public ShortGrowableBuffer() {
this(1024);
}
public ShortGrowableBuffer(int initialCapacity) {
this.data = new short[Math.max(16, initialCapacity)];
this.size = 0;
}
public void add(short value) {
ensureCapacity(size + 1);
data[size++] = value;
}
public int size() {
return size;
}
public short[] toArray() {
return Arrays.copyOf(data, size);
}
private void ensureCapacity(int minCapacity) {
if (minCapacity <= data.length) {
return;
}
int newCapacity = data.length + Math.max(1024, data.length / 2);
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
data = Arrays.copyOf(data, newCapacity);
}
}
\ No newline at end of file
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 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.
*
* CityDoctor2 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 CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser.pointcloud;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
/**
* A utility class for reading and parsing XYZ point cloud data from a file.
*
* The XYZ format is a plain text format where each line typically consists of three
* floating-point numbers specifying the X, Y, and Z coordinates of a point in
* three-dimensional space. Lines beginning with '#' or empty lines are considered
* comments or whitespace and are ignored during parsing.
*
* This class provides a method to read an XYZ file and convert its content into
* a {@code PointCloud} instance containing separate arrays for X, Y, and Z coordinates.
* The class is immutable, and its methods ensure the robustness of the parsing process.
*
* The reader uses internal growable buffers to store points while processing the input file,
* ensuring efficient memory allocation even for large datasets. Each coordinate is
* stored in a dedicated buffer, and the buffers are converted into arrays when the
* file processing completes.
*
* This utility class cannot be instantiated.
*
* This class is thread-safe only if used sequentially, as concurrent modification
* or access to the static method may cause unintended behavior.
*
* @author Radmir Gesler
*/
public final class XyzReader {
private XyzReader() {
}
static PointCloud read(Path file) throws IOException {
FloatGrowableBuffer xs = new FloatGrowableBuffer(16_384);
FloatGrowableBuffer ys = new FloatGrowableBuffer(16_384);
FloatGrowableBuffer zs = new FloatGrowableBuffer(16_384);
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream(file.toFile()), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
String[] parts = line.split("\\s+");
if (parts.length < 3) {
continue;
}
xs.add(Float.parseFloat(parts[0]));
ys.add(Float.parseFloat(parts[1]));
zs.add(Float.parseFloat(parts[2]));
}
}
return new PointCloud(
xs.toArray(),
ys.toArray(),
zs.toArray(),
null,
null,
null,
xs.size()
);
}
}
\ No newline at end of file
/*-
* Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
*
* This file is part of CityDoctor2.
*
* CityDoctor2 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.
*
* CityDoctor2 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 CityDoctor2. If not, see <https://www.gnu.org/licenses/>.
*/
package de.hft.stuttgart.citydoctor2.parser;
import static org.junit.Assert.assertSame;
import de.hft.stuttgart.citydoctor2.datastructure.PointCloud;
import de.hft.stuttgart.citydoctor2.parser.pointcloud.PointCloudParser;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Path;
/**
*
* @author Radmir Gesler
*
*/
public class PointCloudParserTest {
@Test
public void testPointCloudParsing() throws IOException {
testHelperForPointCloud("*.ply","src/test/resources/duedo_small.ply");
testHelperForPointCloud("*.ply","src/test/resources/duedo_small_bin.ply");
testHelperForPointCloud("*.xyz","src/test/resources/duedo_small.xyz");
testHelperForPointCloud("*.pts","src/test/resources/duedo_small.pts");
testHelperForPointCloud("*.txt","src/test/resources/duedo_small.txt");
testHelperForPointCloud("*.las","src/test/resources/duedo_small.las");
testHelperForPointCloud("*.laz","src/test/resources/duedo_small.laz");
}
private void testHelperForPointCloud(String type, String file) throws IOException {
System.out.println("====================================================================");
System.out.println("Test " + type + " parsing from : " + file);
System.out.println("====================================================================");
PointCloud pc = PointCloudParser.read(Path.of(file));
if (pc.sanityCheck()) {
System.out.println(" Sanity check passed");
System.out.println(" Loaded points: " + pc.size);
System.out.println(" First point: " + pc.x[0] + ", " + pc.y[0] + ", " + pc.z[0]);
System.out.println(" Test " + file + " passed");
} else {
System.out.println(" Test " + file + " NOT passed");
}
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment