diff --git a/citydb-cli/src/main/java/org/citydb/cli/common/TransformOptions.java b/citydb-cli/src/main/java/org/citydb/cli/common/TransformOptions.java new file mode 100644 index 00000000..becb1064 --- /dev/null +++ b/citydb-cli/src/main/java/org/citydb/cli/common/TransformOptions.java @@ -0,0 +1,67 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2025 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citydb.cli.common; + +import org.citydb.model.common.Matrix3x4; +import picocli.CommandLine; + +import java.util.Arrays; + +public class TransformOptions implements Option { + @CommandLine.Option(names = "--transform", paramLabel = "", + description = "Transform coordinates using a 3x4 matrix in row-major order. Use 'swap-xy' as a shortcut.") + private String transform; + + private Matrix3x4 transformationMatrix; + + public Matrix3x4 getTransformationMatrix() { + return transformationMatrix; + } + + @Override + public void preprocess(CommandLine commandLine) throws Exception { + if (transform != null) { + if (transform.equalsIgnoreCase("swap-xy")) { + transformationMatrix = Matrix3x4.ofRowMajor( + 0, 1, 0, 0, + 1, 0, 0, 0, + 0, 0, 1, 0); + } else { + String[] values = transform.split(","); + if (values.length == 12) { + try { + transformationMatrix = Matrix3x4.ofRowMajor(Arrays.stream(values) + .map(Double::parseDouble) + .toList()); + } catch (NumberFormatException e) { + throw new CommandLine.ParameterException(commandLine, + "Error: The elements of a 3x4 matrix must be floating point numbers but were '" + + String.join(",", values) + "'"); + } + } else { + throw new CommandLine.ParameterException(commandLine, + "Error: A 3x4 matrix must be in M0,M1,...,M11 format but was '" + transform + "'"); + } + } + } + } +} diff --git a/citydb-cli/src/main/java/org/citydb/cli/exporter/ExportController.java b/citydb-cli/src/main/java/org/citydb/cli/exporter/ExportController.java index 24add67e..a853a840 100644 --- a/citydb-cli/src/main/java/org/citydb/cli/exporter/ExportController.java +++ b/citydb-cli/src/main/java/org/citydb/cli/exporter/ExportController.java @@ -81,6 +81,9 @@ public abstract class ExportController implements Command { @CommandLine.Mixin protected CrsOptions crsOptions; + @CommandLine.Mixin + protected TransformOptions transformOptions; + @CommandLine.ArgGroup(exclusive = false, order = Integer.MAX_VALUE, heading = "Query and filter options:%n") protected QueryOptions queryOptions; @@ -263,6 +266,10 @@ protected ExportOptions getExportOptions() throws ExecutionException { exportOptions.setTargetSrs(crsOptions.getTargetSrs()); } + if (transformOptions != null) { + exportOptions.setAffineTransform(transformOptions.getTransformationMatrix()); + } + if (queryOptions != null) { if (queryOptions.getLodOptions() != null) { exportOptions.setLodOptions(queryOptions.getLodOptions().getExportLodOptions()); diff --git a/citydb-cli/src/main/java/org/citydb/cli/importer/ImportController.java b/citydb-cli/src/main/java/org/citydb/cli/importer/ImportController.java index 5c46de1b..66a74990 100644 --- a/citydb-cli/src/main/java/org/citydb/cli/importer/ImportController.java +++ b/citydb-cli/src/main/java/org/citydb/cli/importer/ImportController.java @@ -85,6 +85,9 @@ enum Mode {import_all, skip, delete, terminate} description = "Compute and overwrite extents of features.") protected Boolean computeEnvelopes; + @CommandLine.Mixin + protected TransformOptions transformOptions; + @CommandLine.ArgGroup(exclusive = false, order = Integer.MAX_VALUE, heading = "Database connection options:%n") protected ConnectionOptions connectionOptions; @@ -295,6 +298,10 @@ protected ImportOptions getImportOptions() throws ExecutionException { importOptions.setNumberOfThreads(threadsOptions.getNumberOfThreads()); } + if (transformOptions != null) { + importOptions.setAffineTransform(transformOptions.getTransformationMatrix()); + } + return importOptions; } diff --git a/citydb-io-citygml/src/main/java/org/citydb/io/citygml/adapter/appearance/GeoreferencedTextureAdapter.java b/citydb-io-citygml/src/main/java/org/citydb/io/citygml/adapter/appearance/GeoreferencedTextureAdapter.java index 9615d8bb..391278b5 100644 --- a/citydb-io-citygml/src/main/java/org/citydb/io/citygml/adapter/appearance/GeoreferencedTextureAdapter.java +++ b/citydb-io-citygml/src/main/java/org/citydb/io/citygml/adapter/appearance/GeoreferencedTextureAdapter.java @@ -86,7 +86,7 @@ public void serialize(GeoreferencedTexture source, org.citygml4j.core.model.appe target.setReferencePoint(new PointProperty(helper.getPoint(referencePoint, false)))); source.getOrientation().ifPresent(transformationMatrix -> - target.setOrientation(TransformationMatrix2x2.ofRowMajorList(transformationMatrix))); + target.setOrientation(TransformationMatrix2x2.ofRowMajorList(transformationMatrix.toRowMajor()))); } private void processWorldFile(org.citygml4j.core.model.appearance.GeoreferencedTexture source, ExternalFile textureImage, ModelBuilderHelper helper) throws ModelBuildException { diff --git a/citydb-io-citygml/src/main/java/org/citydb/io/citygml/adapter/appearance/serializer/AppearanceHelper.java b/citydb-io-citygml/src/main/java/org/citydb/io/citygml/adapter/appearance/serializer/AppearanceHelper.java index c675707a..1b6b545d 100644 --- a/citydb-io-citygml/src/main/java/org/citydb/io/citygml/adapter/appearance/serializer/AppearanceHelper.java +++ b/citydb-io-citygml/src/main/java/org/citydb/io/citygml/adapter/appearance/serializer/AppearanceHelper.java @@ -28,6 +28,7 @@ import org.citydb.model.appearance.SurfaceDataProperty; import org.citydb.model.appearance.TextureCoordinate; import org.citydb.model.common.Child; +import org.citydb.model.common.Matrix3x4; import org.citydb.model.common.Reference; import org.citydb.model.geometry.Geometry; import org.citydb.model.geometry.LinearRing; @@ -146,13 +147,13 @@ private void addTextureTargets(Map> mapping, "#" + k, new AbstractTextureParameterizationProperty(v))))); } - public void addWorldToTextureTargets(Map, List> mapping, ParameterizedTexture target, Set geometries) { + public void addWorldToTextureTargets(Map, Matrix3x4> mapping, ParameterizedTexture target, Set geometries) { Set targets = new HashSet<>(); mapping.forEach((k, v) -> { String surfaceId = k.getOrCreateObjectId(); if (geometries.contains(surfaceId) && targets.add(surfaceId)) { TexCoordGen texCoordGen = new TexCoordGen(); - texCoordGen.setWorldToTexture(TransformationMatrix3x4.ofRowMajorList(v)); + texCoordGen.setWorldToTexture(TransformationMatrix3x4.ofRowMajorList(v.toRowMajor())); target.getTextureParameterizations().add(new TextureAssociationProperty(new TextureAssociation( "#" + surfaceId, new AbstractTextureParameterizationProperty(texCoordGen)))); } diff --git a/citydb-io-citygml/src/main/java/org/citydb/io/citygml/adapter/core/ImplicitGeometryAdapter.java b/citydb-io-citygml/src/main/java/org/citydb/io/citygml/adapter/core/ImplicitGeometryAdapter.java index 53edd34c..11700970 100644 --- a/citydb-io-citygml/src/main/java/org/citydb/io/citygml/adapter/core/ImplicitGeometryAdapter.java +++ b/citydb-io-citygml/src/main/java/org/citydb/io/citygml/adapter/core/ImplicitGeometryAdapter.java @@ -66,7 +66,8 @@ public org.citygml4j.core.model.core.ImplicitGeometry createObject(ImplicitGeome @Override public void serialize(ImplicitGeometryProperty source, org.citygml4j.core.model.core.ImplicitGeometry target, ModelSerializerHelper helper) throws ModelSerializeException { source.getTransformationMatrix().ifPresent(transformationMatrix -> - target.setTransformationMatrix(TransformationMatrix4x4.ofRowMajorList(transformationMatrix))); + target.setTransformationMatrix(TransformationMatrix4x4.ofRowMajorList( + transformationMatrix.toRowMajor()))); source.getReferencePoint().ifPresent(referencePoint -> target.setReferencePoint(new PointProperty(helper.getPoint(referencePoint)))); diff --git a/citydb-model/src/main/java/module-info.java b/citydb-model/src/main/java/module-info.java index 7288afd3..26c00ad5 100644 --- a/citydb-model/src/main/java/module-info.java +++ b/citydb-model/src/main/java/module-info.java @@ -9,5 +9,6 @@ exports org.citydb.model.geometry; exports org.citydb.model.property; exports org.citydb.model.util; + exports org.citydb.model.util.matrix; exports org.citydb.model.walker; } \ No newline at end of file diff --git a/citydb-model/src/main/java/org/citydb/model/appearance/GeoreferencedTexture.java b/citydb-model/src/main/java/org/citydb/model/appearance/GeoreferencedTexture.java index 749b24cc..87f45416 100644 --- a/citydb-model/src/main/java/org/citydb/model/appearance/GeoreferencedTexture.java +++ b/citydb-model/src/main/java/org/citydb/model/appearance/GeoreferencedTexture.java @@ -21,6 +21,7 @@ package org.citydb.model.appearance; +import org.citydb.model.common.Matrix2x2; import org.citydb.model.common.Name; import org.citydb.model.common.Namespaces; import org.citydb.model.common.Visitor; @@ -33,7 +34,7 @@ public class GeoreferencedTexture extends Texture { private Point referencePoint; - private List orientation; + private Matrix2x2 orientation; private List> targets; private GeoreferencedTexture() { @@ -57,14 +58,19 @@ public GeoreferencedTexture setReferencePoint(Point referencePoint) { return this; } - public Optional> getOrientation() { + public Optional getOrientation() { return Optional.ofNullable(orientation); } + public GeoreferencedTexture setOrientation(Matrix2x2 orientation) { + this.orientation = orientation; + return this; + } + public GeoreferencedTexture setOrientation(List orientation) { - this.orientation = orientation != null && orientation.size() > 3 ? - new ArrayList<>(orientation.subList(0, 4)) : - null; + if (orientation != null && orientation.size() > 3) { + this.orientation = Matrix2x2.ofRowMajor(orientation); + } return this; } diff --git a/citydb-model/src/main/java/org/citydb/model/appearance/ParameterizedTexture.java b/citydb-model/src/main/java/org/citydb/model/appearance/ParameterizedTexture.java index eac11820..a2f1d628 100644 --- a/citydb-model/src/main/java/org/citydb/model/appearance/ParameterizedTexture.java +++ b/citydb-model/src/main/java/org/citydb/model/appearance/ParameterizedTexture.java @@ -21,6 +21,7 @@ package org.citydb.model.appearance; +import org.citydb.model.common.Matrix3x4; import org.citydb.model.common.Name; import org.citydb.model.common.Namespaces; import org.citydb.model.common.Visitor; @@ -31,7 +32,7 @@ public class ParameterizedTexture extends Texture { private Map> textureCoordinates; - private Map, List> worldToTextureMappings; + private Map, Matrix3x4> worldToTextureMappings; private ParameterizedTexture() { } @@ -62,12 +63,15 @@ public List getTextureCoordinates(LinearRing linearRing) { } public ParameterizedTexture addTextureCoordinates(LinearRing linearRing, List textureCoordinates) { - Objects.requireNonNull(linearRing, "The linear ring must not be null."); - if (linearRing.getParent().isEmpty()) { - throw new IllegalArgumentException("The linear ring must belong to a target polygon."); + if (textureCoordinates != null) { + Objects.requireNonNull(linearRing, "The linear ring must not be null."); + if (linearRing.getParent().isEmpty()) { + throw new IllegalArgumentException("The linear ring must belong to a target polygon."); + } + + getTextureCoordinates().put(linearRing, textureCoordinates); } - getTextureCoordinates().put(linearRing, textureCoordinates); return this; } @@ -75,7 +79,7 @@ public boolean hasWorldToTextureMappings() { return worldToTextureMappings != null && !worldToTextureMappings.isEmpty(); } - public Map, List> getWorldToTextureMappings() { + public Map, Matrix3x4> getWorldToTextureMappings() { if (worldToTextureMappings == null) { worldToTextureMappings = new IdentityHashMap<>(); } @@ -83,14 +87,22 @@ public Map, List> getWorldToTextureMappings() { return worldToTextureMappings; } - public List getWorldToTextureMapping(Surface surface) { + public Matrix3x4 getWorldToTextureMapping(Surface surface) { return worldToTextureMappings != null ? worldToTextureMappings.get(surface) : null; } + public ParameterizedTexture addWorldToTextureMapping(Surface surface, Matrix3x4 transformationMatrix) { + if (transformationMatrix != null) { + Objects.requireNonNull(surface, "The surface geometry must not be null."); + getWorldToTextureMappings().put(surface, transformationMatrix); + } + + return this; + } + public ParameterizedTexture addWorldToTextureMapping(Surface surface, List transformationMatrix) { - Objects.requireNonNull(surface, "The surface geometry must not be null."); if (transformationMatrix != null && transformationMatrix.size() > 11) { - getWorldToTextureMappings().put(surface, transformationMatrix.subList(0, 12)); + addWorldToTextureMapping(surface, Matrix3x4.ofRowMajor(transformationMatrix)); } return this; diff --git a/citydb-model/src/main/java/org/citydb/model/common/Matrix2x2.java b/citydb-model/src/main/java/org/citydb/model/common/Matrix2x2.java new file mode 100644 index 00000000..4c04c5ef --- /dev/null +++ b/citydb-model/src/main/java/org/citydb/model/common/Matrix2x2.java @@ -0,0 +1,69 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2025 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citydb.model.common; + +import org.citydb.model.util.matrix.Matrix; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class Matrix2x2 extends Matrix { + + private Matrix2x2() { + super(2, 2); + } + + private Matrix2x2(Matrix matrix) { + super(matrix.getElements(), 2, 2); + } + + public static Matrix2x2 newInstance() { + return new Matrix2x2(); + } + + public static Matrix2x2 of(Matrix matrix) { + Objects.requireNonNull(matrix, "The matrix must not be null."); + int rows = matrix.getRows(); + int columns = matrix.getColumns(); + return new Matrix2x2(rows != 2 || columns != 2 ? + Matrix.identity(2, 2).setSubMatrix(0, Math.min(rows, 2) - 1, 0, Math.min(columns, 2) - 1, matrix) : + matrix); + } + + public static Matrix2x2 ofRowMajor(List values) { + Objects.requireNonNull(values, "The matrix values must not be null."); + if (values.size() > 3) { + return new Matrix2x2(new Matrix(values.subList(0, 4), 2)); + } else { + throw new IllegalArgumentException("A 2x2 matrix requires 4 values."); + } + } + + public static Matrix2x2 ofRowMajor(double... values) { + return ofRowMajor(values != null ? Arrays.stream(values).boxed().toList() : null); + } + + public static Matrix2x2 identity() { + return new Matrix2x2(Matrix.identity(2, 2)); + } +} diff --git a/citydb-model/src/main/java/org/citydb/model/common/Matrix3x4.java b/citydb-model/src/main/java/org/citydb/model/common/Matrix3x4.java new file mode 100644 index 00000000..7344dcf1 --- /dev/null +++ b/citydb-model/src/main/java/org/citydb/model/common/Matrix3x4.java @@ -0,0 +1,71 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2025 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citydb.model.common; + +import org.citydb.model.util.matrix.Matrix; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class Matrix3x4 extends Matrix { + + private Matrix3x4() { + super(3, 4); + } + + private Matrix3x4(Matrix matrix) { + super(matrix.getElements(), 3, 4); + } + + public static Matrix3x4 newInstance() { + return new Matrix3x4(); + } + + public static Matrix3x4 of(Matrix matrix) { + Objects.requireNonNull(matrix, "The matrix must not be null."); + int rows = matrix.getRows(); + int columns = matrix.getColumns(); + return new Matrix3x4(rows != 3 || columns != 4 ? + Matrix.identity(4, 4) + .setSubMatrix(0, Math.min(rows, 3) - 1, 0, Math.min(columns, 4) - 1, matrix) + .getSubMatrix(0, 2, 0, 3) : + matrix); + } + + public static Matrix3x4 ofRowMajor(List values) { + Objects.requireNonNull(values, "The matrix values must not be null."); + if (values.size() > 11) { + return new Matrix3x4(new Matrix(values.subList(0, 12), 3)); + } else { + throw new IllegalArgumentException("A 3x4 matrix requires 12 values."); + } + } + + public static Matrix3x4 ofRowMajor(double... values) { + return ofRowMajor(values != null ? Arrays.stream(values).boxed().toList() : null); + } + + public static Matrix3x4 identity() { + return new Matrix3x4(Matrix.identity(3, 4)); + } +} diff --git a/citydb-model/src/main/java/org/citydb/model/common/Matrix4x4.java b/citydb-model/src/main/java/org/citydb/model/common/Matrix4x4.java new file mode 100644 index 00000000..009c5912 --- /dev/null +++ b/citydb-model/src/main/java/org/citydb/model/common/Matrix4x4.java @@ -0,0 +1,69 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2025 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citydb.model.common; + +import org.citydb.model.util.matrix.Matrix; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class Matrix4x4 extends Matrix { + + private Matrix4x4() { + super(4, 4); + } + + private Matrix4x4(Matrix matrix) { + super(matrix.getElements(), 4, 4); + } + + public static Matrix4x4 newInstance() { + return new Matrix4x4(); + } + + public static Matrix4x4 of(Matrix matrix) { + Objects.requireNonNull(matrix, "The matrix must not be null."); + int rows = matrix.getRows(); + int columns = matrix.getColumns(); + return new Matrix4x4(rows != 4 || columns != 4 ? + Matrix.identity(4, 4).setSubMatrix(0, Math.min(rows, 4) - 1, 0, Math.min(columns, 4) - 1, matrix) : + matrix); + } + + public static Matrix4x4 ofRowMajor(List values) { + Objects.requireNonNull(values, "The matrix values must not be null."); + if (values.size() > 15) { + return new Matrix4x4(new Matrix(values.subList(0, 16), 4)); + } else { + throw new IllegalArgumentException("A 4x4 matrix requires 16 values."); + } + } + + public static Matrix4x4 ofRowMajor(double... values) { + return ofRowMajor(values != null ? Arrays.stream(values).boxed().toList() : null); + } + + public static Matrix4x4 identity() { + return new Matrix4x4(Matrix.identity(4, 4)); + } +} diff --git a/citydb-model/src/main/java/org/citydb/model/encoding/Matrix3x4Reader.java b/citydb-model/src/main/java/org/citydb/model/encoding/Matrix3x4Reader.java new file mode 100644 index 00000000..a8fbb2b5 --- /dev/null +++ b/citydb-model/src/main/java/org/citydb/model/encoding/Matrix3x4Reader.java @@ -0,0 +1,50 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2025 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citydb.model.encoding; + +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.reader.ObjectReader; +import org.citydb.model.common.Matrix3x4; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +public class Matrix3x4Reader implements ObjectReader { + @Override + public Matrix3x4 readObject(JSONReader jsonReader, Type type, Object o, long l) { + if (jsonReader.isArray()) { + List values = new ArrayList<>(); + for (Object value : jsonReader.readArray()) { + if (value instanceof Number number) { + values.add(number.doubleValue()); + } + } + + if (values.size() > 11) { + return Matrix3x4.ofRowMajor(values); + } + } + + return null; + } +} diff --git a/citydb-model/src/main/java/org/citydb/model/encoding/Matrix3x4Writer.java b/citydb-model/src/main/java/org/citydb/model/encoding/Matrix3x4Writer.java new file mode 100644 index 00000000..f600db8c --- /dev/null +++ b/citydb-model/src/main/java/org/citydb/model/encoding/Matrix3x4Writer.java @@ -0,0 +1,48 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2025 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citydb.model.encoding; + +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.writer.ObjectWriter; +import org.citydb.model.common.Matrix3x4; + +import java.lang.reflect.Type; +import java.util.List; + +public class Matrix3x4Writer implements ObjectWriter { + @Override + public void write(JSONWriter jsonWriter, Object o, Object o1, Type type, long l) { + if (o instanceof Matrix3x4 matrix) { + jsonWriter.startArray(); + List values = matrix.toRowMajor(); + for (int i = 0; i < values.size(); i++) { + if (i != 0) { + jsonWriter.writeComma(); + } + jsonWriter.writeDouble(values.get(i)); + } + jsonWriter.endArray(); + } else { + jsonWriter.writeNull(); + } + } +} diff --git a/citydb-model/src/main/java/org/citydb/model/geometry/ImplicitGeometry.java b/citydb-model/src/main/java/org/citydb/model/geometry/ImplicitGeometry.java index e9d21085..85a3efce 100644 --- a/citydb-model/src/main/java/org/citydb/model/geometry/ImplicitGeometry.java +++ b/citydb-model/src/main/java/org/citydb/model/geometry/ImplicitGeometry.java @@ -23,6 +23,8 @@ import org.citydb.model.common.*; import org.citydb.model.property.AppearanceProperty; +import org.citydb.model.util.AffineTransformer; +import org.citydb.model.util.matrix.Matrix; import java.util.List; import java.util.Objects; @@ -116,41 +118,30 @@ public ImplicitGeometry addAppearance(AppearanceProperty appearance) { return this; } - public Envelope getEnvelope(List transformationMatrix, Point referencePoint) { - if (geometry != null - && transformationMatrix != null - && transformationMatrix.size() > 15 - && referencePoint != null) { - double[][] matrix = new double[4][4]; - for (int i = 0; i < 4; i++) { - for (int j = 0; j < 4; j++) { - matrix[i][j] = transformationMatrix.get(i * 4 + j); - } + public Envelope getEnvelope(Matrix4x4 transformationMatrix, Point referencePoint) { + if (transformationMatrix != null && referencePoint != null) { + Envelope envelope; + if (geometry != null) { + envelope = geometry.getEnvelope(); + AffineTransformer.of(transformationMatrix.plus(new Matrix(4, 4) + .set(0, 3, referencePoint.getCoordinate().getX()) + .set(1, 3, referencePoint.getCoordinate().getY()) + .set(2, 3, referencePoint.getCoordinate().getZ()))) + .transform(envelope); + } else { + envelope = Envelope.empty().include(Point.of(Coordinate.of( + referencePoint.getCoordinate().getX() + transformationMatrix.get(0, 3), + referencePoint.getCoordinate().getY() + transformationMatrix.get(1, 3), + referencePoint.getCoordinate().getZ() + transformationMatrix.get(2, 3)))); } - matrix[0][3] += referencePoint.getCoordinate().getX(); - matrix[1][3] += referencePoint.getCoordinate().getY(); - matrix[2][3] += referencePoint.getCoordinate().getZ(); - - Envelope template = geometry.getEnvelope(); - return Envelope.of( - multiply(matrix, template.getLowerCorner()), - multiply(matrix, template.getUpperCorner())) - .setSRID(referencePoint.getSRID().orElse(null)) + return envelope.setSRID(referencePoint.getSRID().orElse(null)) .setSrsIdentifier(referencePoint.getSrsIdentifier().orElse(null)); } else { return null; } } - private Coordinate multiply(double[][] matrix, Coordinate coordinate) { - double[] v = new double[]{coordinate.getX(), coordinate.getY(), coordinate.getZ(), 1}; - return Coordinate.of( - matrix[0][0] * v[0] + matrix[0][1] * v[1] + matrix[0][2] * v[2] + matrix[0][3] * v[3], - matrix[1][0] * v[0] + matrix[1][1] * v[1] + matrix[1][2] * v[2] + matrix[1][3] * v[3], - matrix[2][0] * v[0] + matrix[2][1] * v[1] + matrix[2][2] * v[2] + matrix[2][3] * v[3]); - } - @Override public void accept(Visitor visitor) { visitor.visit(this); diff --git a/citydb-model/src/main/java/org/citydb/model/property/ImplicitGeometryProperty.java b/citydb-model/src/main/java/org/citydb/model/property/ImplicitGeometryProperty.java index 4ef79995..12ddf407 100644 --- a/citydb-model/src/main/java/org/citydb/model/property/ImplicitGeometryProperty.java +++ b/citydb-model/src/main/java/org/citydb/model/property/ImplicitGeometryProperty.java @@ -21,15 +21,11 @@ package org.citydb.model.property; -import org.citydb.model.common.Child; -import org.citydb.model.common.InlineOrByReferenceProperty; -import org.citydb.model.common.Name; -import org.citydb.model.common.Reference; +import org.citydb.model.common.*; import org.citydb.model.feature.Feature; import org.citydb.model.geometry.ImplicitGeometry; import org.citydb.model.geometry.Point; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -37,7 +33,7 @@ public class ImplicitGeometryProperty extends Property implements InlineOrByReferenceProperty { private ImplicitGeometry implicitGeometry; private Reference reference; - private List transformationMatrix; + private Matrix4x4 transformationMatrix; private Point referencePoint; private String lod; @@ -93,14 +89,19 @@ public ImplicitGeometryProperty setReference(Reference reference) { return this; } - public Optional> getTransformationMatrix() { + public Optional getTransformationMatrix() { return Optional.ofNullable(transformationMatrix); } + public ImplicitGeometryProperty setTransformationMatrix(Matrix4x4 transformationMatrix) { + this.transformationMatrix = transformationMatrix; + return this; + } + public ImplicitGeometryProperty setTransformationMatrix(List transformationMatrix) { - this.transformationMatrix = transformationMatrix != null && transformationMatrix.size() > 15 ? - new ArrayList<>(transformationMatrix.subList(0, 16)) : - null; + if (transformationMatrix != null && transformationMatrix.size() > 15) { + this.transformationMatrix = Matrix4x4.ofRowMajor(transformationMatrix); + } return this; } diff --git a/citydb-model/src/main/java/org/citydb/model/util/AffineTransformer.java b/citydb-model/src/main/java/org/citydb/model/util/AffineTransformer.java new file mode 100644 index 00000000..f2c637d1 --- /dev/null +++ b/citydb-model/src/main/java/org/citydb/model/util/AffineTransformer.java @@ -0,0 +1,166 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citydb.model.util; + +import org.citydb.model.appearance.GeoreferencedTexture; +import org.citydb.model.appearance.ParameterizedTexture; +import org.citydb.model.common.Matrix2x2; +import org.citydb.model.common.Matrix3x4; +import org.citydb.model.common.Matrix4x4; +import org.citydb.model.common.Visitable; +import org.citydb.model.feature.Feature; +import org.citydb.model.geometry.*; +import org.citydb.model.property.AppearanceProperty; +import org.citydb.model.property.ImplicitGeometryProperty; +import org.citydb.model.util.matrix.Matrix; +import org.citydb.model.walker.ModelWalker; + +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class AffineTransformer { + private final Matrix matrix; + private final Matrix inverse; + private final Processor processor = new Processor(); + + private AffineTransformer(Matrix matrix) { + this.matrix = matrix; + inverse = matrix.invert(); + } + + public static AffineTransformer of(Matrix matrix) { + return new AffineTransformer(Matrix4x4.of(matrix)); + } + + public static AffineTransformer ofRowMajor(List values) { + return new AffineTransformer(Matrix4x4.ofRowMajor(values)); + } + + public static AffineTransformer ofRowMajor(List values, int rows) { + Objects.requireNonNull(values, "The matrix values must not be null."); + return of(new Matrix(values, rows)); + } + + public void transform(Coordinate coordinate) { + Matrix transformed = matrix.times(new Matrix(new double[][]{ + {coordinate.getX()}, + {coordinate.getY()}, + {coordinate.getZ()}, + {1} + })); + + coordinate.setX(transformed.get(0, 0)) + .setY(transformed.get(1, 0)); + if (coordinate.getDimension() == 3) { + coordinate.setZ(transformed.get(2, 0)); + } + } + + public void transform(Feature feature) { + feature.accept(processor); + } + + public void transform(Geometry geometry) { + geometry.accept(processor); + } + + public void transform(Visitable visitable) { + visitable.accept(processor); + } + + public void transform(Envelope envelope) { + if (!envelope.isEmpty()) { + transform(envelope.getLowerCorner()); + transform(envelope.getUpperCorner()); + } + } + + private class Processor extends ModelWalker { + @Override + public void visit(Feature feature) { + feature.getEnvelope().ifPresent(AffineTransformer.this::transform); + super.visit(feature); + } + + @Override + public void visit(GeoreferencedTexture texture) { + texture.getOrientation().ifPresent(orientation -> texture.setOrientation( + Matrix2x2.of(orientation.times(inverse.getSubMatrix(0, 1, 0, 1))))); + super.visit(texture); + } + + @Override + public void visit(ParameterizedTexture texture) { + if (texture.hasWorldToTextureMappings()) { + Map, Matrix3x4> mappings = new IdentityHashMap<>(texture.getWorldToTextureMappings()); + mappings.forEach((surface, transformationMatrix) -> texture.addWorldToTextureMapping(surface, + Matrix3x4.of(Matrix.identity(4, 4) + .setSubMatrix(0, 2, 0, 3, transformationMatrix) + .times(inverse)))); + } + } + + @Override + public void visit(ImplicitGeometryProperty property) { + property.getTransformationMatrix().ifPresent(transformationMatrix -> property.setTransformationMatrix( + Matrix4x4.of(matrix.copy() + .set(0, 3, 0) + .set(1, 3, 0) + .set(2, 3, 0) + .times(transformationMatrix)))); + super.visit(property); + } + + @Override + public void visit(ImplicitGeometry implicitGeometry) { + if (implicitGeometry.hasAppearances()) { + for (AppearanceProperty property : implicitGeometry.getAppearances().getAll()) { + visit(property); + } + } + } + + @Override + public void visit(Point point) { + AffineTransformer.this.transform(point.getCoordinate()); + } + + @Override + public void visit(LineString lineString) { + lineString.getPoints().forEach(AffineTransformer.this::transform); + } + + @Override + public void visit(Polygon polygon) { + transform(polygon.getExteriorRing()); + if (polygon.hasInteriorRings()) { + polygon.getInteriorRings().forEach(this::transform); + } + } + + private void transform(LinearRing ring) { + ring.getPoints().forEach(AffineTransformer.this::transform); + } + } +} diff --git a/citydb-model/src/main/java/org/citydb/model/util/matrix/LUDecomposition.java b/citydb-model/src/main/java/org/citydb/model/util/matrix/LUDecomposition.java new file mode 100644 index 00000000..05cc1a90 --- /dev/null +++ b/citydb-model/src/main/java/org/citydb/model/util/matrix/LUDecomposition.java @@ -0,0 +1,125 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citydb.model.util.matrix; + +public class LUDecomposition { + private final double[][] elements; + private final int rows, columns; + private final int[] piv; + + LUDecomposition(Matrix matrix) { + elements = matrix.copy().elements; + rows = matrix.rows; + columns = matrix.columns; + + piv = new int[rows]; + for (int i = 0; i < rows; i++) { + piv[i] = i; + } + + int pivsign = 1; + double[] column = new double[rows]; + + for (int j = 0; j < columns; j++) { + for (int i = 0; i < rows; i++) { + column[i] = elements[i][j]; + } + + for (int i = 0; i < rows; i++) { + int kmax = Math.min(i, j); + double s = 0; + for (int k = 0; k < kmax; k++) { + s += elements[i][k] * column[k]; + } + + elements[i][j] = column[i] -= s; + } + + int p = j; + for (int i = j + 1; i < rows; i++) { + if (Math.abs(column[i]) > Math.abs(column[p])) { + p = i; + } + } + + if (p != j) { + for (int k = 0; k < columns; k++) { + double t = elements[p][k]; + elements[p][k] = elements[j][k]; + elements[j][k] = t; + } + + int k = piv[p]; + piv[p] = piv[j]; + piv[j] = k; + pivsign = -pivsign; + } + + if (j < rows & elements[j][j] != 0) { + for (int i = j + 1; i < rows; i++) { + elements[i][j] /= elements[j][j]; + } + } + } + } + + Matrix solve(Matrix matrix) { + if (matrix.rows != rows) { + throw new IllegalArgumentException("Matrix row dimensions must agree."); + } else if (!isNonSingular()) { + throw new RuntimeException("Matrix is singular."); + } + + Matrix result = matrix.getSubMatrix(piv, 0, matrix.columns - 1); + for (int k = 0; k < columns; k++) { + for (int i = k + 1; i < columns; i++) { + for (int j = 0; j < matrix.columns; j++) { + result.elements[i][j] -= result.elements[k][j] * elements[i][k]; + } + } + } + + for (int k = columns - 1; k >= 0; k--) { + for (int j = 0; j < matrix.columns; j++) { + result.elements[k][j] /= elements[k][k]; + } + + for (int i = 0; i < k; i++) { + for (int j = 0; j < matrix.columns; j++) { + result.elements[i][j] -= result.elements[k][j] * elements[i][k]; + } + } + } + + return result; + } + + private boolean isNonSingular() { + for (int j = 0; j < columns; j++) { + if (elements[j][j] == 0) { + return false; + } + } + + return true; + } +} diff --git a/citydb-model/src/main/java/org/citydb/model/util/matrix/Matrix.java b/citydb-model/src/main/java/org/citydb/model/util/matrix/Matrix.java new file mode 100644 index 00000000..94914074 --- /dev/null +++ b/citydb-model/src/main/java/org/citydb/model/util/matrix/Matrix.java @@ -0,0 +1,390 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citydb.model.util.matrix; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Matrix implements Serializable { + final double[][] elements; + final int rows, columns; + + protected Matrix(double[][] elements, int rows, int columns) { + this.elements = elements; + this.rows = rows; + this.columns = columns; + } + + public Matrix(int rows, int columns) { + this(new double[rows][columns], rows, columns); + } + + public Matrix(Matrix other) { + this(other.rows, other.columns); + for (int i = 0; i < rows; ++i) { + System.arraycopy(other.elements[i], 0, this.elements[i], 0, columns); + } + } + + public Matrix(double[][] elements) { + for (int i = 1; i < elements.length; i++) { + if (elements[i].length != elements[0].length) { + throw new IllegalArgumentException("All rows must have the same length."); + } + } + + rows = elements.length; + columns = elements[0].length; + this.elements = elements; + } + + public Matrix(double[] rowMajor, int rows) { + this.rows = rows; + columns = rows != 0 ? rowMajor.length / rows : 0; + if (rows * columns != rowMajor.length) { + throw new IllegalArgumentException("Array length must be a multiple of m."); + } + + elements = new double[rows][columns]; + for (int i = 0; i < rows; i++) { + System.arraycopy(rowMajor, i * columns, elements[i], 0, columns); + } + } + + public Matrix(List rowMajor, int rows) { + this(rowMajor.stream() + .map(value -> value != null ? value : 0) + .mapToDouble(Double::doubleValue) + .toArray(), rows); + } + + public static Matrix identity(int rows, int columns) { + Matrix matrix = new Matrix(rows, columns); + for (int i = 0; i < rows; i++) { + matrix.elements[i][i] = 1; + } + + return matrix; + } + + public double[][] getElements() { + return elements; + } + + public List toColumnMajor() { + List values = new ArrayList<>(rows * columns); + for (int i = 0; i < columns; ++i) { + for (int j = 0; j < rows; ++j) { + values.add(elements[j][i]); + } + } + + return values; + } + + public List toRowMajor() { + List values = new ArrayList<>(rows * columns); + for (double[] row : elements) { + for (double value : row) { + values.add(value); + } + } + + return values; + } + + public int getRows() { + return rows; + } + + public int getColumns() { + return columns; + } + + public double get(int row, int column) { + return elements[row][column]; + } + + public Matrix set(int row, int column, double value) { + elements[row][column] = value; + return this; + } + + public Matrix getSubMatrix(int rowStart, int rowEnd, int columnStart, int columnEnd) { + Matrix result = new Matrix(rowEnd - rowStart + 1, columnEnd - columnStart + 1); + for (int i = rowStart; i <= rowEnd; i++) { + System.arraycopy(elements[i], columnStart, result.elements[i - rowStart], 0, columnEnd - columnStart + 1); + } + + return result; + } + + Matrix getSubMatrix(int[] rowIndices, int columnStart, int columnEnd) { + Matrix result = new Matrix(rowIndices.length, columnEnd - columnStart + 1); + for (int i = 0; i < rowIndices.length; i++) { + System.arraycopy(elements[rowIndices[i]], columnStart, result.elements[i], 0, columnEnd - columnStart + 1); + } + + return result; + } + + public Matrix setSubMatrix(int rowStart, int rowEnd, int columnStart, int columnEnd, Matrix matrix) { + for (int i = rowStart; i <= rowEnd; i++) { + System.arraycopy(matrix.elements[i - rowStart], 0, elements[i], columnStart, columnEnd - columnStart + 1); + } + + return this; + } + + public Matrix transpose() { + Matrix result = new Matrix(columns, rows); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + result.elements[j][i] = elements[i][j]; + } + } + + return result; + } + + public Matrix uminus() { + Matrix result = new Matrix(rows, columns); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + result.elements[i][j] = -elements[i][j]; + } + } + + return result; + } + + public Matrix plus(Matrix matrix) { + checkDimensions(matrix); + Matrix result = new Matrix(rows, columns); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + result.elements[i][j] = elements[i][j] + matrix.elements[i][j]; + } + } + + return result; + } + + public Matrix plusEquals(Matrix matrix) { + checkDimensions(matrix); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + elements[i][j] += matrix.elements[i][j]; + } + } + + return this; + } + + public Matrix minus(Matrix matrix) { + checkDimensions(matrix); + Matrix result = new Matrix(rows, columns); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + result.elements[i][j] = elements[i][j] - matrix.elements[i][j]; + } + } + + return result; + } + + public Matrix minusEquals(Matrix matrix) { + checkDimensions(matrix); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + elements[i][j] -= matrix.elements[i][j]; + } + } + + return this; + } + + public Matrix multiply(Matrix matrix) { + checkDimensions(matrix); + Matrix result = new Matrix(rows, columns); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + result.elements[i][j] = elements[i][j] * matrix.elements[i][j]; + } + } + + return result; + } + + public Matrix multiplyEquals(Matrix matrix) { + checkDimensions(matrix); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + elements[i][j] *= matrix.elements[i][j]; + } + } + + return this; + } + + public Matrix rightDivide(Matrix matrix) { + checkDimensions(matrix); + Matrix result = new Matrix(rows, columns); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + result.elements[i][j] = elements[i][j] / matrix.elements[i][j]; + } + } + + return result; + } + + public Matrix rightDivideEquals(Matrix matrix) { + checkDimensions(matrix); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + elements[i][j] /= matrix.elements[i][j]; + } + } + + return this; + } + + public Matrix leftDivide(Matrix matrix) { + checkDimensions(matrix); + Matrix result = new Matrix(rows, columns); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + result.elements[i][j] = matrix.elements[i][j] / elements[i][j]; + } + } + + return result; + } + + public Matrix leftDivideEquals(Matrix matrix) { + checkDimensions(matrix); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + elements[i][j] = matrix.elements[i][j] / elements[i][j]; + } + } + + return this; + } + + public Matrix times(double value) { + Matrix result = new Matrix(rows, columns); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + result.elements[i][j] = value * elements[i][j]; + } + } + + return result; + } + + public Matrix timesEquals(double value) { + for (int i = 0; i < rows; i++) { + for (int j = 0; j < columns; j++) { + elements[i][j] *= value; + } + } + + return this; + } + + public Matrix times(Matrix matrix) { + if (matrix.rows != columns) { + throw new IllegalArgumentException("Matrix inner dimensions must agree."); + } + + Matrix result = new Matrix(rows, matrix.columns); + for (int j = 0; j < matrix.columns; j++) { + for (int i = 0; i < rows; i++) { + double value = 0; + for (int k = 0; k < columns; k++) { + value += elements[i][k] * matrix.elements[k][j]; + } + + result.elements[i][j] = value; + } + } + + return result; + } + + public Matrix solve(Matrix matrix) { + return rows == columns ? + new LUDecomposition(this).solve(matrix) : + new QRDecomposition(this).solve(matrix); + } + + public Matrix invert() { + return solve(identity(rows, rows)); + } + + public Matrix copy() { + return new Matrix(this); + } + + public boolean isEqual(Matrix matrix) { + if (matrix.rows == rows && matrix.columns == columns) { + for (int i = 0; i < rows; i++) { + if (!Arrays.equals(elements[i], matrix.elements[i])) { + return false; + } + } + + return true; + } else { + return false; + } + } + + private void checkDimensions(Matrix matrix) { + if (matrix.rows != rows || matrix.columns != columns) { + throw new IllegalArgumentException("Matrix dimensions must agree."); + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj instanceof Matrix matrix + && matrix.rows == rows + && matrix.columns == columns) { + for (int i = 0; i < rows; i++) { + if (!Arrays.equals(elements[i], matrix.elements[i])) { + return false; + } + } + + return true; + } else { + return false; + } + } +} diff --git a/citydb-model/src/main/java/org/citydb/model/util/matrix/QRDecomposition.java b/citydb-model/src/main/java/org/citydb/model/util/matrix/QRDecomposition.java new file mode 100644 index 00000000..72820d32 --- /dev/null +++ b/citydb-model/src/main/java/org/citydb/model/util/matrix/QRDecomposition.java @@ -0,0 +1,126 @@ +/* + * citydb-tool - Command-line tool for the 3D City Database + * https://www.3dcitydb.org/ + * + * Copyright 2022-2024 + * virtualcitysystems GmbH, Germany + * https://vc.systems/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citydb.model.util.matrix; + +public class QRDecomposition { + private final double[][] elements; + private final int rows, columns; + private final double[] diag; + + QRDecomposition(Matrix A) { + elements = A.copy().elements; + rows = A.rows; + columns = A.columns; + diag = new double[columns]; + + for (int k = 0; k < columns; k++) { + double nrm = 0; + for (int i = k; i < rows; i++) { + nrm = hypot(nrm, elements[i][k]); + } + + if (nrm != 0) { + if (elements[k][k] < 0) { + nrm = -nrm; + } + + for (int i = k; i < rows; i++) { + elements[i][k] /= nrm; + } + + elements[k][k] += 1; + for (int j = k + 1; j < columns; j++) { + double s = 0; + for (int i = k; i < rows; i++) { + s += elements[i][k] * elements[i][j]; + } + + s = -s / elements[k][k]; + for (int i = k; i < rows; i++) { + elements[i][j] += s * elements[i][k]; + } + } + } + + diag[k] = -nrm; + } + } + + Matrix solve(Matrix matrix) { + if (matrix.rows != rows) { + throw new IllegalArgumentException("Matrix row dimensions must agree."); + } else if (!isFullRank()) { + throw new RuntimeException("Matrix is rank deficient."); + } + + double[][] result = matrix.copy().elements; + for (int k = 0; k < columns; k++) { + for (int j = 0; j < matrix.columns; j++) { + double s = 0; + for (int i = k; i < rows; i++) { + s += elements[i][k] * result[i][j]; + } + + s = -s / elements[k][k]; + for (int i = k; i < rows; i++) { + result[i][j] += s * elements[i][k]; + } + } + } + + for (int k = columns - 1; k >= 0; k--) { + for (int j = 0; j < matrix.columns; j++) { + result[k][j] /= diag[k]; + } + + for (int i = 0; i < k; i++) { + for (int j = 0; j < matrix.columns; j++) { + result[i][j] -= result[k][j] * elements[i][k]; + } + } + } + + return new Matrix(result, columns, matrix.columns).getSubMatrix(0, columns - 1, 0, matrix.columns - 1); + } + + private boolean isFullRank() { + for (int j = 0; j < columns; j++) { + if (diag[j] == 0) { + return false; + } + } + + return true; + } + + private double hypot(double a, double b) { + if (Math.abs(a) > Math.abs(b)) { + double r = b / a; + return Math.abs(a) * Math.sqrt(1 + r * r); + } else if (b != 0) { + double r = a / b; + return Math.abs(b) * Math.sqrt(1 + r * r); + } else { + return 0; + } + } +} diff --git a/citydb-operation/src/main/java/org/citydb/operation/exporter/ExportOptions.java b/citydb-operation/src/main/java/org/citydb/operation/exporter/ExportOptions.java index 48b7d89d..b786be98 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/exporter/ExportOptions.java +++ b/citydb-operation/src/main/java/org/citydb/operation/exporter/ExportOptions.java @@ -27,6 +27,9 @@ import org.citydb.core.concurrent.LazyCheckedInitializer; import org.citydb.core.file.OutputFile; import org.citydb.core.file.output.RegularOutputFile; +import org.citydb.model.common.Matrix3x4; +import org.citydb.model.encoding.Matrix3x4Reader; +import org.citydb.model.encoding.Matrix3x4Writer; import org.citydb.operation.exporter.options.AppearanceOptions; import org.citydb.operation.exporter.options.LodOptions; @@ -45,6 +48,8 @@ public class ExportOptions { private OutputFile outputFile; private int numberOfThreads; private SrsReference targetSrs; + @JSONField(serializeUsing = Matrix3x4Writer.class, deserializeUsing = Matrix3x4Reader.class) + private Matrix3x4 affineTransform; private LodOptions lodOptions; private AppearanceOptions appearanceOptions; @@ -88,6 +93,15 @@ public ExportOptions setTargetSrs(SrsReference targetSrs) { return this; } + public Optional getAffineTransform() { + return Optional.ofNullable(affineTransform); + } + + public ExportOptions setAffineTransform(Matrix3x4 affineTransform) { + this.affineTransform = affineTransform; + return this; + } + public Optional getLodOptions() { return Optional.ofNullable(lodOptions); } diff --git a/citydb-operation/src/main/java/org/citydb/operation/exporter/appearance/GeoreferencedTextureExporter.java b/citydb-operation/src/main/java/org/citydb/operation/exporter/appearance/GeoreferencedTextureExporter.java index 60bbe6b2..5372846e 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/exporter/appearance/GeoreferencedTextureExporter.java +++ b/citydb-operation/src/main/java/org/citydb/operation/exporter/appearance/GeoreferencedTextureExporter.java @@ -29,7 +29,6 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.util.stream.Collectors; import java.util.stream.IntStream; public class GeoreferencedTextureExporter extends TextureExporter { @@ -45,7 +44,7 @@ protected GeoreferencedTexture doExport(ResultSet rs) throws ExportException, SQ if (orientation != null) { texture.setOrientation(IntStream.range(0, orientation.size()) .mapToObj(orientation::getDouble) - .collect(Collectors.toList())); + .toList()); } Point referencePoint = getGeometry(rs.getObject("gt_reference_point"), Point.class); diff --git a/citydb-operation/src/main/java/org/citydb/operation/exporter/hierarchy/PropertyBuilder.java b/citydb-operation/src/main/java/org/citydb/operation/exporter/hierarchy/PropertyBuilder.java index 14a49174..e734a5c2 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/exporter/hierarchy/PropertyBuilder.java +++ b/citydb-operation/src/main/java/org/citydb/operation/exporter/hierarchy/PropertyBuilder.java @@ -32,8 +32,6 @@ import org.citydb.operation.exporter.ExportHelper; import org.citydb.operation.exporter.property.PropertyStub; -import java.util.stream.Collectors; - public class PropertyBuilder { private final ExportHelper helper; @@ -97,7 +95,7 @@ private ImplicitGeometryProperty buildImplicitGeometryProperty(PropertyStub prop property.setTransformationMatrix(propertyStub.getArrayValue().getValues().stream() .filter(Value::isDouble) .map(Value::doubleValue) - .collect(Collectors.toList())); + .toList()); } return property.setReferencePoint(propertyStub.getReferencePoint()) diff --git a/citydb-operation/src/main/java/org/citydb/operation/exporter/util/EnvelopeHelper.java b/citydb-operation/src/main/java/org/citydb/operation/exporter/util/EnvelopeHelper.java index 6426aefa..07320a8b 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/exporter/util/EnvelopeHelper.java +++ b/citydb-operation/src/main/java/org/citydb/operation/exporter/util/EnvelopeHelper.java @@ -21,6 +21,7 @@ package org.citydb.operation.exporter.util; +import org.citydb.model.common.Matrix4x4; import org.citydb.model.common.Reference; import org.citydb.model.common.RelationType; import org.citydb.model.feature.Feature; @@ -34,7 +35,10 @@ import org.citydb.model.walker.ModelWalker; import org.citydb.operation.exporter.ExportHelper; -import java.util.*; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; public class EnvelopeHelper { private final ExportHelper helper; @@ -96,11 +100,9 @@ private Envelope computeEnvelope(Feature feature, Map if (geometryInfo.hasImplicitGeometries()) { for (ImplicitGeometryProperty property : geometryInfo.getImplicitGeometries()) { - List transformationMatrix = property.getTransformationMatrix().orElse(null); + Matrix4x4 transformationMatrix = property.getTransformationMatrix().orElse(null); Point referencePoint = property.getReferencePoint().orElse(null); - if (transformationMatrix != null - && transformationMatrix.size() > 11 - && referencePoint != null) { + if (transformationMatrix != null && referencePoint != null) { ImplicitGeometry geometry = property.getObject().orElse( implicitGeometries.get(property.getReference() .map(Reference::getTarget).orElse(null))); @@ -108,9 +110,9 @@ private Envelope computeEnvelope(Feature feature, Map envelope.include(geometry.getEnvelope(transformationMatrix, referencePoint)); } else { envelope.include(Point.of(Coordinate.of( - referencePoint.getCoordinate().getX() + transformationMatrix.get(3), - referencePoint.getCoordinate().getY() + transformationMatrix.get(7), - referencePoint.getCoordinate().getZ() + transformationMatrix.get(11)))); + referencePoint.getCoordinate().getX() + transformationMatrix.get(0, 3), + referencePoint.getCoordinate().getY() + transformationMatrix.get(1, 3), + referencePoint.getCoordinate().getZ() + transformationMatrix.get(2, 3)))); } } } diff --git a/citydb-operation/src/main/java/org/citydb/operation/exporter/util/Postprocessor.java b/citydb-operation/src/main/java/org/citydb/operation/exporter/util/Postprocessor.java index 6d349290..9f81d411 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/exporter/util/Postprocessor.java +++ b/citydb-operation/src/main/java/org/citydb/operation/exporter/util/Postprocessor.java @@ -31,6 +31,7 @@ import org.citydb.model.property.FeatureProperty; import org.citydb.model.property.ImplicitGeometryProperty; import org.citydb.model.property.Property; +import org.citydb.model.util.AffineTransformer; import org.citydb.model.walker.ModelWalker; import org.citydb.operation.exporter.ExportHelper; @@ -43,6 +44,7 @@ public class Postprocessor { private final ExportHelper helper; private final EnvelopeHelper envelopeHelper; private final AppearanceHelper appearanceHelper; + private final AffineTransformer transformer; private final Comparator> comparator = Comparator.comparingLong( property -> property.getDescriptor() .map(DatabaseDescriptor::getId) @@ -52,6 +54,7 @@ public Postprocessor(ExportHelper helper) { this.helper = helper; appearanceHelper = new AppearanceHelper(helper); envelopeHelper = new EnvelopeHelper(helper); + transformer = helper.getOptions().getAffineTransform().map(AffineTransformer::of).orElse(null); } public void process(Feature feature) { @@ -75,11 +78,20 @@ public void process(Feature feature) { envelopeHelper.updateEnvelope(feature); } + if (transformer != null) { + transformer.transform(feature); + } + sortAttributes(feature); } public void process(Visitable visitable) { appearanceHelper.assignSurfaceData(visitable, helper.getSurfaceDataMapper()); + + if (transformer != null) { + transformer.transform(visitable); + } + sortAttributes(visitable); } diff --git a/citydb-operation/src/main/java/org/citydb/operation/exporter/util/SurfaceDataMapper.java b/citydb-operation/src/main/java/org/citydb/operation/exporter/util/SurfaceDataMapper.java index 289ac765..5e69e2f3 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/exporter/util/SurfaceDataMapper.java +++ b/citydb-operation/src/main/java/org/citydb/operation/exporter/util/SurfaceDataMapper.java @@ -27,7 +27,6 @@ import org.citydb.model.appearance.TextureCoordinate; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.IntStream; public class SurfaceDataMapper { @@ -90,7 +89,7 @@ public SurfaceDataMapper buildWorldToTextureMapping(JSONObject mapping, long geo if (entry.getValue() instanceof JSONArray array) { List worldToTexture = IntStream.range(0, array.size()) .mapToObj(array::getDouble) - .collect(Collectors.toList()); + .toList(); worldToTextureMappings.computeIfAbsent(surfaceData, v -> new HashMap<>()) .put(getKey(geometryDataId, entry.getKey()), worldToTexture); } diff --git a/citydb-operation/src/main/java/org/citydb/operation/importer/ImportHelper.java b/citydb-operation/src/main/java/org/citydb/operation/importer/ImportHelper.java index ce18b58d..df7562e1 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/importer/ImportHelper.java +++ b/citydb-operation/src/main/java/org/citydb/operation/importer/ImportHelper.java @@ -29,6 +29,7 @@ import org.citydb.model.common.Visitable; import org.citydb.model.feature.Feature; import org.citydb.model.feature.FeatureDescriptor; +import org.citydb.model.util.AffineTransformer; import org.citydb.operation.importer.common.DatabaseImporter; import org.citydb.operation.importer.feature.FeatureImporter; import org.citydb.operation.importer.reference.CacheType; @@ -51,6 +52,7 @@ public class ImportHelper { private final SchemaMapping schemaMapping; private final TableHelper tableHelper; private final SequenceHelper sequenceHelper; + private final AffineTransformer transformer; private final Map caches = new EnumMap<>(CacheType.class); private final List logEntries = new ArrayList<>(); private final Importer.TransactionMode transactionMode; @@ -70,6 +72,7 @@ public class ImportHelper { schemaMapping = adapter.getSchemaAdapter().getSchemaMapping(); tableHelper = new TableHelper(this); sequenceHelper = new SequenceHelper(this); + transformer = options.getAffineTransform().map(AffineTransformer::of).orElse(null); batchSize = options.getBatchSize() > 0 ? Math.min(options.getBatchSize(), adapter.getSchemaAdapter().getMaximumBatchSize()) : ImportOptions.DEFAULT_BATCH_SIZE; @@ -110,6 +113,10 @@ public FileLocator getFileLocator(ExternalFile file) { FeatureDescriptor importFeature(Feature feature) throws ImportException { try { + if (transformer != null) { + transformer.transform(feature); + } + generateSequenceValues(feature); FeatureDescriptor descriptor = tableHelper.getOrCreateImporter(FeatureImporter.class).doImport(feature); diff --git a/citydb-operation/src/main/java/org/citydb/operation/importer/ImportOptions.java b/citydb-operation/src/main/java/org/citydb/operation/importer/ImportOptions.java index 5472255b..bc5dd701 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/importer/ImportOptions.java +++ b/citydb-operation/src/main/java/org/citydb/operation/importer/ImportOptions.java @@ -21,10 +21,15 @@ package org.citydb.operation.importer; +import com.alibaba.fastjson2.annotation.JSONField; import org.citydb.config.SerializableConfig; import org.citydb.core.CoreConstants; +import org.citydb.model.common.Matrix3x4; +import org.citydb.model.encoding.Matrix3x4Reader; +import org.citydb.model.encoding.Matrix3x4Writer; import java.nio.file.Path; +import java.util.Optional; @SerializableConfig(name = "importOptions") public class ImportOptions { @@ -33,6 +38,8 @@ public class ImportOptions { private String tempDirectory; private int numberOfThreads; private int batchSize = DEFAULT_BATCH_SIZE; + @JSONField(serializeUsing = Matrix3x4Writer.class, deserializeUsing = Matrix3x4Reader.class) + private Matrix3x4 affineTransform; public Path getTempDirectory() { return tempDirectory != null ? CoreConstants.WORKING_DIR.resolve(tempDirectory) : null; @@ -60,4 +67,13 @@ public ImportOptions setBatchSize(int batchSize) { this.batchSize = batchSize; return this; } + + public Optional getAffineTransform() { + return Optional.ofNullable(affineTransform); + } + + public ImportOptions setAffineTransform(Matrix3x4 affineTransform) { + this.affineTransform = affineTransform; + return this; + } } diff --git a/citydb-operation/src/main/java/org/citydb/operation/importer/address/AddressImporter.java b/citydb-operation/src/main/java/org/citydb/operation/importer/address/AddressImporter.java index 72a4a157..31721265 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/importer/address/AddressImporter.java +++ b/citydb-operation/src/main/java/org/citydb/operation/importer/address/AddressImporter.java @@ -68,7 +68,7 @@ public AddressDescriptor doImport(Address address) throws ImportException, SQLEx stmt.setString(11, address.getCountry().orElse(null)); String arrayValue = address.getFreeText() - .map(array -> JSONArray.copyOf(array.getValues().stream() + .map(array -> new JSONArray(array.getValues().stream() .map(Value::rawValue) .collect(Collectors.toList())).toString()) .orElse(null); diff --git a/citydb-operation/src/main/java/org/citydb/operation/importer/appearance/GeoreferencedTextureImporter.java b/citydb-operation/src/main/java/org/citydb/operation/importer/appearance/GeoreferencedTextureImporter.java index 80ac7066..f431697d 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/importer/appearance/GeoreferencedTextureImporter.java +++ b/citydb-operation/src/main/java/org/citydb/operation/importer/appearance/GeoreferencedTextureImporter.java @@ -25,6 +25,7 @@ import org.citydb.database.schema.Sequence; import org.citydb.model.appearance.GeoreferencedTexture; import org.citydb.model.geometry.Point; +import org.citydb.model.util.matrix.Matrix; import org.citydb.operation.importer.ImportException; import org.citydb.operation.importer.ImportHelper; @@ -51,7 +52,8 @@ public long doImport(GeoreferencedTexture texture) throws ImportException, SQLEx long surfaceDataId = nextSequenceValue(Sequence.SURFACE_DATA); String orientation = texture.getOrientation() - .map(JSONArray::copyOf) + .map(Matrix::toRowMajor) + .map(JSONArray::new) .map(JSONArray::toString) .orElse(null); if (orientation != null) { diff --git a/citydb-operation/src/main/java/org/citydb/operation/importer/property/AttributeImporter.java b/citydb-operation/src/main/java/org/citydb/operation/importer/property/AttributeImporter.java index 32a7356d..3b2de7c9 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/importer/property/AttributeImporter.java +++ b/citydb-operation/src/main/java/org/citydb/operation/importer/property/AttributeImporter.java @@ -86,7 +86,7 @@ private PropertyDescriptor doImport(Attribute attribute, long propertyId, long p stmt.setString(13, attribute.getUom().orElse(null)); String arrayValue = attribute.getArrayValue() - .map(array -> JSONArray.copyOf(array.getValues().stream() + .map(array -> new JSONArray(array.getValues().stream() .map(Value::rawValue) .collect(Collectors.toList())).toString()) .orElse(null); diff --git a/citydb-operation/src/main/java/org/citydb/operation/importer/property/ImplicitGeometryPropertyImporter.java b/citydb-operation/src/main/java/org/citydb/operation/importer/property/ImplicitGeometryPropertyImporter.java index ea5aaf84..83e95634 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/importer/property/ImplicitGeometryPropertyImporter.java +++ b/citydb-operation/src/main/java/org/citydb/operation/importer/property/ImplicitGeometryPropertyImporter.java @@ -28,6 +28,7 @@ import org.citydb.model.geometry.ImplicitGeometry; import org.citydb.model.property.ImplicitGeometryProperty; import org.citydb.model.property.PropertyDescriptor; +import org.citydb.model.util.matrix.Matrix; import org.citydb.operation.importer.ImportException; import org.citydb.operation.importer.ImportHelper; import org.citydb.operation.importer.geometry.ImplicitGeometryImporter; @@ -84,7 +85,8 @@ PropertyDescriptor doImport(ImplicitGeometryProperty property, long propertyId, } String transformationMatrix = property.getTransformationMatrix() - .map(JSONArray::copyOf) + .map(Matrix::toRowMajor) + .map(JSONArray::new) .map(JSONArray::toString) .orElse(null); if (transformationMatrix != null) { diff --git a/citydb-operation/src/main/java/org/citydb/operation/importer/util/SurfaceDataMapper.java b/citydb-operation/src/main/java/org/citydb/operation/importer/util/SurfaceDataMapper.java index 7a0dd620..944d5544 100644 --- a/citydb-operation/src/main/java/org/citydb/operation/importer/util/SurfaceDataMapper.java +++ b/citydb-operation/src/main/java/org/citydb/operation/importer/util/SurfaceDataMapper.java @@ -23,6 +23,7 @@ import com.alibaba.fastjson2.JSONArray; import org.citydb.model.appearance.*; +import org.citydb.model.common.Matrix3x4; import org.citydb.model.geometry.GeometryDescriptor; import org.citydb.model.geometry.LinearRing; import org.citydb.model.geometry.Polygon; @@ -70,9 +71,10 @@ private Map getParameterizedTextureMapping(Parameteriz for (Surface surface : entry.getValue()) { String objectId = surface.getObjectId().orElse(null); if (objectId != null) { - List worldToTexture = texture.getWorldToTextureMapping(surface); + Matrix3x4 worldToTexture = texture.getWorldToTextureMapping(surface); if (worldToTexture != null) { - mapping.getOrCreateWorldToTextureMapping().put(objectId, JSONArray.copyOf(worldToTexture)); + mapping.getOrCreateWorldToTextureMapping() + .put(objectId, new JSONArray(worldToTexture.toRowMajor())); } if (surface instanceof Polygon polygon) {