Skip to content

Commit

Permalink
Merge pull request #51 from 3dcitydb/feature-affine-transform
Browse files Browse the repository at this point in the history
Add possibility to transform coordinates using a 3x4 matrix during import and export
  • Loading branch information
clausnagel authored Jan 3, 2025
2 parents b85944c + 146d39d commit 92114c3
Show file tree
Hide file tree
Showing 33 changed files with 1,337 additions and 76 deletions.
Original file line number Diff line number Diff line change
@@ -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 = "<m0,m1,...,m11|swap-xy>",
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 + "'");
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -295,6 +298,10 @@ protected ImportOptions getImportOptions() throws ExecutionException {
importOptions.setNumberOfThreads(threadsOptions.getNumberOfThreads());
}

if (transformOptions != null) {
importOptions.setAffineTransform(transformOptions.getTransformationMatrix());
}

return importOptions;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -146,13 +147,13 @@ private void addTextureTargets(Map<LinearRing, List<TextureCoordinate>> mapping,
"#" + k, new AbstractTextureParameterizationProperty(v)))));
}

public void addWorldToTextureTargets(Map<Surface<?>, List<Double>> mapping, ParameterizedTexture target, Set<String> geometries) {
public void addWorldToTextureTargets(Map<Surface<?>, Matrix3x4> mapping, ParameterizedTexture target, Set<String> geometries) {
Set<String> 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))));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))));
Expand Down
1 change: 1 addition & 0 deletions citydb-model/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,7 +34,7 @@

public class GeoreferencedTexture extends Texture<GeoreferencedTexture> {
private Point referencePoint;
private List<Double> orientation;
private Matrix2x2 orientation;
private List<Surface<?>> targets;

private GeoreferencedTexture() {
Expand All @@ -57,14 +58,19 @@ public GeoreferencedTexture setReferencePoint(Point referencePoint) {
return this;
}

public Optional<List<Double>> getOrientation() {
public Optional<Matrix2x2> getOrientation() {
return Optional.ofNullable(orientation);
}

public GeoreferencedTexture setOrientation(Matrix2x2 orientation) {
this.orientation = orientation;
return this;
}

public GeoreferencedTexture setOrientation(List<Double> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,7 +32,7 @@

public class ParameterizedTexture extends Texture<ParameterizedTexture> {
private Map<LinearRing, List<TextureCoordinate>> textureCoordinates;
private Map<Surface<?>, List<Double>> worldToTextureMappings;
private Map<Surface<?>, Matrix3x4> worldToTextureMappings;

private ParameterizedTexture() {
}
Expand Down Expand Up @@ -62,35 +63,46 @@ public List<TextureCoordinate> getTextureCoordinates(LinearRing linearRing) {
}

public ParameterizedTexture addTextureCoordinates(LinearRing linearRing, List<TextureCoordinate> 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;
}

public boolean hasWorldToTextureMappings() {
return worldToTextureMappings != null && !worldToTextureMappings.isEmpty();
}

public Map<Surface<?>, List<Double>> getWorldToTextureMappings() {
public Map<Surface<?>, Matrix3x4> getWorldToTextureMappings() {
if (worldToTextureMappings == null) {
worldToTextureMappings = new IdentityHashMap<>();
}

return worldToTextureMappings;
}

public List<Double> 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<Double> 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;
Expand Down
69 changes: 69 additions & 0 deletions citydb-model/src/main/java/org/citydb/model/common/Matrix2x2.java
Original file line number Diff line number Diff line change
@@ -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<Double> 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));
}
}
Loading

0 comments on commit 92114c3

Please sign in to comment.