package com.revolsys.elevation.cloud.las;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import com.revolsys.collection.map.LinkedHashMapEx;
import com.revolsys.collection.map.MapEx;
import com.revolsys.elevation.cloud.PointCloud;
import com.revolsys.elevation.cloud.las.pointformat.LasPoint;
import com.revolsys.elevation.cloud.las.pointformat.LasPoint0Core;
import com.revolsys.elevation.cloud.las.pointformat.LasPointFormat;
import com.revolsys.elevation.cloud.las.zip.ArithmeticDecoder;
import com.revolsys.elevation.cloud.las.zip.LazDecompress;
import com.revolsys.elevation.cloud.las.zip.LazDecompressGpsTime11V1;
import com.revolsys.elevation.cloud.las.zip.LazDecompressGpsTime11V2;
import com.revolsys.elevation.cloud.las.zip.LazDecompressPoint10V1;
import com.revolsys.elevation.cloud.las.zip.LazDecompressPoint10V2;
import com.revolsys.elevation.cloud.las.zip.LazDecompressRgb12V1;
import com.revolsys.elevation.cloud.las.zip.LazDecompressRgb12V2;
import com.revolsys.elevation.cloud.las.zip.LazItemType;
import com.revolsys.elevation.tin.TriangulatedIrregularNetwork;
import com.revolsys.elevation.tin.quadedge.QuadEdgeDelaunayTinBuilder;
import com.revolsys.geometry.cs.CoordinateSystem;
import com.revolsys.geometry.model.BoundingBox;
import com.revolsys.geometry.model.GeometryFactory;
import com.revolsys.geometry.model.Point;
import com.revolsys.io.BaseCloseable;
import com.revolsys.io.channels.ChannelReader;
import com.revolsys.io.endian.EndianOutputStream;
import com.revolsys.io.map.MapSerializer;
import com.revolsys.spring.resource.InputStreamResource;
import com.revolsys.spring.resource.Resource;
import com.revolsys.util.Exceptions;
public class LasPointCloud implements PointCloud<LasPoint>, BaseCloseable, MapSerializer {
public static void forEachPoint(final Object source, final Consumer<? super LasPoint> action) {
final Resource resource = Resource.getResource(source);
try (
final LasPointCloud pointCloud = new LasPointCloud(resource)) {
pointCloud.forEachPoint(action);
}
}
private GeometryFactory geometryFactory = GeometryFactory.fixedNoSrid(1000.0, 1000.0, 1000.0);
private LasPointCloudHeader header;
private List<LasPoint> points = new ArrayList<>();
private ChannelReader reader;
private Resource resource;
private ZipInputStream zipIn;
public LasPointCloud(final GeometryFactory geometryFactory) {
this(LasPointFormat.Core, geometryFactory);
}
public LasPointCloud(final LasPointFormat pointFormat, final GeometryFactory geometryFactory) {
this.setHeader(new LasPointCloudHeader(pointFormat, geometryFactory));
}
public LasPointCloud(final Resource resource) {
this(resource, null);
}
public LasPointCloud(Resource resource, final GeometryFactory geometryFactory) {
this.resource = resource;
if (resource.getFileNameExtension().equals("zip")) {
boolean found = false;
final String baseName = resource.getBaseName();
final String fileName = baseName + ".las";
this.zipIn = this.resource.newBufferedInputStream(ZipInputStream::new);
try {
for (ZipEntry zipEntry = this.zipIn.getNextEntry(); zipEntry != null; zipEntry = this.zipIn
.getNextEntry()) {
final String name = zipEntry.getName();
if (name.equalsIgnoreCase(fileName)) {
resource = new InputStreamResource(this.zipIn);
found = true;
break;
}
}
} catch (final IOException e) {
throw Exceptions.wrap("Error reading: " + resource, e);
}
if (!found) {
throw new IllegalArgumentException("Cannot find file: " + resource + "!" + fileName);
}
}
this.reader = resource.newChannelReader(8096, ByteOrder.LITTLE_ENDIAN);
this.setHeader(new LasPointCloudHeader(this.reader, geometryFactory));
}
@SuppressWarnings("unchecked")
public <P extends LasPoint0Core> P addPoint(final double x, final double y, final double z) {
final LasPoint lasPoint = this.header.newLasPoint(this, x, y, z);
this.points.add(lasPoint);
return (P)lasPoint;
}
public void clear() {
this.header.clear();
this.points.clear();
}
@Override
public void close() {
final ChannelReader reader = this.reader;
this.reader = null;
if (reader != null) {
reader.close();
}
if (this.zipIn != null) {
try {
this.zipIn.close();
} catch (final IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
@Override
public void forEachPoint(final Consumer<? super LasPoint> action) {
final long pointCount = getPointCount();
try {
final ChannelReader reader = this.reader;
if (reader == null) {
this.points.forEach(action);
} else if (pointCount == 0) {
this.reader = null;
} else if (this.header.isLaszip()) {
forEachPointLaz(action);
} else {
try (
BaseCloseable closable = this) {
final LasPointFormat pointFormat = getPointFormat();
for (int i = 0; i < pointCount; i++) {
final LasPoint point = pointFormat.readLasPoint(this, reader);
action.accept(point);
}
}
}
} finally {
this.reader = null;
}
}
private void forEachPointLaz(final Consumer<? super LasPoint> action) {
try (
ArithmeticDecoder decoder = new ArithmeticDecoder(this.reader);
BaseCloseable closable = this;) {
final LasZipHeader lasZipHeader = getLasZipHeader();
final LazDecompress[] pointDecompressors = newLazDecompressors(lasZipHeader, decoder);
if (lasZipHeader.isCompressor(LasZipHeader.LASZIP_COMPRESSOR_POINTWISE)) {
forEachPointLazPointwise(decoder, pointDecompressors, action);
} else {
forEachPointLazChunked(decoder, pointDecompressors, action);
}
}
}
private void forEachPointLazChunked(final ArithmeticDecoder decoder,
final LazDecompress[] pointDecompressors, final Consumer<? super LasPoint> action) {
final ChannelReader reader = this.reader;
final long chunkTableOffset = reader.getLong();
final long chunkSize = getLasZipHeader().getChunkSize();
long chunkReadCount = chunkSize;
final long pointCount = getPointCount();
for (int i = 0; i < pointCount; i++) {
final LasPoint point;
final LasPointFormat pointFormat = getPointFormat();
if (chunkSize == chunkReadCount) {
point = pointFormat.readLasPoint(this, reader);
for (final LazDecompress pointDecompressor : pointDecompressors) {
pointDecompressor.init(point);
}
decoder.reset();
chunkReadCount = 0;
} else {
point = pointFormat.newLasPoint(this);
for (final LazDecompress pointDecompressor : pointDecompressors) {
pointDecompressor.read(point);
}
}
action.accept(point);
chunkReadCount++;
}
}
private void forEachPointLazPointwise(final ArithmeticDecoder decoder,
final LazDecompress[] pointDecompressors, final Consumer<? super LasPoint> action) {
final LasPointFormat pointFormat = getPointFormat();
{
final ChannelReader reader = this.reader;
final LasPoint point = pointFormat.readLasPoint(this, reader);
for (final LazDecompress pointDecompressor : pointDecompressors) {
pointDecompressor.init(point);
}
decoder.reset();
action.accept(point);
}
final long pointCount = getPointCount();
for (int i = 1; i < pointCount; i++) {
final LasPoint point = pointFormat.newLasPoint(this);
for (final LazDecompress pointDecompressor : pointDecompressors) {
pointDecompressor.read(point);
}
action.accept(point);
}
}
@Override
public BoundingBox getBoundingBox() {
return this.header.getBoundingBox();
}
@Override
public GeometryFactory getGeometryFactory() {
return this.geometryFactory;
}
public LasPointCloudHeader getHeader() {
return this.header;
}
public LasZipHeader getLasZipHeader() {
return this.header.getLasZipHeader();
}
public long getPointCount() {
return this.header.getPointCount();
}
public LasPointFormat getPointFormat() {
return this.header.getPointFormat();
}
public List<LasPoint> getPoints() {
return this.points;
}
public LazDecompress[] newLazDecompressors(final LasZipHeader lasZipHeader,
final ArithmeticDecoder decoder) {
final int numItems = lasZipHeader.getNumItems();
final LazDecompress[] pointDecompressors = new LazDecompress[numItems];
for (int i = 0; i < numItems; i++) {
final LazItemType type = lasZipHeader.getType(i);
final int version = lasZipHeader.getVersion(i);
if (version < 1 || version > 2) {
throw new RuntimeException(version + " not yet supported");
}
switch (type) {
case POINT10:
if (version == 1) {
pointDecompressors[i] = new LazDecompressPoint10V1(this, decoder);
} else {
pointDecompressors[i] = new LazDecompressPoint10V2(this, decoder);
}
break;
case GPSTIME11:
if (version == 1) {
pointDecompressors[i] = new LazDecompressGpsTime11V1(decoder);
} else {
pointDecompressors[i] = new LazDecompressGpsTime11V2(decoder);
}
break;
case RGB12:
if (version == 1) {
pointDecompressors[i] = new LazDecompressRgb12V1(decoder);
} else {
pointDecompressors[i] = new LazDecompressRgb12V2(decoder);
}
break;
default:
throw new RuntimeException(type + " not yet supported");
}
}
return pointDecompressors;
}
@Override
public TriangulatedIrregularNetwork newTriangulatedIrregularNetwork() {
final GeometryFactory geometryFactory = getGeometryFactory();
final QuadEdgeDelaunayTinBuilder tinBuilder = new QuadEdgeDelaunayTinBuilder(geometryFactory);
forEachPoint((lasPoint) -> {
tinBuilder.insertVertex(lasPoint);
});
final TriangulatedIrregularNetwork tin = tinBuilder.newTriangulatedIrregularNetwork();
return tin;
}
@Override
public TriangulatedIrregularNetwork newTriangulatedIrregularNetwork(
final Predicate<? super Point> filter) {
final GeometryFactory geometryFactory = getGeometryFactory();
final QuadEdgeDelaunayTinBuilder tinBuilder = new QuadEdgeDelaunayTinBuilder(geometryFactory);
forEachPoint((lasPoint) -> {
if (filter.test(lasPoint)) {
tinBuilder.insertVertex(lasPoint);
}
});
final TriangulatedIrregularNetwork tin = tinBuilder.newTriangulatedIrregularNetwork();
return tin;
}
public void read() {
if (this.reader != null) {
this.points = new ArrayList<>((int)getPointCount());
forEachPoint(this.points::add);
}
}
@SuppressWarnings("unchecked")
public <P extends Point> int read(final Predicate<P> filter) {
if (this.reader != null) {
this.points = new ArrayList<>((int)getPointCount());
forEachPoint((point) -> {
if (filter.test((P)point)) {
this.points.add(point);
}
});
}
return this.points.size();
}
public void setCoordinateSystemInternal(final CoordinateSystem coordinateSystem) {
this.geometryFactory = this.geometryFactory.convertCoordinateSystem(coordinateSystem);
}
private void setHeader(final LasPointCloudHeader header) {
this.header = header;
this.geometryFactory = header.getGeometryFactory();
}
@Override
public double toDoubleX(final int x) {
return this.geometryFactory.toDoubleX(x);
}
@Override
public double toDoubleY(final int y) {
return this.geometryFactory.toDoubleY(y);
}
@Override
public double toDoubleZ(final int z) {
return this.geometryFactory.toDoubleZ(z);
}
@Override
public int toIntX(final double x) {
return this.geometryFactory.toIntX(x);
}
@Override
public int toIntY(final double y) {
return this.geometryFactory.toIntY(y);
}
@Override
public int toIntZ(final double z) {
return this.geometryFactory.toIntZ(z);
}
@Override
public MapEx toMap() {
final MapEx map = new LinkedHashMapEx();
addToMap(map, "url", this.resource.getUri());
addToMap(map, "header", this.header);
return map;
}
public void writePointCloud(final Object target) {
final Resource resource = Resource.getResource(target);
try (
EndianOutputStream out = resource.newBufferedOutputStream(EndianOutputStream::new)) {
this.header.writeHeader(out);
for (final LasPoint point : this.points) {
point.write(out);
}
}
}
}