updates for v1.1.0

This commit is contained in:
Hari Nair
2025-07-30 11:24:30 -04:00
parent dfaa734383
commit f826c9acb8
32 changed files with 1588 additions and 301 deletions

View File

@@ -1,5 +1,20 @@
# Terrasaur Changelog
## July 30, 2025 - v1.1.0
- Updates to existing tools
- AdjustShapeModelToOtherShapeModel
- fix intersection bug
- CreateSBMTStructure
- new options: -flipX, -flipY, -spice, -date, -observer, -target, -cameraFrame
- ValidateNormals
- new option: -fast to only check for overhangs if center and normal point in opposite directions
- New tools:
- FacetInfo: Print info about a facet
- PointCloudOverlap: Find points in a point cloud which overlap a reference point cloud
- TriAx: Generate a triaxial ellipsoid in ICQ format
## April 28, 2025 - v1.0.1
- Add MIT license to repository and source code

Binary file not shown.

Binary file not shown.

View File

@@ -16,7 +16,7 @@ The Terrasaur package requires Java 21 or later. Some freely available versions
Download
~~~~~~~~
Binary packages for use on Mac OS X and Linux are available at ...
Binary packages for use on Mac OS X and Linux are available at `GitHub <https://github.com/JHUAPL/Terrasaur/releases>`__.
We have not tried using the softare on Microsoft Windows, but users may try the Linux package with the `Windows Subsystem for Linux <https://docs.microsoft.com/en-us/windows/wsl/>`__.

View File

@@ -1,35 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

70
doc/tools/ColorSpots.rst Normal file
View File

@@ -0,0 +1,70 @@
.. _ColorSpots:
##########
ColorSpots
##########
ColorSpots takes as input a shape model and a file containing (x, y, z, value),
or (lat, lon, value). It writes out the mean and standard deviation of values
within a specified range for each facet.
.. include:: ../toolDescriptions/ColorSpots.txt
:literal:
********
Examples
********
Download the :download:`Apophis<./support_files/apophis_g_15618mm_rdr_obj_0000n00000_v001.obj>`
shape model and the :download:`info<./support_files/xyzrandom.txt>` file containing
cartesian coordinates and a random value.
Run ColorSpots:
::
ColorSpots -obj apophis_g_15618mm_rdr_obj_0000n00000_v001.obj -xyz \
-info xyzrandom.txt -outFile apophis_value_at_vertex.csv -noWeight \
-allFacets -additionalFields n -searchRadius 0.015 -writeVertices
The first few lines of apophis_value_at_vertex.csv look like:
::
% head apophis_value_at_vertex.csv
0.000000e+00, 0.000000e+00, 1.664960e-01, -3.805764e-02, 5.342315e-01, 4.000000e+01
1.589500e-02, 0.000000e+00, 1.591030e-01, 6.122849e-02, 6.017192e-01, 5.000000e+01
7.837000e-03, 1.486800e-02, 1.591670e-01, -6.072964e-03, 5.220682e-01, 5.700000e+01
-7.747000e-03, 1.506300e-02, 1.621040e-01, 9.146163e-02, 5.488631e-01, 4.900000e+01
-1.554900e-02, 0.000000e+00, 1.657970e-01, -8.172811e-03, 5.270302e-01, 3.400000e+01
-7.982000e-03, -1.571100e-02, 1.694510e-01, -2.840524e-02, 5.045911e-01, 3.900000e+01
8.060000e-03, -1.543300e-02, 1.655150e-01, 3.531959e-02, 5.464390e-01, 4.900000e+01
3.179500e-02, 0.000000e+00, 1.515820e-01, -1.472434e-02, 5.967265e-01, 5.400000e+01
2.719700e-02, 1.658200e-02, 1.508930e-01, -9.050683e-03, 5.186966e-01, 4.700000e+01
1.554100e-02, 2.901300e-02, 1.530770e-01, -7.053547e-02, 4.980369e-01, 7.000000e+01
The columns are:
.. list-table:: ColorSpots Vertex output
:header-rows: 1
* - Column
- Value
* - 1
- X
* - 2
- Y
* - 3
- Z
* - 4
- mean value in region
* - 5
- standard deviation in region
* - 6
- number of points in region
.. figure:: images/ColorSpots-n.png
:alt: Number of points in region at each vertex
This image shows the number of points in the region at each vertex.

View File

@@ -23,7 +23,7 @@ Local Model Comparison
Download the :download:`reference<./support_files/EVAL20_wtr.obj>` and :download:`comparison<./support_files/EVAL20.obj>`
shape models. You can view them in a tool such as
`ParaView<https://www.paraview.org/>`.
`ParaView <https://www.paraview.org/>`__.
.. figure:: images/CompareOBJ_local_1.png
@@ -32,8 +32,8 @@ shape models. You can view them in a tool such as
Run CompareOBJ to find the optimal transform to align the comparison with the reference:
::
CompareOBJ -computeOptimalRotationAndTranslation -model F3H-1/EVAL20.obj \
-reference F3H-1/EVAL20_wtr.obj -computeVerticalError verticalError.txt \
CompareOBJ -computeOptimalRotationAndTranslation -model EVAL20.obj \
-reference EVAL20_wtr.obj -computeVerticalError verticalError.txt \
-saveOptimalShape optimal.obj -savePlateDiff plateDiff.txt -savePlateIndex plateIndex.txt
The screen output is
@@ -77,7 +77,7 @@ model for comparison:
::
ShapeFormatConverter -input Bennu/Bennu49k.obj -output BennuComparison.obj \
ShapeFormatConverter -input Bennu49k.obj -output BennuComparison.obj \
-rotate 5,0,0,1 -translate 0.01,-0.01,0.01
This rotates the shape model by 5 degrees about the z axis and then translates
@@ -94,11 +94,11 @@ Run CompareOBJ to find the optimal transform to align the comparison with the re
CompareOBJ -computeOptimalRotationAndTranslation \
-model BennuComparison.obj \
-reference Bennu/Bennu49k.obj \
-computeVerticalError CompareOBJ/terrasaur-verticalError.txt \
-saveOptimalShape CompareOBJ/terrasaur-optimal.obj \
-savePlateDiff CompareOBJ/terrasaur-plateDiff.txt \
-savePlateIndex CompareOBJ/terrasaur-plateIndex.txt
-reference Bennu49k.obj \
-computeVerticalError terrasaur-verticalError.txt \
-saveOptimalShape terrasaur-optimal.obj \
-savePlateDiff terrasaur-plateDiff.txt \
-savePlateIndex terrasaur-plateIndex.txt
The screen output is

View File

@@ -0,0 +1,38 @@
.. _PointCloudOverlap:
#################
PointCloudOverlap
#################
*****
Usage
*****
.. include:: ../toolDescriptions/PointCloudOverlap.txt
:literal:
********
Examples
********
Download the :download:`reference<./support_files/EVAL20_wtr.obj>` and :download:`comparison<./support_files/EVAL20.obj>`
shape models. You can view them in a tool such as
`ParaView <https://www.paraview.org/>`__.
.. figure:: images/PointCloudOverlap_1.png
This image shows the reference (pink) and input (blue) shape models.
Run PointCloudOverlap:
::
PointCloudOverlap -inputFile EVAL20.obj -referenceFile EVAL20_wtr.obj -outputFile overlap.vtk
Note that OBJ is supported as an input format but not as an output format.
.. figure:: images/PointCloudOverlap_2.png
The points in white are those in the input model that overlap the reference.

43
doc/tools/TriAx.rst Normal file
View File

@@ -0,0 +1,43 @@
.. _TriAx:
=====
TriAx
=====
TriAx is an implementation of the SPC tool TRIAX, which generates a triaxial ellipsoid in ICQ format.
*****
Usage
*****
.. include:: ../toolDescriptions/TriAx.txt
:literal:
*******
Example
*******
Generate an ellipsoid with dimensions 10, 8, 6, with q = 8.
::
TriAx -A 10 -B 8 -C 6 -Q 8 -output triax.icq -saveOBJ
The following ellipsoid is generated:
.. container:: figures-row
.. figure:: images/TriAx_X.png
:alt: looking down from the +X direction
looking down from the +X direction
.. figure:: images/TriAx_Y.png
:alt: looking down from the +Y direction
looking down from the +Y direction
.. figure:: images/TriAx_Z.png
:alt: looking down from the +Z direction
looking down from the +Z direction

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -89,7 +89,7 @@ function build_jar() {
function make_scripts() {
classes=$(jar tf "${scriptPath}"/target/${packageName}.jar | grep $appSrcDir | grep -v '\$' | grep -v "package-info" | grep class)
classes=$(jar tf ${scriptPath}/target/${packageName}.jar | grep $appSrcDir | grep -v '\$' | grep -v "package-info" | grep -v "Immutable" | grep class)
for class in $classes; do
base=$(basename "$class" ".class")

48
pom.xml
View File

@@ -1,5 +1,6 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>edu.jhuapl.ses.srn</groupId>
<artifactId>Terrasaur</artifactId>
@@ -55,25 +56,29 @@
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>5.0.0</version>
<configuration>
<licenseSets>
<licenseSet>
<header>com/mycila/maven/plugin/license/templates/MIT.txt</header>
<includes>
<include>**/*.java</include>
</includes>
</licenseSet>
</licenseSets>
</configuration>
<executions>
<execution>
<id>check-license</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<configuration>
<licenseSets>
<licenseSet>
<header>com/mycila/maven/plugin/license/templates/MIT.txt</header>
<includes>
<include>**/*.java</include>
</includes>
<excludes>
<exclude>3rd-party/**/*.java</exclude>
<exclude>support-libraries/3rd-party/**/*.java</exclude>
</excludes>
</licenseSet>
</licenseSets>
</configuration>
<executions>
<execution>
<id>check-license</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
@@ -126,7 +131,8 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
<configuration>
<argLine> -Djava.library.path=${project.basedir}/3rd-party/${env.ARCH}/spice/JNISpice/lib
<argLine>
-Djava.library.path=${project.basedir}/3rd-party/${env.ARCH}/spice/JNISpice/lib
</argLine>
</configuration>
</plugin>

View File

@@ -25,7 +25,6 @@ package terrasaur.apps;
import java.io.File;
import java.nio.charset.Charset;
import java.util.*;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
@@ -33,14 +32,14 @@ import org.apache.commons.io.FileUtils;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import spice.basic.Plane;
import spice.basic.Vector3;
import terrasaur.smallBodyModel.BoundingBox;
import terrasaur.smallBodyModel.SmallBodyModel;
import terrasaur.templates.TerrasaurTool;
import terrasaur.utils.Log4j2Configurator;
import terrasaur.utils.NativeLibraryLoader;
import terrasaur.utils.PolyDataUtil;
import spice.basic.Plane;
import spice.basic.Vector3;
import vtk.vtkGenericCell;
import vtk.vtkPoints;
import vtk.vtkPolyData;
@@ -76,7 +75,7 @@ public class AdjustShapeModelToOtherShapeModel implements TerrasaurTool {
return TerrasaurTool.super.fullDescription(options, header, "");
}
private static Options defineOptions() {
private static Options defineOptions() {
Options options = TerrasaurTool.defineOptions();
options.addOption(
Option.builder("from")
@@ -182,6 +181,7 @@ public class AdjustShapeModelToOtherShapeModel implements TerrasaurTool {
Vector3D origin = new Vector3D(0., 0., 0.);
for (int i = 0; i < numberPoints; ++i) {
points.GetPoint(i, p);
Vector3D thisPoint = new Vector3D(p);
Vector3D lookDir;
@@ -198,18 +198,20 @@ public class AdjustShapeModelToOtherShapeModel implements TerrasaurTool {
}
Vector3D lookPt = lookDir.scalarMultiply(diagonalLength);
lookPt = lookPt.add(origin);
lookPt = lookPt.add(thisPoint);
List<Vector3D> intersections = new ArrayList<>();
for (vtksbCellLocator cellLocator : cellLocators) {
double[] intersectPoint = new double[3];
// trace ray from the lookPt to the origin - first intersection is the farthest intersection
// from the origin
// trace ray from thisPoint to the lookPt - Assume cell intersection is the closest one if
// there are multiple?
// NOTE: result should return 1 in case of intersection but doesn't sometimes.
// Use the norm of intersection point to test for intersection instead.
int result =
cellLocator.IntersectWithLine(
thisPoint.toArray(),
lookPt.toArray(),
origin.toArray(),
tol,
t,
intersectPoint,
@@ -219,38 +221,32 @@ public class AdjustShapeModelToOtherShapeModel implements TerrasaurTool {
cell);
Vector3D intersectVector = new Vector3D(intersectPoint);
if (fitPlane || localModel) {
// NOTE: result should return 1 in case of intersection but doesn't sometimes.
// Use the norm of intersection point to test for intersection instead.
NavigableMap<Double, Vector3D> pointsMap = new TreeMap<>();
if (intersectVector.getNorm() > 0) {
pointsMap.put(origin.subtract(intersectVector).getNorm(), intersectVector);
}
lookPt = lookDir.scalarMultiply(-diagonalLength);
lookPt = lookPt.add(origin);
result =
cellLocator.IntersectWithLine(
lookPt.toArray(),
origin.toArray(),
tol,
t,
intersectPoint,
pcoords,
subId,
cell_id,
cell);
intersectVector = new Vector3D(intersectPoint);
if (intersectVector.getNorm() > 0) {
pointsMap.put(origin.subtract(intersectVector).getNorm(), intersectVector);
}
if (!pointsMap.isEmpty()) intersections.add(pointsMap.get(pointsMap.firstKey()));
} else {
if (result > 0) intersections.add(intersectVector);
NavigableMap<Double, Vector3D> pointsMap = new TreeMap<>();
if (intersectVector.getNorm() > 0) {
pointsMap.put(thisPoint.subtract(intersectVector).getNorm(), intersectVector);
}
// look in the other direction
lookPt = lookDir.scalarMultiply(-diagonalLength);
lookPt = lookPt.add(thisPoint);
result =
cellLocator.IntersectWithLine(
thisPoint.toArray(),
lookPt.toArray(),
tol,
t,
intersectPoint,
pcoords,
subId,
cell_id,
cell);
intersectVector = new Vector3D(intersectPoint);
if (intersectVector.getNorm() > 0) {
pointsMap.put(thisPoint.subtract(intersectVector).getNorm(), intersectVector);
}
if (!pointsMap.isEmpty()) intersections.add(pointsMap.get(pointsMap.firstKey()));
}
if (intersections.isEmpty()) throw new Exception("Error: no intersections at all");

View File

@@ -60,8 +60,7 @@ public class CreateSBMTStructure implements TerrasaurTool {
@Override
public String fullDescription(Options options) {
String header =
"This tool creates an SBMT ellipse file from a set of point on an image.";
String header = "This tool creates an SBMT ellipse file from a set of points on an image.";
String footer = "";
return TerrasaurTool.super.fullDescription(options, header, footer);
}
@@ -129,20 +128,31 @@ public class CreateSBMTStructure implements TerrasaurTool {
private static Options defineOptions() {
Options options = TerrasaurTool.defineOptions();
options.addOption(
Option.builder("flipX")
.desc("If present, negate the X coordinate of the input points.")
.build());
options.addOption(
Option.builder("flipY")
.desc("If present, negate the Y coordinate of the input points.")
.build());
options.addOption(
Option.builder("input")
.required()
.hasArg()
.desc(
"""
Required. Name or input file. This is a text file with a pair of pixel coordinates per line. The pixel
"""
Required. Name or input file. This is a text file with a pair of pixel coordinates (X and Y) per line. The pixel
coordinates are offsets from the image center. For example:
# My test file
627.51274 876.11775
630.53612 883.55992
626.3499 881.46681
89.6628 285.01
97.8027 280.126
95.0119 285.01
-13.8299 323.616
-1.9689 331.756
-11.7367 330.826
Empty lines or lines beginning with # are ignored.
@@ -161,11 +171,38 @@ axis and the third is a location for the semi-minor axis.""")
.hasArg()
.desc("Required. Name of output file.")
.build());
options.addOption(
Option.builder("spice")
.hasArg()
.desc(
"If present, name of metakernel to read. Other required options with -spice are -date, -observer, -target, and -cameraFrame.")
.build());
options.addOption(
Option.builder("date")
.hasArgs()
.desc("Only used with -spice. Date of image (e.g. 2022 SEP 26 23:11:12.649).")
.build());
options.addOption(
Option.builder("observer")
.hasArg()
.desc("Only used with -spice. Observing body (e.g. DART)")
.build());
options.addOption(
Option.builder("target")
.hasArg()
.desc("Only used with -spice. Target body (e.g. DIMORPHOS).")
.build());
options.addOption(
Option.builder("cameraFrame")
.hasArg()
.desc("Only used with -spice. Camera frame (e.g. DART_DRACO).")
.build());
options.addOption(
Option.builder("sumFile")
.required()
.hasArg()
.desc("Required. Name of sum file to read.")
.desc(
"Required. Name of sum file to read. This is still required with -spice, but only used as a template to create a new sum file.")
.build());
return options;
}
@@ -179,7 +216,7 @@ axis and the third is a location for the semi-minor axis.""")
Map<MessageLabel, String> startupMessages = defaultOBJ.startupMessages(cl);
for (MessageLabel ml : startupMessages.keySet())
logger.info(String.format("%s %s", ml.label, startupMessages.get(ml)));
logger.info("{} {}", ml.label, startupMessages.get(ml));
NativeLibraryLoader.loadSpiceLibraries();
NativeLibraryLoader.loadVtkLibraries();
@@ -195,6 +232,8 @@ axis and the third is a location for the semi-minor axis.""")
}
RangeFromSumFile rfsf = new RangeFromSumFile(sumFile, polyData);
boolean flipX = cl.hasOption("flipX");
boolean flipY = cl.hasOption("flipY");
List<Vector3D> intercepts = new ArrayList<>();
List<String> lines =
FileUtils.readLines(new File(cl.getOptionValue("input")), Charset.defaultCharset());
@@ -204,6 +243,9 @@ axis and the third is a location for the semi-minor axis.""")
int ix = (int) Math.round(Double.parseDouble(parts[0]));
int iy = (int) Math.round(Double.parseDouble(parts[1]));
if (flipX) ix *= -1;
if (flipY) iy *= -1;
Map.Entry<Long, Vector3D> entry = rfsf.findIntercept(ix, iy);
long cellID = entry.getKey();
if (cellID > -1) intercepts.add(entry.getValue());
@@ -216,12 +258,13 @@ axis and the third is a location for the semi-minor axis.""")
// p1 and p2 define the long axis of the ellipse
Vector3D p1 = intercepts.get(i);
Vector3D p2 = intercepts.get(i+1);
Vector3D p2 = intercepts.get(i + 1);
// p3 lies on the short axis
Vector3D p3 = intercepts.get(i+2);
Vector3D p3 = intercepts.get(i + 2);
SBMTEllipseRecord record = createRecord(i/3, String.format("Ellipse %d", i/3), p1, p2, p3);
SBMTEllipseRecord record =
createRecord(i / 3, String.format("Ellipse %d", i / 3), p1, p2, p3);
records.add(record);
}

View File

@@ -0,0 +1,258 @@
/*
* The MIT License
* Copyright © 2025 Johns Hopkins University Applied Physics Laboratory
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package terrasaur.apps;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Map;
import java.util.NavigableSet;
import java.util.TreeSet;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.math3.geometry.euclidean.threed.SphericalCoordinates;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.immutables.value.Value;
import terrasaur.templates.TerrasaurTool;
import terrasaur.utils.CellInfo;
import terrasaur.utils.NativeLibraryLoader;
import terrasaur.utils.PolyDataStatistics;
import terrasaur.utils.PolyDataUtil;
import vtk.vtkIdList;
import vtk.vtkOBBTree;
import vtk.vtkPolyData;
public class FacetInfo implements TerrasaurTool {
private static final Logger logger = LogManager.getLogger();
/**
* This doesn't need to be private, or even declared, but you might want to if you have other
* constructors.
*/
private FacetInfo() {}
@Override
public String shortDescription() {
return "Print info about a facet.";
}
@Override
public String fullDescription(Options options) {
String header = "Prints information about facet(s).";
String footer =
"""
This tool prints out facet center, normal, angle between center and
normal, and other information about the specified facet(s).""";
return TerrasaurTool.super.fullDescription(options, header, footer);
}
private vtkPolyData polyData;
private vtkOBBTree searchTree;
private Vector3D origin;
private FacetInfo(vtkPolyData polyData) {
this.polyData = polyData;
PolyDataStatistics stats = new PolyDataStatistics(polyData);
origin = new Vector3D(stats.getCentroid());
logger.info("Origin is at {}", origin);
logger.info("Creating search tree");
searchTree = new vtkOBBTree();
searchTree.SetDataSet(polyData);
searchTree.SetTolerance(1e-12);
searchTree.BuildLocator();
}
/**
* @param cellId id of this cell
* @return Set of neighboring cells (ones which share a vertex with this one)
*/
private NavigableSet<Long> neighbors(long cellId) {
NavigableSet<Long> neighborCellIds = new TreeSet<>();
vtkIdList vertexIdlist = new vtkIdList();
CellInfo.getCellInfo(polyData, cellId, vertexIdlist);
vtkIdList facetIdlist = new vtkIdList();
for (long i = 0; i < vertexIdlist.GetNumberOfIds(); i++) {
long vertexId = vertexIdlist.GetId(i);
polyData.GetPointCells(vertexId, facetIdlist);
}
for (long i = 0; i < facetIdlist.GetNumberOfIds(); i++) {
long id = facetIdlist.GetId(i);
if (id == cellId) continue;
neighborCellIds.add(id);
}
return neighborCellIds;
}
@Value.Immutable
public abstract static class FacetInfoLine {
public abstract long index();
public abstract Vector3D radius();
public abstract Vector3D normal();
/**
* @return facets between this and origin
*/
public abstract NavigableSet<Long> interiorIntersections();
/**
* @return facets between this and infinity
*/
public abstract NavigableSet<Long> exteriorIntersections();
public static String getHeader() {
return "# Index, "
+ "Center Lat (deg), "
+ "Center Lon (deg), "
+ "Radius, "
+ "Radial Vector, "
+ "Normal Vector, "
+ "Angle between radial and normal (deg), "
+ "facets between this and origin, "
+ "facets between this and infinity";
}
public String toCSV() {
SphericalCoordinates spc = new SphericalCoordinates(radius());
StringBuilder sb = new StringBuilder();
sb.append(String.format("%d, ", index()));
sb.append(String.format("%.4f, ", 90 - Math.toDegrees(spc.getPhi())));
sb.append(String.format("%.4f, ", Math.toDegrees(spc.getTheta())));
sb.append(String.format("%.6f, ", spc.getR()));
sb.append(
String.format("%.6f %.6f %.6f, ", radius().getX(), radius().getY(), radius().getZ()));
sb.append(
String.format("%.6f %.6f %.6f, ", normal().getX(), normal().getY(), normal().getZ()));
sb.append(String.format("%.3f, ", Math.toDegrees(Vector3D.angle(radius(), normal()))));
sb.append(String.format("%d", interiorIntersections().size()));
if (!interiorIntersections().isEmpty()) {
for (long id : interiorIntersections()) sb.append(String.format(" %d", id));
}
sb.append(", ");
sb.append(String.format("%d", exteriorIntersections().size()));
if (!exteriorIntersections().isEmpty()) {
for (long id : exteriorIntersections()) sb.append(String.format(" %d", id));
}
return sb.toString();
}
}
private FacetInfoLine getFacetInfoLine(long cellId) {
CellInfo ci = CellInfo.getCellInfo(polyData, cellId, new vtkIdList());
vtkIdList cellIds = new vtkIdList();
searchTree.IntersectWithLine(origin.toArray(), ci.center().toArray(), null, cellIds);
// count up all crossings of the surface between the origin and the facet.
NavigableSet<Long> insideIds = new TreeSet<>();
for (long j = 0; j < cellIds.GetNumberOfIds(); j++) {
if (cellIds.GetId(j) == cellId) continue;
insideIds.add(cellIds.GetId(j));
}
Vector3D infinity = ci.center().scalarMultiply(1e9);
cellIds = new vtkIdList();
searchTree.IntersectWithLine(infinity.toArray(), ci.center().toArray(), null, cellIds);
// count up all crossings of the surface between the infinity and the facet.
NavigableSet<Long> outsideIds = new TreeSet<>();
for (long j = 0; j < cellIds.GetNumberOfIds(); j++) {
if (cellIds.GetId(j) == cellId) continue;
outsideIds.add(cellIds.GetId(j));
}
return ImmutableFacetInfoLine.builder()
.index(cellId)
.radius(ci.center())
.normal(ci.normal())
.interiorIntersections(insideIds)
.exteriorIntersections(outsideIds)
.build();
}
private static Options defineOptions() {
Options options = TerrasaurTool.defineOptions();
options.addOption(
Option.builder("facet")
.required()
.hasArgs()
.desc("Facet(s) to query. Separate multiple indices with whitespace.")
.build());
options.addOption(
Option.builder("obj").required().hasArg().desc("Shape model to validate.").build());
options.addOption(
Option.builder("output").required().hasArg().desc("CSV file to write.").build());
return options;
}
public static void main(String[] args) {
TerrasaurTool defaultOBJ = new FacetInfo();
Options options = defineOptions();
CommandLine cl = defaultOBJ.parseArgs(args, options);
Map<MessageLabel, String> startupMessages = defaultOBJ.startupMessages(cl);
for (MessageLabel ml : startupMessages.keySet())
logger.info("{} {}", ml.label, startupMessages.get(ml));
NativeLibraryLoader.loadVtkLibraries();
try {
vtkPolyData polydata = PolyDataUtil.loadShapeModel(cl.getOptionValue("obj"));
FacetInfo app = new FacetInfo(polydata);
try (PrintWriter pw = new PrintWriter(cl.getOptionValue("output"))) {
pw.println(FacetInfoLine.getHeader());
for (long cellId :
Arrays.stream(cl.getOptionValues("facet")).mapToLong(Long::parseLong).toArray()) {
pw.println(app.getFacetInfoLine(cellId).toCSV());
NavigableSet<Long> neighbors = app.neighbors(cellId);
for (long neighborCellId : neighbors)
pw.println(app.getFacetInfoLine(neighborCellId).toCSV());
}
}
logger.info("Wrote {}", cl.getOptionValue("output"));
} catch (Exception e) {
logger.error(e.getLocalizedMessage(), e);
}
logger.info("Finished.");
}
}

View File

@@ -305,8 +305,8 @@ public class PointCloudFormatConverter implements TerrasaurTool {
}
} else {
if (halfSize < 0 || groundSampleDistance < 0) {
System.out.printf(
"Must supply -halfSize and -groundSampleDistance for %s output\n",
logger.error(
"Must supply -halfSize and -groundSampleDistance for {} output",
outFormat);
return;
}

View File

@@ -0,0 +1,417 @@
/*
* The MIT License
* Copyright © 2025 Johns Hopkins University Applied Physics Laboratory
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package terrasaur.apps;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import nom.tam.fits.FitsException;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import org.apache.commons.math3.geometry.euclidean.twod.Vector2D;
import org.apache.commons.math3.geometry.euclidean.twod.hull.MonotoneChain;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import picante.math.coords.LatitudinalVector;
import spice.basic.SpiceException;
import spice.basic.Vector3;
import terrasaur.enums.FORMATS;
import terrasaur.templates.TerrasaurTool;
import terrasaur.utils.NativeLibraryLoader;
import terrasaur.utils.VectorStatistics;
import terrasaur.utils.tessellation.StereographicProjection;
import vtk.vtkPoints;
public class PointCloudOverlap implements TerrasaurTool {
private static final Logger logger = LogManager.getLogger();
@Override
public String shortDescription() {
return "Find points in a point cloud which overlap a reference point cloud.";
}
@Override
public String fullDescription(Options options) {
String header = "";
String footer =
"\nThis program finds all points in the input point cloud which overlap the points in a reference point cloud.\n\n"
+ "Supported input formats are ASCII, BINARY, L2, OBJ, and VTK. Supported output formats are ASCII, BINARY, L2, and VTK. "
+ "ASCII format is white spaced delimited x y z coordinates. BINARY files must contain double precision x y z coordinates.\n\n"
+ "A plane is fit to the reference point cloud and all points in each cloud are projected onto this plane. Any point in the "
+ "projected input cloud which falls within the outline of the projected reference cloud is considered to be overlapping.";
return TerrasaurTool.super.fullDescription(options, header, footer);
}
private Path2D.Double polygon;
private StereographicProjection proj;
/*- Useful for debugging
private vtkPoints ref2DPoints;
private vtkPoints input2DPoints;
private vtkPolyData polygonPolyData;
private vtkCellArray polygonCells;
private vtkPoints polygonPoints;
private vtkDoubleArray polygonSuccessArray;
*/
public PointCloudOverlap(Collection<double[]> refPoints) {
if (refPoints != null) {
VectorStatistics vStats = new VectorStatistics();
for (double[] pt : refPoints) vStats.add(new Vector3(pt));
Vector3D centerXYZ = vStats.getMean();
proj =
new StereographicProjection(
new LatitudinalVector(1, centerXYZ.getDelta(), centerXYZ.getAlpha()));
createRefPolygon(refPoints);
}
}
public StereographicProjection getProjection() {
return proj;
}
private void createRefPolygon(Collection<double[]> refPoints) {
List<Vector2D> stereographicPoints = new ArrayList<>();
for (double[] refPt : refPoints) {
Vector3D point3D = new Vector3D(refPt);
Point2D point = proj.forward(point3D.getDelta(), point3D.getAlpha());
stereographicPoints.add(new Vector2D(point.getX(), point.getY()));
}
/*-
ref2DPoints = new vtkPoints();
input2DPoints = new vtkPoints();
polygonPolyData = new vtkPolyData();
polygonCells = new vtkCellArray();
polygonPoints = new vtkPoints();
polygonSuccessArray = new vtkDoubleArray();
polygonSuccessArray.SetName("success")
polygonPolyData.SetPoints(polygonPoints);
polygonPolyData.SetLines(polygonCells);
polygonPolyData.GetCellData().AddArray(polygonSuccessArray);
for (Vector2D refPoint : refPoints)
ref2DPoints.InsertNextPoint(refPoint.getX(), refPoint.getY(), 0);
*/
MonotoneChain mc = new MonotoneChain();
List<Vector2D> vertices = new ArrayList<>(mc.findHullVertices(stereographicPoints));
/*-
for (int i = 1; i < vertices.size(); i++) {
Vector2D lastPt = vertices.get(i - 1);
Vector2D thisPt = vertices.get(i);
System.out.printf("%f %f to %f %f distance %f\n", lastPt.getX(), lastPt.getY(), thisPt.getX(),
thisPt.getY(), lastPt.distance(thisPt));
}
Vector2D lastPt = vertices.get(vertices.size() - 1);
Vector2D thisPt = vertices.get(0);
System.out.printf("%f %f to %f %f distance %f\n", lastPt.getX(), lastPt.getY(), thisPt.getX(),
thisPt.getY(), lastPt.distance(thisPt));
*/
// int id0 = 0;
for (Vector2D vertex : vertices) {
// int id1 = polygonPoints.InsertNextPoint(vertex.getX(), vertex.getY(), 0);
if (polygon == null) {
polygon = new Path2D.Double();
polygon.moveTo(vertex.getX(), vertex.getY());
} else {
polygon.lineTo(vertex.getX(), vertex.getY());
/*-
vtkLine line = new vtkLine();
line.GetPointIds().SetId(0, id0);
line.GetPointIds().SetId(1, id1);
polygonCells.InsertNextCell(line);
*/
}
// id0 = id1;
}
polygon.closePath();
/*-
vtkPolyDataWriter writer = new vtkPolyDataWriter();
writer.SetInputData(polygonPolyData);
writer.SetFileName("polygon2D.vtk");
writer.SetFileTypeToBinary();
writer.Update();
writer = new vtkPolyDataWriter();
polygonPolyData = new vtkPolyData();
polygonPolyData.SetPoints(ref2DPoints);
writer.SetInputData(polygonPolyData);
writer.SetFileName("refPoints.vtk");
writer.SetFileTypeToBinary();
writer.Update();
*/
}
public boolean isEnclosed(double[] xyz) {
Vector3D point = new Vector3D(xyz);
Point2D projected = proj.forward(point.getDelta(), point.getAlpha());
return polygon.contains(projected.getX(), projected.getY());
}
/**
* @param inputPoints points to consider
* @param scale scale factor
* @return indices of all points inside the scaled polygon
*/
public List<Integer> scalePoints(List<double[]> inputPoints, double scale) {
List<Vector2D> projected = new ArrayList<>();
for (double[] inputPoint : inputPoints) {
Vector3D point = new Vector3D(inputPoint);
Point2D projectedPoint = proj.forward(point.getDelta(), point.getAlpha());
projected.add(new Vector2D(projectedPoint.getX(), projectedPoint.getY()));
}
Vector2D center = new Vector2D(0, 0);
for (Vector2D inputPoint : projected) center = center.add(inputPoint);
center = center.scalarMultiply(1. / inputPoints.size());
List<Vector2D> translatedPoints = new ArrayList<>();
for (Vector2D inputPoint : projected) translatedPoints.add(inputPoint.subtract(center));
Path2D.Double thisPolygon = null;
MonotoneChain mc = new MonotoneChain();
Collection<Vector2D> vertices = mc.findHullVertices(translatedPoints);
for (Vector2D vertex : vertices) {
if (thisPolygon == null) {
thisPolygon = new Path2D.Double();
thisPolygon.moveTo(scale * vertex.getX(), scale * vertex.getY());
} else {
thisPolygon.lineTo(scale * vertex.getX(), scale * vertex.getY());
}
}
thisPolygon.closePath();
List<Integer> indices = new ArrayList<>();
for (int i = 0; i < projected.size(); i++) {
Vector2D inputPoint = projected.get(i);
if (thisPolygon.contains(
inputPoint.getX() - center.getX(), inputPoint.getY() - center.getY())) indices.add(i);
}
return indices;
}
private static Options defineOptions() {
Options options = new Options();
options.addOption(
Option.builder("inputFormat")
.hasArg()
.desc(
"Format of input file. If not present format will be inferred from file extension.")
.build());
options.addOption(
Option.builder("inputFile")
.required()
.hasArg()
.desc("Required. Name of input file.")
.build());
options.addOption(
Option.builder("inllr")
.desc(
"If present, input values are assumed to be lon, lat, rad. Default is x, y, z. Only used with ASCII or BINARY formats.")
.build());
options.addOption(
Option.builder("referenceFormat")
.hasArg()
.desc(
"Format of reference file. If not present format will be inferred from file extension.")
.build());
options.addOption(
Option.builder("referenceFile")
.required()
.hasArg()
.desc("Required. Name of reference file.")
.build());
options.addOption(
Option.builder("refllr")
.desc(
"If present, reference values are assumed to be lon, lat, rad. Default is x, y, z. Only used with ASCII or BINARY formats.")
.build());
options.addOption(
Option.builder("outputFormat")
.hasArg()
.desc(
"Format of output file. If not present format will be inferred from file extension.")
.build());
options.addOption(
Option.builder("outputFile")
.required()
.hasArg()
.desc("Required. Name of output file.")
.build());
options.addOption(
Option.builder("outllr")
.desc(
"If present, output values will be lon, lat, rad. Default is x, y, z. Only used with ASCII or BINARY formats.")
.build());
options.addOption(
Option.builder("scale")
.hasArg()
.desc("Value to scale bounding box containing intersect region. Default is 1.0.")
.build());
return options;
}
public static void main(String[] args)
throws SpiceException, IOException, InterruptedException, FitsException {
NativeLibraryLoader.loadVtkLibraries();
NativeLibraryLoader.loadSpiceLibraries();
TerrasaurTool defaultOBJ = new PointCloudOverlap(null);
Options options = defineOptions();
CommandLine cl = defaultOBJ.parseArgs(args, options);
Map<MessageLabel, String> startupMessages = defaultOBJ.startupMessages(cl);
for (MessageLabel ml : startupMessages.keySet())
logger.info(String.format("%s %s", ml.label, startupMessages.get(ml)));
// Read the reference file
FORMATS refFormat =
cl.hasOption("referenceFormat")
? FORMATS.valueOf(cl.getOptionValue("referenceFormat").toUpperCase())
: FORMATS.formatFromExtension(cl.getOptionValue("referenceFile"));
String refFile = cl.getOptionValue("referenceFile");
boolean refLLR = cl.hasOption("refllr");
PointCloudFormatConverter pcfc = new PointCloudFormatConverter(refFormat, FORMATS.VTK);
pcfc.read(refFile, refLLR);
vtkPoints referencePoints = pcfc.getPoints();
logger.info("{} points read from {}", referencePoints.GetNumberOfPoints(), refFile);
List<double[]> refPts = new ArrayList<>();
for (int i = 0; i < referencePoints.GetNumberOfPoints(); i++) {
refPts.add(referencePoints.GetPoint(i));
}
// create the overlap object and set the enclosing polygon
PointCloudOverlap pco = new PointCloudOverlap(refPts);
// Read the input point cloud
FORMATS inFormat =
cl.hasOption("inputFormat")
? FORMATS.valueOf(cl.getOptionValue("inputFormat").toUpperCase())
: FORMATS.formatFromExtension(cl.getOptionValue("inputFile"));
String inFile = cl.getOptionValue("inputFile");
boolean inLLR = cl.hasOption("inllr");
pcfc = new PointCloudFormatConverter(inFormat, FORMATS.VTK);
pcfc.read(inFile, inLLR);
vtkPoints inputPoints = pcfc.getPoints();
logger.info("{} points read from {}", inputPoints.GetNumberOfPoints(), inFile);
List<Integer> enclosedIndices = new ArrayList<>();
for (int i = 0; i < inputPoints.GetNumberOfPoints(); i++) {
double[] pt = inputPoints.GetPoint(i);
if (pco.isEnclosed(pt)) enclosedIndices.add(i);
}
if (cl.hasOption("scale")) {
List<double[]> pts = new ArrayList<>();
for (Integer i : enclosedIndices) pts.add(inputPoints.GetPoint(i));
// this list includes which of the enclosed points are inside the scaled polygon
List<Integer> theseIndices = pco.scalePoints(pts, Double.parseDouble(cl.getOptionValue("scale")));
// now relate this list back to the original list of points
List<Integer> newIndices = new ArrayList<>();
for (Integer i : theseIndices) newIndices.add(enclosedIndices.get(i));
enclosedIndices = newIndices;
}
VectorStatistics xyzStats = new VectorStatistics();
VectorStatistics xyStats = new VectorStatistics();
for (Integer i : enclosedIndices) {
double[] thisPt = inputPoints.GetPoint(i);
Vector3D thisPt3D = new Vector3D(thisPt);
xyzStats.add(thisPt3D);
Point2D projectedPt = pco.getProjection().forward(thisPt3D.getDelta(), thisPt3D.getAlpha());
xyStats.add(new Vector3(projectedPt.getX(), projectedPt.getY(), 0));
}
logger.info(
"Center XYZ: {}, {}, {}",
xyzStats.getMean().getX(), xyzStats.getMean().getY(), xyzStats.getMean().getZ());
Vector3D centerXYZ = xyzStats.getMean();
logger.info(
"Center lon, lat: {}, {}\n",
Math.toDegrees(centerXYZ.getAlpha()), Math.toDegrees(centerXYZ.getDelta()));
logger.info(
"xmin/xmax/extent: {}/{}/{}\n",
xyzStats.getMin().getX(),
xyzStats.getMax().getX(),
xyzStats.getMax().getX() - xyzStats.getMin().getX());
logger.info(
"ymin/ymax/extent: {}/{}/{}\n",
xyzStats.getMin().getY(),
xyzStats.getMax().getY(),
xyzStats.getMax().getY() - xyzStats.getMin().getY());
logger.info(
"zmin/zmax/extent: {}/{}/{}\n",
xyzStats.getMin().getZ(),
xyzStats.getMax().getZ(),
xyzStats.getMax().getZ() - xyzStats.getMin().getZ());
FORMATS outFormat =
cl.hasOption("outputFormat")
? FORMATS.valueOf(cl.getOptionValue("outputFormat").toUpperCase())
: FORMATS.formatFromExtension(cl.getOptionValue("outputFile"));
vtkPoints pointsToWrite = new vtkPoints();
for (Integer i : enclosedIndices) pointsToWrite.InsertNextPoint(inputPoints.GetPoint(i));
pcfc = new PointCloudFormatConverter(FORMATS.VTK, outFormat);
pcfc.setPoints(pointsToWrite);
String outputFilename = cl.getOptionValue("outputFile");
pcfc.write(outputFilename, cl.hasOption("outllr"));
if (new File(outputFilename).exists()) {
logger.info(
"{} points written to {}",
pointsToWrite.GetNumberOfPoints(), outputFilename);
} else {
logger.error("Could not write {}", outputFilename);
}
logger.info("Finished");
}
}
// TODO write out center of output pointcloud

View File

@@ -313,7 +313,7 @@ public class RenderShapeFromSumFile implements TerrasaurTool {
logger.printf(
Level.DEBUG,
"Thread %d lat/lon %.2f/%.2f, %s, sum %f, cells %d, %.2f",
Thread.currentThread().getId(),
Thread.currentThread().threadId(),
Math.toDegrees(intersectPoint.getDelta()),
Math.toDegrees(intersectPoint.getAlpha()),
intersectPoint.toString(),
@@ -338,7 +338,7 @@ public class RenderShapeFromSumFile implements TerrasaurTool {
@Override
public Map<Integer, Brightness> call() throws Exception {
logger.info("Thread {}: starting", Thread.currentThread().getId());
logger.info("Thread {}: starting", Thread.currentThread().threadId());
int xPixels = subPixel * nPixelsX;
@@ -380,7 +380,7 @@ public class RenderShapeFromSumFile implements TerrasaurTool {
logger.debug(
String.format(
"Thread %d: No intersection with local model for pixel (%d,%d): lat/lon %.2f/%.2f, using global intersection %d %s",
Thread.currentThread().getId(),
Thread.currentThread().threadId(),
i,
j,
Math.toDegrees(intersectPt3D.getDelta()),
@@ -397,7 +397,7 @@ public class RenderShapeFromSumFile implements TerrasaurTool {
}
}
logger.info("Thread {}: finished", Thread.currentThread().getId());
logger.info("Thread {}: finished", Thread.currentThread().threadId());
return brightness;
}

View File

@@ -0,0 +1,166 @@
/*
* The MIT License
* Copyright © 2025 Johns Hopkins University Applied Physics Laboratory
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package terrasaur.apps;
import java.io.*;
import java.util.Map;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import terrasaur.templates.TerrasaurTool;
import terrasaur.utils.ICQUtils;
import terrasaur.utils.NativeLibraryLoader;
import terrasaur.utils.PolyDataUtil;
import vtk.vtkPolyData;
public class TriAx implements TerrasaurTool {
private static final Logger logger = LogManager.getLogger();
private TriAx() {}
@Override
public String shortDescription() {
return "Generate a triaxial ellipsoid in ICQ format.";
}
@Override
public String fullDescription(Options options) {
String footer =
"\nTriAx is an implementation of the SPC tool TRIAX, which generates a triaxial ellipsoid in ICQ format.\n";
return TerrasaurTool.super.fullDescription(options, "", footer);
}
static Options defineOptions() {
Options options = TerrasaurTool.defineOptions();
options.addOption(
Option.builder("A")
.required()
.hasArg()
.desc("Long axis of the ellipsoid, arbitrary units (usually assumed to be km).")
.build());
options.addOption(
Option.builder("B")
.required()
.hasArg()
.desc("Medium axis of the ellipsoid, arbitrary units (usually assumed to be km).")
.build());
options.addOption(
Option.builder("C")
.required()
.hasArg()
.desc("Short axis of the ellipsoid, arbitrary units (usually assumed to be km).")
.build());
options.addOption(
Option.builder("Q")
.required()
.hasArg()
.desc("ICQ size parameter. This is conventionally but not necessarily a power of 2.")
.build());
options.addOption(
Option.builder("saveOBJ")
.desc(
"If present, save in OBJ format as well. "
+ "File will have the same name as ICQ file with an OBJ extension.")
.build());
options.addOption(
Option.builder("output").hasArg().required().desc("Name of ICQ file to write.").build());
return options;
}
static final int MAX_Q = 512;
public static void main(String[] args) {
TerrasaurTool defaultOBJ = new TriAx();
Options options = defineOptions();
CommandLine cl = defaultOBJ.parseArgs(args, options);
Map<MessageLabel, String> startupMessages = defaultOBJ.startupMessages(cl);
for (MessageLabel ml : startupMessages.keySet())
logger.info("{} {}", ml.label, startupMessages.get(ml));
int q = Integer.parseInt(cl.getOptionValue("Q"));
String shapefile = cl.getOptionValue("output");
double[] ax = new double[3];
ax[0] = Double.parseDouble(cl.getOptionValue("A"));
ax[1] = Double.parseDouble(cl.getOptionValue("B"));
ax[2] = Double.parseDouble(cl.getOptionValue("C"));
double[][][][] vec = new double[3][MAX_Q + 1][MAX_Q + 1][6];
for (int f = 0; f < 6; f++) {
for (int i = 0; i <= q; i++) {
for (int j = 0; j <= q; j++) {
double[] u = ICQUtils.xyf2u(q, i, j, f, ax);
double z =
1
/ Math.sqrt(
Math.pow(u[0] / ax[0], 2)
+ Math.pow(u[1] / ax[1], 2)
+ Math.pow(u[2] / ax[2], 2));
double[] v = new Vector3D(u).scalarMultiply(z).toArray();
for (int k = 0; k < 3; k++) {
vec[k][i][j][f] = v[k];
}
}
}
}
ICQUtils.writeICQ(q, vec, shapefile);
if (cl.hasOption("saveOBJ")) {
String basename = FilenameUtils.getBaseName(shapefile);
String dirname = FilenameUtils.getFullPath(shapefile);
if (dirname.isEmpty()) dirname = ".";
File obj = new File(dirname, basename + ".obj");
NativeLibraryLoader.loadVtkLibraries();
try {
vtkPolyData polydata = PolyDataUtil.loadShapeModel(shapefile);
if (polydata == null) {
logger.error("Cannot read {}", shapefile);
System.exit(0);
}
polydata = PolyDataUtil.removeDuplicatePoints(polydata);
polydata = PolyDataUtil.removeUnreferencedPoints(polydata);
polydata = PolyDataUtil.removeZeroAreaFacets(polydata);
PolyDataUtil.saveShapeModelAsOBJ(polydata, obj.getPath());
} catch (Exception e) {
logger.error(e);
}
}
}
}

View File

@@ -30,7 +30,6 @@ import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
@@ -65,6 +64,12 @@ public class ValidateNormals implements TerrasaurTool {
static Options defineOptions() {
Options options = TerrasaurTool.defineOptions();
options.addOption(
Option.builder("fast")
.desc("If present, only check for overhangs if center and normal point in opposite " +
"directions. Default behavior is to always check for intersections between body center " +
"and facet center.")
.build());
options.addOption(
Option.builder("origin")
.hasArg()
@@ -126,10 +131,12 @@ public class ValidateNormals implements TerrasaurTool {
private final long index0;
private final long index1;
private final boolean fast;
public FlippedNormalFinder(long index0, long index1) {
public FlippedNormalFinder(long index0, long index1, boolean fast) {
this.index0 = index0;
this.index1 = index1;
this.fast = fast;
}
@Override
@@ -158,20 +165,21 @@ public class ValidateNormals implements TerrasaurTool {
long index = index0 + i;
CellInfo ci = CellInfo.getCellInfo(polyData, index, idList);
getOBBTree().IntersectWithLine(origin, ci.center().toArray(), null, cellIds);
boolean isOpposite = (ci.center().dotProduct(ci.normal()) < 0);
// count up all crossings of the surface between the origin and the facet.
int numCrossings = 0;
for (int j = 0; j < cellIds.GetNumberOfIds(); j++) {
if (cellIds.GetId(j) == index) break;
numCrossings++;
if (isOpposite || !fast) {
// count up all crossings of the surface between the origin and the facet.
getOBBTree().IntersectWithLine(origin, ci.center().toArray(), null, cellIds);
for (int j = 0; j < cellIds.GetNumberOfIds(); j++) {
if (cellIds.GetId(j) == index) break;
numCrossings++;
}
}
// if numCrossings is even, the radial and normal should point in the same direction. If it
// is odd, the
// radial and normal should point in opposite directions.
// is odd, the radial and normal should point in opposite directions.
boolean shouldBeOpposite = (numCrossings % 2 == 1);
boolean isOpposite = (ci.center().dotProduct(ci.normal()) < 0);
// XOR operator - true if both conditions are different
if (isOpposite ^ shouldBeOpposite) flippedNormals.add(index);
@@ -208,7 +216,7 @@ public class ValidateNormals implements TerrasaurTool {
Map<MessageLabel, String> startupMessages = defaultOBJ.startupMessages(cl);
for (MessageLabel ml : startupMessages.keySet())
logger.info(String.format("%s %s", ml.label, startupMessages.get(ml)));
logger.info("{} {}", ml.label, startupMessages.get(ml));
NativeLibraryLoader.loadVtkLibraries();
@@ -234,6 +242,7 @@ public class ValidateNormals implements TerrasaurTool {
Set<Long> flippedNormals = new HashSet<>();
boolean fast = cl.hasOption("fast");
int numThreads =
cl.hasOption("numThreads") ? Integer.parseInt(cl.getOptionValue("numThreads")) : 1;
try (ExecutorService executor = Executors.newFixedThreadPool(numThreads)) {
@@ -244,7 +253,7 @@ public class ValidateNormals implements TerrasaurTool {
long fromIndex = i * numFacets;
long toIndex = Math.min(polyData.GetNumberOfCells(), fromIndex + numFacets);
FlippedNormalFinder fnf = app.new FlippedNormalFinder(fromIndex, toIndex);
FlippedNormalFinder fnf = app.new FlippedNormalFinder(fromIndex, toIndex, fast);
futures.add(executor.submit(fnf));
}

View File

@@ -308,7 +308,7 @@ public class ValidateOBJ implements TerrasaurTool {
TriangularFacet facet =
new TriangularFacet(new VectorIJK(pt0), new VectorIJK(pt1), new VectorIJK(pt2));
if (facet.getArea() > 0) {
stats.addValue(facet.getCenter().getDot(facet.getNormal()));
stats.addValue(facet.getCenter().createUnitized().getDot(facet.getNormal()));
cStats.add(facet.getCenter());
nStats.add(facet.getNormal());
}
@@ -374,7 +374,7 @@ public class ValidateOBJ implements TerrasaurTool {
Map<MessageLabel, String> startupMessages = defaultOBJ.startupMessages(cl);
for (MessageLabel ml : startupMessages.keySet())
logger.info(String.format("%s %s", ml.label, startupMessages.get(ml)));
logger.info("{} {}", ml.label, startupMessages.get(ml));
NativeLibraryLoader.loadVtkLibraries();
vtkPolyData polyData = PolyDataUtil.loadShapeModel(cl.getOptionValue("obj"));
@@ -418,7 +418,7 @@ public class ValidateOBJ implements TerrasaurTool {
if (cl.hasOption("output")) {
PolyDataUtil.saveShapeModelAsOBJ(polyData, cl.getOptionValue("output"));
logger.info(String.format("Wrote OBJ file %s", cl.getOptionValue("output")));
logger.info("Wrote OBJ file {}", cl.getOptionValue("output"));
}
}
}

View File

@@ -129,7 +129,7 @@ public abstract class CellInfo {
polydata.GetPoint(idList.GetId(2), pt2);
}
private static CellInfo fromPoints(double[] pt0, double[] pt1, double[] pt2) {
public static CellInfo fromPoints(double[] pt0, double[] pt1, double[] pt2) {
ImmutableCellInfo.Builder builder = ImmutableCellInfo.builder();
builder.pt0(new Vector3D(pt0));

View File

@@ -0,0 +1,111 @@
/*
* The MIT License
* Copyright © 2025 Johns Hopkins University Applied Physics Laboratory
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package terrasaur.utils;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
public class ICQUtils {
private static final Logger logger = LogManager.getLogger();
public static double[] xyf2u(int q, double x, double y, int face, double[] ax) {
double pi = Math.acos(-1.0);
double[] dt = new double[3];
double[] v = new double[3];
int[][][] u = defu();
dt[0] = Math.tan((2 * x / q - 1) * pi / 4);
dt[1] = Math.tan((2 * y / q - 1) * pi / 4);
dt[2] = 1 / Math.sqrt(1 + dt[0] * dt[0] + dt[1] * dt[1]);
dt[0] *= dt[2];
dt[1] *= dt[2];
for (int k = 0; k < 3; k++) {
for (int j = 0; j < 3; j++) {
int sign = u[j][k][face];
if (sign != 0) {
v[k] = dt[j] * sign * Math.sqrt(ax[k]);
}
}
}
return new Vector3D(v).normalize().toArray();
}
static int[][][] defu() {
int[][][] u = new int[3][3][6];
u[0][0][0] = 1;
u[1][1][0] = -1;
u[2][2][0] = 1;
u[0][0][1] = 1;
u[1][2][1] = -1;
u[2][1][1] = -1;
u[0][1][2] = -1;
u[1][2][2] = -1;
u[2][0][2] = -1;
u[0][0][3] = -1;
u[1][2][3] = -1;
u[2][1][3] = 1;
u[0][1][4] = 1;
u[1][2][4] = -1;
u[2][0][4] = 1;
u[0][0][5] = 1;
u[1][1][5] = 1;
u[2][2][5] = -1;
return u;
}
public static void writeICQ(int q, double[][][][] vec, String filename){
try (PrintWriter out = new PrintWriter(new FileWriter(filename))) {
out.println(q);
for (int f = 0; f < 6; f++) {
for (int j = 0; j <= q; j++) {
for (int i = 0; i <= q; i++) {
for (int k = 0; k < 3; k++) {
out.printf("%12.5f", vec[k][i][j][f]);
}
out.println();
}
}
}
} catch (IOException e) {
logger.error(e);
}
}
}

View File

@@ -521,17 +521,17 @@ public class PolyDataUtil {
n[q][q][4] = n[0][q][3];
n[q][q][5] = n[0][q][3];
try (PrintWriter pw = new PrintWriter("java.txt")) {
for (int f = 0; f < 6; f++) {
for (int i = 0; i < q; i++) {
for (int j = 0; j < q; j++) {
pw.printf("%3d%3d%3d%6d\n", i, j, f + 1, n[i][j][f]);
}
}
}
} catch (FileNotFoundException e) {
logger.error(e.getLocalizedMessage());
}
// try (PrintWriter pw = new PrintWriter("java.txt")) {
// for (int f = 0; f < 6; f++) {
// for (int i = 0; i < q; i++) {
// for (int j = 0; j < q; j++) {
// pw.printf("%3d%3d%3d%6d\n", i, j, f + 1, n[i][j][f]);
// }
// }
// }
// } catch (FileNotFoundException e) {
// logger.error(e.getLocalizedMessage());
// }
pltLines.add(String.format("%d", 12 * q * q));
n0 = 0;

View File

@@ -29,32 +29,35 @@ import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
import net.jafama.FastMath;
import org.apache.commons.io.FileUtils;
import org.apache.commons.math3.geometry.euclidean.threed.Rotation;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.immutables.value.Value;
import net.jafama.FastMath;
import terrasaur.utils.ImmutableSumFile.Builder;
import terrasaur.utils.math.MathConversions;
import picante.math.vectorspace.RotationMatrixIJK;
import picante.math.vectorspace.VectorIJK;
import picante.mechanics.*;
import picante.mechanics.providers.aberrated.AberrationCorrection;
import spice.basic.Plane;
import spice.basic.Ray;
import spice.basic.RayPlaneIntercept;
import spice.basic.SpiceException;
import spice.basic.Vector3;
import terrasaur.utils.ImmutableSumFile.Builder;
import terrasaur.utils.math.MathConversions;
import terrasaur.utils.spice.SpiceBundle;
/**
* Class describing Bob Gaskell's sum file object
*
* @author nairah1
*
* @author nairah1
*/
@Value.Immutable
public abstract class SumFile {
private final static Logger logger = LogManager.getLogger(SumFile.class);
private static final Logger logger = LogManager.getLogger(SumFile.class);
// line 1
public abstract String picnm();
@@ -107,23 +110,63 @@ public abstract class SumFile {
// line 13
public abstract Vector3D sig_ptg();
@Nullable
public abstract Vector3D frustum1();
@Nullable
public abstract Vector3D frustum2();
@Nullable
public abstract Vector3D frustum3();
@Nullable
public abstract Vector3D frustum4();
/**
*
* @param translation
* @param rotation
* @return A SumFile with the scobj transformed and the cx, cy, cz, and sz vectors rotated.
* @param bundle SPICE bundle
* @param observer spacecraft
* @param target target
* @param cameraFrame Camera Frame
* @param t time
* @return SumFile with scobj, C matrix, and sun direction evaluated using SPICE at time t
*/
public SumFile fromSpice(
SpiceBundle bundle, EphemerisID observer, EphemerisID target, FrameID cameraFrame, double t) {
FrameID bodyFixedFrame = bundle.getBodyFixedFrame(target);
VectorIJK scobj =
bundle
.getAbProvider()
.createAberratedPositionVectorFunction(
target, observer, bodyFixedFrame, Coverage.ALL_TIME, AberrationCorrection.LT_S)
.getPosition(t);
RotationMatrixIJK cMatrix =
bundle
.getAbProvider()
.getFrameProvider()
.createFrameTransformFunction(cameraFrame, bodyFixedFrame, Coverage.ALL_TIME)
.getTransform(t);
VectorIJK sunDir =
bundle
.getAbProvider()
.createAberratedPositionVectorFunction(
CelestialBodies.SUN,
target,
bodyFixedFrame,
Coverage.ALL_TIME,
AberrationCorrection.LT_S)
.getPosition(t)
.unitize();
Builder b = ImmutableSumFile.builder().from(this);
b.utcString(bundle.getTimeConversion().tdbToUTCString(t, "C"));
b.scobj(MathConversions.toVector3D(scobj));
b.cx(MathConversions.toVector3D(cMatrix.mxv(VectorIJK.I)));
b.cy(MathConversions.toVector3D(cMatrix.mxv(VectorIJK.J)));
b.cz(MathConversions.toVector3D(cMatrix.mxv(VectorIJK.K)));
b.sz(MathConversions.toVector3D(cMatrix.mxv(sunDir)));
return b.build();
}
/**
* @param translation translation to apply to scobj
* @param rotation rotation to apply to C matrix and sun direction
* @return SumFile with the scobj transformed and the cx, cy, cz, and sz vectors rotated.
*/
public SumFile transform(Vector3D translation, Rotation rotation) {
Builder b = ImmutableSumFile.builder().from(this);
@@ -141,10 +184,8 @@ public abstract class SumFile {
}
/**
* Construct a SumFile from the input file
*
* @param file
* @return
* @param file file to read
* @return SumFile
*/
public static SumFile fromFile(File file) {
SumFile s = null;
@@ -157,10 +198,8 @@ public abstract class SumFile {
}
/**
* Construct a SumFile from the input string list
*
* @param lines
* @return
* @param lines lines to read
* @return SumFile
*/
public static SumFile fromLines(List<String> lines) {
Builder b = ImmutableSumFile.builder();
@@ -180,95 +219,114 @@ public abstract class SumFile {
parts = lines.get(4).trim().split("\\s+");
double[] tmp = new double[3];
for (int i = 0; i < 3; i++)
tmp[i] = parseFortranDouble(parts[i]);
for (int i = 0; i < 3; i++) tmp[i] = parseFortranDouble(parts[i]);
b.scobj(new Vector3D(tmp));
parts = lines.get(5).trim().split("\\s+");
tmp = new double[3];
for (int i = 0; i < 3; i++)
tmp[i] = parseFortranDouble(parts[i]);
for (int i = 0; i < 3; i++) tmp[i] = parseFortranDouble(parts[i]);
b.cx(new Vector3D(tmp).normalize());
parts = lines.get(6).trim().split("\\s+");
tmp = new double[3];
for (int i = 0; i < 3; i++)
tmp[i] = parseFortranDouble(parts[i]);
for (int i = 0; i < 3; i++) tmp[i] = parseFortranDouble(parts[i]);
b.cy(new Vector3D(tmp).normalize());
parts = lines.get(7).trim().split("\\s+");
tmp = new double[3];
for (int i = 0; i < 3; i++)
tmp[i] = parseFortranDouble(parts[i]);
for (int i = 0; i < 3; i++) tmp[i] = parseFortranDouble(parts[i]);
b.cz(new Vector3D(tmp).normalize());
parts = lines.get(8).trim().split("\\s+");
tmp = new double[3];
for (int i = 0; i < 3; i++)
tmp[i] = parseFortranDouble(parts[i]);
for (int i = 0; i < 3; i++) tmp[i] = parseFortranDouble(parts[i]);
b.sz(new Vector3D(tmp).normalize());
parts = lines.get(9).trim().split("\\s+");
tmp = new double[3];
for (int i = 0; i < 3; i++)
tmp[i] = parseFortranDouble(parts[i]);
for (int i = 0; i < 3; i++) tmp[i] = parseFortranDouble(parts[i]);
b.kmat1(new Vector3D(tmp));
tmp = new double[3];
for (int i = 0; i < 3; i++)
tmp[i] = parseFortranDouble(parts[i + 3]);
for (int i = 0; i < 3; i++) tmp[i] = parseFortranDouble(parts[i + 3]);
b.kmat2(new Vector3D(tmp));
parts = lines.get(10).trim().split("\\s+");
for (int i = 0; i < 4; i++)
b.addDistortion(parseFortranDouble(parts[i]));
for (int i = 0; i < 4; i++) b.addDistortion(parseFortranDouble(parts[i]));
parts = lines.get(11).trim().split("\\s+");
tmp = new double[3];
for (int i = 0; i < 3; i++)
tmp[i] = parseFortranDouble(parts[i]);
for (int i = 0; i < 3; i++) tmp[i] = parseFortranDouble(parts[i]);
b.sig_vso(new Vector3D(tmp));
parts = lines.get(11).trim().split("\\s+");
tmp = new double[3];
for (int i = 0; i < 3; i++)
tmp[i] = parseFortranDouble(parts[i]);
for (int i = 0; i < 3; i++) tmp[i] = parseFortranDouble(parts[i]);
b.sig_ptg(new Vector3D(tmp));
b.frustum1(Vector3D.ZERO);
b.frustum2(Vector3D.ZERO);
b.frustum3(Vector3D.ZERO);
b.frustum4(Vector3D.ZERO);
SumFile s = b.build();
double fov1 = Math.abs(Math.atan(s.npx() / (2.0 * s.mmfl() * s.kmat1().getX())));
double fov2 = Math.abs(Math.atan(s.nln() / (2.0 * s.mmfl() * s.kmat2().getY())));
Vector3D cornerVector = new Vector3D(-FastMath.tan(fov1), -FastMath.tan(fov2), 1.0);
double fx = cornerVector.getX() * s.cx().getX() + cornerVector.getY() * s.cy().getX()
+ cornerVector.getZ() * s.cz().getX();
double fy = cornerVector.getX() * s.cx().getY() + cornerVector.getY() * s.cy().getY()
+ cornerVector.getZ() * s.cz().getY();
double fz = cornerVector.getX() * s.cx().getZ() + cornerVector.getY() * s.cy().getZ()
+ cornerVector.getZ() * s.cz().getZ();
double fx =
cornerVector.getX() * s.cx().getX()
+ cornerVector.getY() * s.cy().getX()
+ cornerVector.getZ() * s.cz().getX();
double fy =
cornerVector.getX() * s.cx().getY()
+ cornerVector.getY() * s.cy().getY()
+ cornerVector.getZ() * s.cz().getY();
double fz =
cornerVector.getX() * s.cx().getZ()
+ cornerVector.getY() * s.cy().getZ()
+ cornerVector.getZ() * s.cz().getZ();
b.frustum3(new Vector3D(fx, fy, fz).normalize());
fx = -cornerVector.getX() * s.cx().getX() + cornerVector.getY() * s.cy().getX()
+ cornerVector.getZ() * s.cz().getX();
fy = -cornerVector.getX() * s.cx().getY() + cornerVector.getY() * s.cy().getY()
+ cornerVector.getZ() * s.cz().getY();
fz = -cornerVector.getX() * s.cx().getZ() + cornerVector.getY() * s.cy().getZ()
+ cornerVector.getZ() * s.cz().getZ();
fx =
-cornerVector.getX() * s.cx().getX()
+ cornerVector.getY() * s.cy().getX()
+ cornerVector.getZ() * s.cz().getX();
fy =
-cornerVector.getX() * s.cx().getY()
+ cornerVector.getY() * s.cy().getY()
+ cornerVector.getZ() * s.cz().getY();
fz =
-cornerVector.getX() * s.cx().getZ()
+ cornerVector.getY() * s.cy().getZ()
+ cornerVector.getZ() * s.cz().getZ();
b.frustum4(new Vector3D(fx, fy, fz).normalize());
fx = cornerVector.getX() * s.cx().getX() - cornerVector.getY() * s.cy().getX()
+ cornerVector.getZ() * s.cz().getX();
fy = cornerVector.getX() * s.cx().getY() - cornerVector.getY() * s.cy().getY()
+ cornerVector.getZ() * s.cz().getY();
fz = cornerVector.getX() * s.cx().getZ() - cornerVector.getY() * s.cy().getZ()
+ cornerVector.getZ() * s.cz().getZ();
fx =
cornerVector.getX() * s.cx().getX()
- cornerVector.getY() * s.cy().getX()
+ cornerVector.getZ() * s.cz().getX();
fy =
cornerVector.getX() * s.cx().getY()
- cornerVector.getY() * s.cy().getY()
+ cornerVector.getZ() * s.cz().getY();
fz =
cornerVector.getX() * s.cx().getZ()
- cornerVector.getY() * s.cy().getZ()
+ cornerVector.getZ() * s.cz().getZ();
b.frustum1(new Vector3D(fx, fy, fz).normalize());
fx = -cornerVector.getX() * s.cx().getX() - cornerVector.getY() * s.cy().getX()
+ cornerVector.getZ() * s.cz().getX();
fy = -cornerVector.getX() * s.cx().getY() - cornerVector.getY() * s.cy().getY()
+ cornerVector.getZ() * s.cz().getY();
fz = -cornerVector.getX() * s.cx().getZ() - cornerVector.getY() * s.cy().getZ()
+ cornerVector.getZ() * s.cz().getZ();
fx =
-cornerVector.getX() * s.cx().getX()
- cornerVector.getY() * s.cy().getX()
+ cornerVector.getZ() * s.cz().getX();
fy =
-cornerVector.getX() * s.cx().getY()
- cornerVector.getY() * s.cy().getY()
+ cornerVector.getZ() * s.cz().getY();
fz =
-cornerVector.getX() * s.cx().getZ()
- cornerVector.getY() * s.cy().getZ()
+ cornerVector.getZ() * s.cz().getZ();
b.frustum2(new Vector3D(fx, fy, fz).normalize());
return b.build();
@@ -277,17 +335,15 @@ public abstract class SumFile {
/**
* Account for numbers of the form .1192696009D+03 rather than .1192696009E+03 (i.e. a D instead
* of an E). This function replaces D's with E's.
*
* @param s
* @return
*
* @param s String
* @return input string with all instances of 'D' replaced with 'E'
*/
private static double parseFortranDouble(String s) {
return Double.parseDouble(s.replace('D', 'E'));
}
/**
* Write the sum file object to a string
*/
/** Write the sum file object to a string */
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@@ -297,25 +353,52 @@ public abstract class SumFile {
String.format("%6d%6d%6d%6d%56s\n", npx(), nln(), t1(), t2(), " NPX, NLN, THRSH "));
sb.append(
String.format("%20.10e%20.10e%20.10e%20s\n", mmfl(), px0(), ln0(), " MMFL, CTR "));
sb.append(String.format("%20.10e%20.10e%20.10e%20s\n", scobj().getX(), scobj().getY(),
scobj().getZ(), " SCOBJ "));
sb.append(String.format("%20.10e%20.10e%20.10e%20s\n", cx().getX(), cx().getY(), cx().getZ(),
" CX "));
sb.append(String.format("%20.10e%20.10e%20.10e%20s\n", cy().getX(), cy().getY(), cy().getZ(),
" CY "));
sb.append(String.format("%20.10e%20.10e%20.10e%20s\n", cz().getX(), cz().getY(), cz().getZ(),
" CZ "));
sb.append(String.format("%20.10e%20.10e%20.10e%20s\n", sz().getX(), sz().getY(), sz().getZ(),
" SZ "));
sb.append(String.format("%10.4f%10.4f%10.4f%10.4f%10.4f%10.4f%20s\n", kmat1().getX(),
kmat1().getY(), kmat1().getZ(), kmat2().getX(), kmat2().getY(), kmat2().getZ(),
" K-MATRIX "));
sb.append(String.format("%15.5f%15.5f%15.5f%15.5f%20s\n", distortion().get(0),
distortion().get(1), distortion().get(2), distortion().get(3), " DISTORTION "));
sb.append(String.format("%20.10e%20.10e%20.10e%20s\n", sig_vso().getX(), sig_vso().getY(),
sig_vso().getZ(), " SIGMA_VSO "));
sb.append(String.format("%20.10e%20.10e%20.10e%20s\n", sig_ptg().getX(), sig_ptg().getY(),
sig_ptg().getZ(), " SIGMA_PTG "));
sb.append(
String.format(
"%20.10e%20.10e%20.10e%20s\n",
scobj().getX(), scobj().getY(), scobj().getZ(), " SCOBJ "));
sb.append(
String.format(
"%20.10e%20.10e%20.10e%20s\n",
cx().getX(), cx().getY(), cx().getZ(), " CX "));
sb.append(
String.format(
"%20.10e%20.10e%20.10e%20s\n",
cy().getX(), cy().getY(), cy().getZ(), " CY "));
sb.append(
String.format(
"%20.10e%20.10e%20.10e%20s\n",
cz().getX(), cz().getY(), cz().getZ(), " CZ "));
sb.append(
String.format(
"%20.10e%20.10e%20.10e%20s\n",
sz().getX(), sz().getY(), sz().getZ(), " SZ "));
sb.append(
String.format(
"%10.4f%10.4f%10.4f%10.4f%10.4f%10.4f%20s\n",
kmat1().getX(),
kmat1().getY(),
kmat1().getZ(),
kmat2().getX(),
kmat2().getY(),
kmat2().getZ(),
" K-MATRIX "));
sb.append(
String.format(
"%15.5f%15.5f%15.5f%15.5f%20s\n",
distortion().get(0),
distortion().get(1),
distortion().get(2),
distortion().get(3),
" DISTORTION "));
sb.append(
String.format(
"%20.10e%20.10e%20.10e%20s\n",
sig_vso().getX(), sig_vso().getY(), sig_vso().getZ(), " SIGMA_VSO "));
sb.append(
String.format(
"%20.10e%20.10e%20.10e%20s\n",
sig_ptg().getX(), sig_ptg().getY(), sig_ptg().getZ(), " SIGMA_PTG "));
sb.append(String.format("%s\n", "LANDMARKS"));
sb.append(String.format("%s\n", "LIMB FITS"));
sb.append(String.format("%s\n", "END FILE"));
@@ -324,95 +407,77 @@ public abstract class SumFile {
}
/**
* Boresight direction. This is the same as {@link #cz()}.
*
* @return
* @return Boresight direction. This is the same as {@link #cz()}.
*/
public Vector3D boresight() {
return cz();
}
/**
* Sun direction. This is the same as {@link #sz()}.
*
* @return
* @return Sun direction. This is the same as {@link #sz()}.
*/
public Vector3D sunDirection() {
return sz();
}
/**
* <pre>
* return new Vector3D(1. / npx(), frustum3().subtract(frustum4()))
* </pre>
*
* @return
* <pre>new Vector3D(1. / npx(), frustum3().subtract(frustum4()))</pre>
*/
public Vector3D xPerPixel() {
return new Vector3D(1. / npx(), frustum3().subtract(frustum4()));
}
/**
* <pre>
* return new Vector3D(1. / nln(), frustum3().subtract(frustum1()));
* </pre>
*
* @return
* <pre> new Vector3D(1. / nln(), frustum3().subtract(frustum1()));
* </pre>
*/
public Vector3D yPerPixel() {
return new Vector3D(1. / nln(), frustum3().subtract(frustum1()));
}
/**
* Angular size per pixel in the X direction.
*
* @return
* @return Angular size per pixel in the X direction calculated using
* <pre>Vector3D.angle(frustum3(), frustum4()) / npx()</pre>
*/
public double horizontalResolution() {
return Vector3D.angle(frustum3(), frustum4()) / npx();
}
/**
* Angular size per pixel in the Y direction.
*
* @return
* @return Angular size per pixel in the Y direction calculated using
* <pre>Vector3D.angle(frustum3(), frustum1()) / nln()</pre>
*/
public double verticalResolution() {
return Vector3D.angle(frustum3(), frustum1()) / nln();
}
/**
* Height of the image in pixels.
*
* @return
* @return Height of the image in pixels.
*/
public int imageHeight() {
double kmatrix00 = Math.abs(kmat1().getX());
double kmatrix11 = Math.abs(kmat2().getY());
int imageHeight = nln();
if (kmatrix00 > kmatrix11)
imageHeight = (int) Math.round(nln() * (kmatrix00 / kmatrix11));
if (kmatrix00 > kmatrix11) imageHeight = (int) Math.round(nln() * (kmatrix00 / kmatrix11));
return imageHeight;
}
/**
* Width of the image in pixels.
*
* @return
* @return Width of the image in pixels.
*/
public int imageWidth() {
double kmatrix00 = Math.abs(kmat1().getX());
double kmatrix11 = Math.abs(kmat2().getY());
int imageWidth = npx();
if (kmatrix11 > kmatrix00)
imageWidth = (int) Math.round(npx() * (kmatrix11 / kmatrix00));
if (kmatrix11 > kmatrix00) imageWidth = (int) Math.round(npx() * (kmatrix11 / kmatrix00));
return imageWidth;
}
/**
* Get the rotation to convert from body fixed coordinates to camera coordinates. {@link #cx()},
* {@link #cx()}, {@link #cz()} are the rows of this matrix.
*
* @return rotation to convert from body fixed coordinates to camera coordinates. {@link #cx()},
* {@link #cx()}, {@link #cz()} are the rows of this matrix.
*/
public Rotation getBodyFixedToCamera() {
double[][] m = new double[3][];
@@ -423,9 +488,8 @@ public abstract class SumFile {
}
/**
*
* @param directions from the spacecraft, in the body fixed frame.
* @return
* @return list of booleans set to true if direction is in the field of view, or false if not
* @throws SpiceException
*/
public List<Boolean> isInFOV(List<Vector3> directions) throws SpiceException {
@@ -450,10 +514,10 @@ public abstract class SumFile {
}
Path2D.Double shape = new Path2D.Double();
shape.moveTo(xAxis.dot(points.get(0)), yAxis.dot(points.get(0)));
shape.moveTo(xAxis.dot(points.getFirst()), yAxis.dot(points.getFirst()));
for (int i = 1; i < points.size(); i++)
shape.lineTo(xAxis.dot(points.get(i)), yAxis.dot(points.get(i)));
shape.lineTo(xAxis.dot(points.get(0)), yAxis.dot(points.get(0)));
shape.lineTo(xAxis.dot(points.getFirst()), yAxis.dot(points.getFirst()));
for (Vector3 direction : directions) {
RayPlaneIntercept rpi =
@@ -464,5 +528,4 @@ public abstract class SumFile {
return Collections.unmodifiableList(isInFOV);
}
}

View File

@@ -80,6 +80,7 @@ public class FibonacciSphere implements SphericalTessellation {
}
/**
* Get statistics on the distances between each point and its closest neighbor
*
* @return statistics on the distances between each point and its closest neighbor
*/
@@ -181,9 +182,10 @@ public class FibonacciSphere implements SphericalTessellation {
}
/**
* Get the nearest point from the desired location.
*
* @param lv input location
* @return key is distance to tile center in radians, value is tile index
* @return key is distance in radians, value is point index
*/
public Map.Entry<Double, Long> getNearest(LatitudinalVector lv) {
return getNearest(CoordConverters.convert(lv));
@@ -198,7 +200,7 @@ public class FibonacciSphere implements SphericalTessellation {
*
*
* @param ijk cartesian coordinates
* @return key is distance to tile center in radians, value is tile index
* @return key is distance in radians, value is point index
*/
public Map.Entry<Double, Long> getNearest(UnwritableVectorIJK ijk) {
final long n = getNumTiles();
@@ -248,7 +250,7 @@ public class FibonacciSphere implements SphericalTessellation {
}
}
return new AbstractMap.SimpleEntry<>(Math.sqrt(d), j);
return new AbstractMap.SimpleEntry<Double, Long>(Math.sqrt(d), j);
}
private MatrixIJ getLocalToGlobalTransform(UnwritableVectorIJK p) {
@@ -282,8 +284,8 @@ public class FibonacciSphere implements SphericalTessellation {
/**
*
* @param i tile index
* @return distance to this tile's closest neighbor center
* @param i point index
* @return distance to this point's closest neighbor
*/
public Double getDist(int i) {
return getClosestNeighborDistance().get(i);
@@ -292,7 +294,7 @@ public class FibonacciSphere implements SphericalTessellation {
/**
*
* @param lv input point
* @return map of tile indices sorted by distance from the input point
* @return map of points sorted by distance from the input point
*/
public NavigableMap<Double, Integer> getDistanceMap(LatitudinalVector lv) {
UnwritableVectorIJK ijk = CoordConverters.convert(lv);

View File

@@ -29,13 +29,12 @@ import picante.math.vectorspace.UnwritableVectorIJK;
import java.awt.geom.Point2D;
/**
* Implement a stereographic projection. This is package private so that there's no conflict with
* anything in the cartography package. Based on Snyder (1987).
* Implement a stereographic projection. Based on Snyder (1987).
*
* @author nairah1
*
*/
class StereographicProjection {
public class StereographicProjection {
private final double centerLat, centerLon;
private final double sinCenterLat, cosCenterLat;
@@ -47,7 +46,7 @@ class StereographicProjection {
*
* @param center projection center
*/
StereographicProjection(LatitudinalVector center) {
public StereographicProjection(LatitudinalVector center) {
this(1.0, center);
}
@@ -58,7 +57,7 @@ class StereographicProjection {
* @param R scale
* @param center projection center
*/
StereographicProjection(double R, LatitudinalVector center) {
public StereographicProjection(double R, LatitudinalVector center) {
this.R = R;
this.centerLat = center.getLatitude();
this.centerLon = center.getLongitude();

View File

@@ -23,13 +23,20 @@
package terrasaur.utils;
import static org.junit.Assert.assertEquals;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import picante.mechanics.EphemerisID;
import picante.spice.fov.FOV;
import picante.spice.fov.FOVFactory;
import terrasaur.utils.spice.SpiceBundle;
public class SumFileTest {
@@ -38,19 +45,63 @@ public class SumFileTest {
File file = ResourceUtils.writeResourceToFile("/M605862153F5.SUM");
Assert.assertNotNull(file);
List<String> lines = FileUtils.readLines(file, Charset.defaultCharset());
SumFile sumFile = SumFile.fromLines(lines);
assertEquals(Vector3D.angle(sumFile.frustum1(),
new Vector3D(0.4395120989622179, 0.898199076601014, -0.008217886523401116)), 0, 1E-10);
assertEquals(Vector3D.angle(sumFile.frustum2(),
new Vector3D(0.43798867001719116, 0.8968954485311167, 0.06119215097329732)), 0, 1E-10);
assertEquals(Vector3D.angle(sumFile.frustum3(),
new Vector3D(0.3761137519676121, 0.926529032684429, -0.009077288896014253)), 0, 1E-10);
assertEquals(Vector3D.angle(sumFile.frustum4(),
new Vector3D(0.37459032302258627, 0.9252254046145307, 0.060332748600675515)), 0, 1E-10);
assertEquals(
0,
Vector3D.angle(
sumFile.frustum1(),
new Vector3D(0.4395120989622179, 0.898199076601014, -0.008217886523401116)),
1E-10);
assertEquals(
0,
Vector3D.angle(
sumFile.frustum2(),
new Vector3D(0.43798867001719116, 0.8968954485311167, 0.06119215097329732)),
1E-10);
assertEquals(
0,
Vector3D.angle(
sumFile.frustum3(),
new Vector3D(0.3761137519676121, 0.926529032684429, -0.009077288896014253)),
1E-10);
assertEquals(
0,
Vector3D.angle(
sumFile.frustum4(),
new Vector3D(0.37459032302258627, 0.9252254046145307, 0.060332748600675515)),
1E-10);
}
@Ignore
@Test
public void testSPICE() {
SumFile sumFile1 =
SumFile.fromFile(
new File(
"/project/sis/users/nairah1/SBCLT/tickets/52-create-sbmt-structure-file/spice/D717505942G0.SUM"));
SpiceBundle bundle =
new SpiceBundle.Builder()
.addMetakernels(List.of("/project/dart/data/SPICE/dra/mk/d520_v02_N0066.tm"))
.build();
EphemerisID observer = bundle.getObject("DART");
EphemerisID target = bundle.getObject("DIMORPHOS");
FOV fov = new FOVFactory(bundle.getKernelPool()).create(-135101);
double t = bundle.getTimeConversion().utcStringToTDB("2022 SEP 26 23:14:23.328");
SumFile sumFile2 =
SumFile.fromFile(
new File(
"/project/sis/users/nairah1/SBCLT/tickets/52-create-sbmt-structure-file/spice/D717506133G0.SUM"));
SumFile sumFile3 = sumFile1.fromSpice(bundle, observer, target, fov.getFrameID(), t);
System.out.println(sumFile1);
System.out.println(sumFile2);
System.out.println(sumFile3);
}
}