package com.revolsys.elevation.cloud.las; import java.io.IOException; import java.sql.Date; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.function.BiFunction; import com.revolsys.collection.map.LinkedHashMapEx; import com.revolsys.collection.map.MapEx; import com.revolsys.elevation.cloud.las.pointformat.LasPoint; import com.revolsys.elevation.cloud.las.pointformat.LasPointFormat; import com.revolsys.geometry.cs.CoordinateSystem; import com.revolsys.geometry.model.BoundingBox; import com.revolsys.geometry.model.BoundingBoxProxy; import com.revolsys.geometry.model.GeometryFactory; import com.revolsys.geometry.model.GeometryFactoryProxy; import com.revolsys.geometry.util.BoundingBoxUtil; import com.revolsys.io.channels.ChannelReader; import com.revolsys.io.endian.EndianOutputStream; import com.revolsys.io.map.MapSerializer; import com.revolsys.record.schema.RecordDefinition; import com.revolsys.spring.resource.Resource; import com.revolsys.util.Exceptions; import com.revolsys.util.Pair; public class LasPointCloudHeader implements BoundingBoxProxy, GeometryFactoryProxy, MapSerializer { private static final Map<Pair<String, Integer>, BiFunction<LasPointCloudHeader, byte[], Object>> PROPERTY_FACTORY_BY_KEY = new HashMap<>(); static { LasProjection.init(PROPERTY_FACTORY_BY_KEY); LasZipHeader.init(PROPERTY_FACTORY_BY_KEY); } public static final Version VERSION_1_0 = new Version(1, 0); public static final Version VERSION_1_1 = new Version(1, 1); public static final Version VERSION_1_2 = new Version(1, 2); public static final Version VERSION_1_3 = new Version(1, 3); public static final Version VERSION_1_4 = new Version(1, 4); private static final long MAX_UNSIGNED_INT = 1l << 32; private final double[] bounds; private int dayOfYear = new GregorianCalendar().get(Calendar.DAY_OF_YEAR); private int fileSourceId; private String generatingSoftware = "RevolutionGIS"; private GeometryFactory geometryFactory = GeometryFactory.fixedNoSrid(1000.0, 1000.0, 1000.0); private int globalEncoding = 0; private final Map<Pair<String, Integer>, LasVariableLengthRecord> lasProperties = new LinkedHashMap<>(); private Version version = LasPointCloudHeader.VERSION_1_2; private long pointCount = 0; private final long[] pointCountByReturn = { 0l, 0l, 0l, 0l, 0l, 0l, 0l, 0l, 0l, 0l, 0l, 0l, 0l, 0l, 0l }; private LasPointFormat pointFormat = LasPointFormat.Core; private final byte[] projectId = new byte[16]; private RecordDefinition recordDefinition; private int recordLength = 20; private Resource resource; private String systemIdentifier = "TRANSFORMATION"; private int year = new GregorianCalendar().get(Calendar.YEAR); private Date date; private int headerSize; private long pointRecordsOffset; private boolean laszip; private ChannelReader reader; private LasZipHeader lasZipHeader; @SuppressWarnings("unused") public LasPointCloudHeader(final ChannelReader reader, final GeometryFactory geometryFactory) { setGeometryFactory(geometryFactory); try { if (reader.getUsAsciiString(4).equals("LASF")) { this.fileSourceId = reader.getUnsignedShort(); this.globalEncoding = reader.getUnsignedShort(); // final long guid1 = Buffers.getLEUnsignedInt(header); // final int guid2 = Buffers.getLEUnsignedShort(header); // final int guid3 = Buffers.getLEUnsignedShort(header); // final byte[] guid4 = header.getBytes(8); reader.getBytes(this.projectId); this.version = new Version(reader); this.systemIdentifier = reader.getUsAsciiString(32); this.generatingSoftware = reader.getUsAsciiString(32); this.dayOfYear = reader.getUnsignedShort(); this.year = reader.getUnsignedShort(); final Calendar calendar = new GregorianCalendar(); calendar.set(Calendar.YEAR, this.year); calendar.set(Calendar.DAY_OF_YEAR, this.dayOfYear); this.date = new Date(calendar.getTimeInMillis()); this.headerSize = reader.getUnsignedShort(); this.pointRecordsOffset = reader.getUnsignedInt(); final long numberOfVariableLengthRecords = reader.getUnsignedInt(); int pointFormatId = reader.getUnsignedByte(); if (pointFormatId > 127) { pointFormatId -= 128; this.laszip = true; } this.pointFormat = LasPointFormat.getById(pointFormatId); this.recordLength = reader.getUnsignedShort(); this.pointCount = (int)reader.getUnsignedInt(); for (int i = 0; i < 5; i++) { this.pointCountByReturn[i] = reader.getUnsignedInt(); } final double scaleX = 1 / reader.getDouble(); final double scaleY = 1 / reader.getDouble(); final double scaleZ = 1 / reader.getDouble(); final double offsetX = reader.getDouble(); final double offsetY = reader.getDouble(); final double offsetZ = reader.getDouble(); final CoordinateSystem coordinateSystem = this.geometryFactory.getCoordinateSystem(); this.geometryFactory = GeometryFactory.newWithOffsets(coordinateSystem, offsetX, scaleX, offsetY, scaleY, offsetZ, scaleZ); final double maxX = reader.getDouble(); final double minX = reader.getDouble(); final double maxY = reader.getDouble(); final double minY = reader.getDouble(); final double maxZ = reader.getDouble(); final double minZ = reader.getDouble(); if (this.headerSize > 227) { if (this.version.atLeast(LasPointCloudHeader.VERSION_1_3)) { final long startOfWaveformDataPacketRecord = reader.getUnsignedLong(); // TODO // unsigned // long // long support // needed if (this.version.atLeast(LasPointCloudHeader.VERSION_1_4)) { final long startOfFirstExetendedDataRecord = reader.getUnsignedLong(); final long numberOfExtendedVariableLengthRecords = reader.getUnsignedInt(); this.pointCount = reader.getUnsignedLong(); for (int i = 0; i < 15; i++) { this.pointCountByReturn[i] = reader.getUnsignedLong(); } } } } this.headerSize += readVariableLengthRecords(numberOfVariableLengthRecords); this.bounds = new double[] { minX, minY, minZ, maxX, maxY, maxZ }; if (this.version.equals(LasPointCloudHeader.VERSION_1_0)) { reader.skipBytes(2); this.headerSize += 2; } final int skipCount = (int)(this.pointRecordsOffset - this.headerSize); reader.skipBytes(skipCount); // Skip to first point record this.recordDefinition = this.pointFormat.newRecordDefinition(this.geometryFactory); } else { throw new IllegalArgumentException(this.resource + " is not a valid LAS file"); } } catch (final IOException e) { throw Exceptions.wrap("Error reading " + this.resource, e); } } public LasPointCloudHeader(final LasPointFormat pointFormat, final GeometryFactory geometryFactory) { this.pointFormat = pointFormat; if (this.pointFormat.getId() > 5) { this.globalEncoding |= 0b10000; } this.recordLength = pointFormat.getRecordLength(); setGeometryFactory(geometryFactory); this.bounds = BoundingBoxUtil.newBounds(3); } protected void addProperty(final LasVariableLengthRecord property) { final Pair<String, Integer> key = property.getKey(); this.lasProperties.put(key, property); } protected void clear() { this.pointCount = 0; for (int i = 0; i < this.pointCountByReturn.length; i++) { this.pointCountByReturn[i] = 0; } Arrays.fill(this.bounds, Double.NaN); } @Override public BoundingBox getBoundingBox() { return this.geometryFactory.newBoundingBox(3, this.bounds); } public int getDayOfYear() { return this.dayOfYear; } public int getFileSourceId() { return this.fileSourceId; } public String getGeneratingSoftware() { return this.generatingSoftware; } @Override public GeometryFactory getGeometryFactory() { return this.geometryFactory; } public int getGlobalEncoding() { return this.globalEncoding; } protected LasVariableLengthRecord getLasProperty(final Pair<String, Integer> key) { return this.lasProperties.get(key); } public LasZipHeader getLasZipHeader() { return this.lasZipHeader; } public long getPointCount() { return this.pointCount; } public LasPointFormat getPointFormat() { return this.pointFormat; } public int getPointFormatId() { return this.pointFormat.getId(); } public byte[] getProjectId() { return this.projectId; } public RecordDefinition getRecordDefinition() { return this.recordDefinition; } public int getRecordLength() { return this.recordLength; } public String getSystemIdentifier() { return this.systemIdentifier; } public Version getVersion() { return this.version; } public int getYear() { return this.year; } public boolean isLaszip() { return this.laszip; } public LasPoint newLasPoint(final LasPointCloud lasPointCloud, final double x, final double y, final double z) { this.pointCount++; this.pointCountByReturn[0]++; BoundingBoxUtil.expand(this.bounds, 3, x, y, z); return this.pointFormat.newLasPoint(lasPointCloud, x, y, z); } private int readVariableLengthRecords(final long numberOfVariableLengthRecords) throws IOException { int byteCount = 0; for (int i = 0; i < numberOfVariableLengthRecords; i++) { @SuppressWarnings("unused") final int reserved = this.reader.getUnsignedShort(); // Ignore reserved // value; final String userId = this.reader.getUsAsciiString(16); final int recordId = this.reader.getUnsignedShort(); final int valueLength = this.reader.getUnsignedShort(); final String description = this.reader.getUsAsciiString(32); final byte[] bytes = this.reader.getBytes(valueLength); final LasVariableLengthRecord property = new LasVariableLengthRecord(userId, recordId, description, bytes); addProperty(property); byteCount += 54 + valueLength; } for (final Entry<Pair<String, Integer>, LasVariableLengthRecord> entry : this.lasProperties .entrySet()) { final Pair<String, Integer> key = entry.getKey(); final LasVariableLengthRecord property = entry.getValue(); final BiFunction<LasPointCloudHeader, byte[], Object> converter = PROPERTY_FACTORY_BY_KEY .get(key); if (converter != null) { property.convertValue(converter, this); } } return byteCount; } protected void removeLasProperties(final String userId) { for (final Iterator<LasVariableLengthRecord> iterator = this.lasProperties.values() .iterator(); iterator.hasNext();) { final LasVariableLengthRecord property = iterator.next(); if (userId.equals(property.getUserId())) { iterator.remove(); } } } public void setCoordinateSystemInternal(final CoordinateSystem coordinateSystem) { this.geometryFactory = this.geometryFactory.convertCoordinateSystem(coordinateSystem); } protected void setGeometryFactory(final GeometryFactory geometryFactory) { if (geometryFactory != null) { final CoordinateSystem coordinateSystem = geometryFactory.getCoordinateSystem(); if (coordinateSystem == null) { throw new IllegalArgumentException("A valid coordinate system must be specified"); } else { double scaleX = geometryFactory.getScaleX(); if (scaleX == 0) { scaleX = 1000; } double scaleY = geometryFactory.getScaleY(); if (scaleY == 0) { scaleY = 1000; } double scaleZ = geometryFactory.getScaleZ(); if (scaleZ == 0) { scaleZ = 1000; } final double offsetX = geometryFactory.getOffsetX(); final double offsetY = geometryFactory.getOffsetY(); final double offsetZ = geometryFactory.getOffsetZ(); this.geometryFactory = GeometryFactory.newWithOffsets(coordinateSystem, offsetX, scaleX, offsetY, scaleY, offsetZ, scaleZ); LasProjection.setCoordinateSystem(this, coordinateSystem); } } } public void setLasZipHeader(final LasZipHeader lasZipHeader) { this.lasZipHeader = lasZipHeader; } @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, "version", this.version); addToMap(map, "fileSourceId", this.fileSourceId, 0); addToMap(map, "systemIdentifier", this.systemIdentifier); addToMap(map, "generatingSoftware", this.generatingSoftware); addToMap(map, "date", this.date); addToMap(map, "headerSize", this.headerSize); addToMap(map, "pointRecordsOffset", this.pointRecordsOffset, 0); addToMap(map, "pointFormat", this.pointFormat.getId()); addToMap(map, "pointCount", this.pointCount); int returnCount = 15; if (this.pointFormat.getId() < 6) { returnCount = 5; } int returnIndex = 0; final List<Long> pointCountByReturn = new ArrayList<>(); for (final long pointCountForReturn : this.pointCountByReturn) { if (returnIndex < returnCount) { pointCountByReturn.add(pointCountForReturn); } returnIndex++; } addToMap(map, "pointCountByReturn", pointCountByReturn); return map; } public void writeHeader(final EndianOutputStream out) { out.writeBytes("LASF"); out.writeLEUnsignedShort(this.fileSourceId); out.writeLEUnsignedShort(this.globalEncoding); out.write(this.projectId); out.write((byte)this.version.getMajor()); out.write((byte)this.version.getMinor()); // out.writeString("TRANSFORMATION", 32); // System Identifier // out.writeString("RevolutionGis", 32); // Generating Software out.writeString(this.systemIdentifier, 32); // System Identifier out.writeString(this.generatingSoftware, 32); // Generating Software out.writeLEUnsignedShort(this.dayOfYear); out.writeLEUnsignedShort(this.year); int headerSize = 227; if (this.version.atLeast(LasPointCloudHeader.VERSION_1_3)) { headerSize += 8; if (this.version.atLeast(LasPointCloudHeader.VERSION_1_4)) { headerSize += 140; } } out.writeLEUnsignedShort(headerSize); final int numberOfVariableLengthRecords = this.lasProperties.size(); int variableLengthRecordsSize = 0; for (final LasVariableLengthRecord record : this.lasProperties.values()) { variableLengthRecordsSize += 54 + record.getBytes().length; } final long offsetToPointData = headerSize + variableLengthRecordsSize; out.writeLEUnsignedInt(offsetToPointData); out.writeLEUnsignedInt(numberOfVariableLengthRecords); final int pointFormatId = this.pointFormat.getId(); out.write(pointFormatId); out.writeLEUnsignedShort(this.recordLength); if (this.pointCount > MAX_UNSIGNED_INT) { out.writeLEUnsignedInt(0); } else { out.writeLEUnsignedInt(this.pointCount); } for (int i = 0; i < 5; i++) { final long count = this.pointCountByReturn[i]; if (count > MAX_UNSIGNED_INT) { out.writeLEUnsignedInt(0); } else { out.writeLEUnsignedInt(count); } } for (int axisIndex = 0; axisIndex < 3; axisIndex++) { final double resolution = this.geometryFactory.getResolution(axisIndex); out.writeLEDouble(resolution); } for (int axisIndex = 0; axisIndex < 3; axisIndex++) { final double offset = this.geometryFactory.getOffset(axisIndex); out.writeLEDouble(offset); } for (int axisIndex = 0; axisIndex < 3; axisIndex++) { final double max = this.bounds[3 + axisIndex]; out.writeLEDouble(max); final double min = this.bounds[axisIndex]; out.writeLEDouble(min); } if (this.version.atLeast(LasPointCloudHeader.VERSION_1_3)) { out.writeLEUnsignedLong(0); // startOfWaveformDataPacketRecord if (this.version.atLeast(LasPointCloudHeader.VERSION_1_4)) { out.writeLEUnsignedLong(0); // startOfFirstExetendedDataRecord out.writeLEUnsignedInt(0); // numberOfExtendedVariableLengthRecords out.writeLEUnsignedLong(this.pointCount); for (int i = 0; i < 15; i++) { final long count = this.pointCountByReturn[i]; out.writeLEUnsignedLong(count); } } } for (final LasVariableLengthRecord record : this.lasProperties.values()) { out.writeLEUnsignedShort(0); final String userId = record.getUserId(); out.writeString(userId, 16); final int recordId = record.getRecordId(); out.writeLEUnsignedShort(recordId); final int valueLength = record.getValueLength(); out.writeLEUnsignedShort(valueLength); final String description = record.getDescription(); out.writeString(description, 32); final byte[] bytes = record.getBytes(); out.write(bytes); } } }